仿即刻的点赞滚动放大波纹图标

综合技术 2017-12-07

首先感谢关于仿写者刘金伟:

https://github.com/arvinljw/ThumbUpSample的作者,从中收到了启发。

先来看一张效果图(没图说个蛋蛋)

大体思路:

上面这个控件PraiseView我把它拆成了两部分:一个左边的ImageView这个点击的时候会有放大的动画,比较简单。右边的那个控件ScrollTextView复制数字加减进位,文字的滚动。这样的好处是避免复杂的尺寸计算以及绘制逻辑,同时拆成两个代码不会显得过于冗长,便于理解。

关键代码解析:

public class PraiseView extends LinearLayout implements View.OnClickListener {
    private static final int DIP_8 = DisplayUtil.dip2px(8);
    /**
     * 默认的padding为缩放动画留出空间
     */
    private final static int PADDING = DIP_8;

    private ImageView mImageView;
    private ScrollTextView mScrollTextView;
    private Drawable mPraiseDrawable;
    private Drawable mUnPraiseDrawable;
    private int mTextSize;
    private int mTextColor;
    public boolean mCanClick = true;
    private AnimatorSet mAnimatorSet;
    private int mLikeCount;
    private boolean mIsLiked;
    //圆的半径
    private int mCircleMaxRadius;
    //园的颜色
    private int mCircleColor = Color.parseColor("#E73256");
    private Paint mCirclePaint = new Paint();
    private int mCurrentRadius = 0;
    private IPraiseListener mIPraiseListener;
    private ValueAnimator valueAnimator;


    public PraiseView(Context context) {
        this(context, null);
    }

    public PraiseView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PraiseView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    private void initView(Context context, @Nullable AttributeSet attrs) {
        View.inflate(context, R.layout.layout_praise_view, this);
        setOrientation(HORIZONTAL);
        setGravity(Gravity.CENTER_VERTICAL);
        setPadding(PADDING, PADDING, PADDING, PADDING);
        setOnClickListener(this);
        mImageView = findViewById(R.id.iv_praise);
        mScrollTextView = findViewById(R.id.scroll_text_praise);

        TypedArray attrArray = context.obtainStyledAttributes(attrs, R.styleable.PraiseView);
        mTextSize = attrArray.getDimensionPixelSize(R.styleable.PraiseView_pv_textSize, DisplayUtil.sp2px(12));
        mTextColor = attrArray.getColor(R.styleable.PraiseView_pv_textColor, Color.parseColor("#757575"));
        mPraiseDrawable = attrArray.getDrawable(R.styleable.PraiseView_pv_praise_imageSrc);
        mUnPraiseDrawable = attrArray.getDrawable(R.styleable.PraiseView_pv_unPraise_imageSrc);
        attrArray.recycle();

        initView();
    }

    private void initView() {
        if (mPraiseDrawable == null) {
            mPraiseDrawable = getResources().getDrawable(R.mipmap.icon_praise_orange);
        }
        if (mUnPraiseDrawable == null) {
            mUnPraiseDrawable = getResources().getDrawable(R.mipmap.icon_un_praise_gray);
        }
        mImageView.setImageDrawable(mIsLiked ? mPraiseDrawable : mUnPraiseDrawable);
        mScrollTextView.setTextColorAndSize(mTextColor, mTextSize);
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.STROKE);
        mCirclePaint.setStrokeWidth(DisplayUtil.dip2px(2));
    }


    public void bindData(IPraiseListener praiseListener, boolean isLike, int likeCount) {
        mLikeCount = likeCount;
        mIPraiseListener = praiseListener;
        setLiked(isLike);
        refreshText(likeCount);
    }

    void refreshText(int likeCount) {
        mScrollTextView.bindData(likeCount > 0 ? likeCount : 0);

    }

    public void setLiked(boolean isLike) {
        mIsLiked = isLike;
        mImageView.setImageDrawable(isLike ? mPraiseDrawable : mUnPraiseDrawable);
    }


    public void clickLike() {
        setLiked(!mIsLiked);

        if (mAnimatorSet == null) {
            mAnimatorSet = generateScaleAnim(mImageView, 1f, 1.3f, 0.9f, 1f);
        } else {
            mAnimatorSet.cancel();
        }
        mAnimatorSet.start();
        if (mIsLiked) {
            mLikeCount++;
        } else if (mLikeCount > 0) {
            mLikeCount--;
        }
        mIPraiseListener.like(mIsLiked, mLikeCount);
        mScrollTextView.bindDataWithAnim(mLikeCount);


    }


    @Override
    public void onClick(View v) {
        if (!mCanClick) return;
        clickLike();
        generateCircleAnim();

    }

    /**
     * 生成一个缩放动画 X轴和Y轴
     *
     * @param view       需要播放动画的View
     * @param scaleValue 缩放轨迹
     * @return AnimatorSet 动画对象
     */
    public static AnimatorSet generateScaleAnim(View view, float... scaleValue) {
        AnimatorSet animatorSet = new AnimatorSet();
        ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, View.SCALE_X, scaleValue);
        animatorX.setDuration(600);

        ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, View.SCALE_Y, scaleValue);
        animatorY.setDuration(600);

        List animatorList = new ArrayList(2);
        animatorList.add(animatorX);
        animatorList.add(animatorY);
        animatorSet.playTogether(animatorList);
        return animatorSet;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCurrentRadius, mCirclePaint);
    }

    /**
     * 计算波纹动画的最大半径
     */
    private void calculateRadius() {
        mCircleMaxRadius = Math.min(getWidth(), getHeight()) / 2 - DIP_8;
    }

    public interface IPraiseListener {
        void like(boolean isPraise, int praiseCount);
    }

    /***
     * 波纹动画
     */
    private void generateCircleAnim() {
        calculateRadius();
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
        }
        valueAnimator = ValueAnimator.ofInt(0, mCircleMaxRadius);
        valueAnimator.setDuration(400);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentRadius = (int) animation.getAnimatedValue();
                if (mCurrentRadius >= mCircleMaxRadius) {
                    mCurrentRadius = 0;
                }
                mCirclePaint.setColor(ColorUtils.setAlphaComponent(mCircleColor, (int) ((mCircleMaxRadius - mCurrentRadius) * 1.0f / mCircleMaxRadius * 255)));
                invalidate();
            }
        });
        valueAnimator.start();
    }
}

可以看到PraiView继承了LinearLayout,因此不需要进行复杂的尺寸和绘制,使用默认的就好了。 /***

* 波纹动画
     */
    private void generateCircleAnim() {
        calculateRadius();
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
        }
        valueAnimator = ValueAnimator.ofInt(0, mCircleMaxRadius);
        valueAnimator.setDuration(400);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentRadius = (int) animation.getAnimatedValue();
                if (mCurrentRadius >= mCircleMaxRadius) {
                    mCurrentRadius = 0;
                }
                mCirclePaint.setColor(ColorUtils.setAlphaComponent(mCircleColor, (int) ((mCircleMaxRadius - mCurrentRadius) * 1.0f / mCircleMaxRadius * 255)));
                invalidate();
            }
        });
        valueAnimator.start();
    }
}

通过ValueAnimator不断改变圆的半径,进行不断的重绘,形成了点击波纹扩散的效果,注意的是:

@Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCurrentRadius, mCirclePaint);
    }

画波纹的代码一定要放在super.dispatchDraw(canvas);操作的后面,也就是说波纹是前景,这样会更加美观,否则就成了背景,另外随州波纹的扩撒波纹的颜色逐渐透明这里用了ColorUtils.setAlphaComponent()。

接下来看下ScrollTextView这个控件,有两个比较重要的点:

  1. 怎么处理进位退位?/**
    * 计算不变,原来,和改变后各部分的数字
     * 这里是只针对加一和减一去计算的算法,因为直接设置的时候没有动画
     */
    private void calculateChangeNum(int change) {
        mChange = change;
        if (change == 0) {
            mChangeNumbers[0] = String.valueOf(mOriginValue);
            mChangeNumbers[1] = "";
            mChangeNumbers[2] = "";
            return;
        }
        toBigger = change > 0;
        String oldNum = String.valueOf(mOriginValue);
        String newNum = String.valueOf(mOriginValue + change);
    
        int oldNumLen = oldNum.length();
    
        if (isLengthDifferent(mOriginValue, mOriginValue + change)) {
            mChangeNumbers[0] = "";
            mChangeNumbers[1] = oldNum;
            mChangeNumbers[2] = newNum;
        } else {
            for (int i = 0; i < oldNumLen; i++) {
                char oldC1 = oldNum.charAt(i);
                char newC1 = newNum.charAt(i);
                if (oldC1 != newC1) {
                    if (i == 0) {
                        mChangeNumbers[0] = "";
                    } else {
                        mChangeNumbers[0] = newNum.substring(0, i);
                    }
                    mChangeNumbers[1] = oldNum.substring(i);
                    mChangeNumbers[2] = newNum.substring(i);
                    break;
                }
            }
        }
        mOriginValue = mOriginValue + change;
    
    
    }

这里采用一个长度为3的数组存放不变的数字、原来的数字、变化后的数字。例如:

87到88,那么数组的元素为"8","7","8";99到100,那么数组的元素为"","99","100"。不变的数字在draw的时候直接花一次就好了,原来的数字和变化后的数字需要不断改变Y值形成滚动的动画。

private void drawText(Canvas canvas) {
    Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
    float y = (getHeight() - fontMetrics.bottom - fontMetrics.top) / 2;
    canvas.drawText(String.valueOf(mChangeNumbers[0]), mStartX, y, mTextPaint);
    if (mChange != 0) {
        //字体滚动
        float fraction = (mTextSize - Math.abs(mOldOffsetY)) / mTextSize;
        Log.e("drawText", "drawText" + fraction);
        mTextPaint.setColor(ColorUtils.setAlphaComponent(mTextColor, (int) (fraction * 255)));
        canvas.drawText(String.valueOf(mChangeNumbers[1]), mSingleTextWidth * mChangeNumbers[0].length() + mStartX, y + mOldOffsetY, mTextPaint);
        mTextPaint.setColor(ColorUtils.setAlphaComponent(mTextColor, (int) ((1 - fraction) * 255)));
        canvas.drawText(String.valueOf(mChangeNumbers[2]), mSingleTextWidth * mChangeNumbers[0].length() + mStartX, y + mNewOffsetY, mTextPaint);
    }

}

值得注意的是这里:

private int getContentWidth() {
    /**
     * 加1为了防止进位时宽度不够显示不下
     */
    return (int) (getPaddingRight() + getPaddingLeft() + mSingleTextWidth * (String.valueOf(mOriginValue).length() + 1));
}

控件的宽度为当前字符的宽度再加一个字符宽度,避免发生进位时显示不全的问题。

代码github 对你有帮助的话顺手给个星吧!

您可能感兴趣的

Unable to get leading business name in Android Lol... I use the following code to get the activity name of the foreground app in the variable foregroundTaskPackageName . It works on all OS versions be...
Android开发常用的插件及工具 1、 GitHub ,这个不管是做安卓还是其他,只要是开发就必上的网站,也是天朝没有墙掉为数不多的网站 2、 Stack OverFlow ,这个和上面一样,国外非常著名的问答网站,在上面基本上很多问题都可以得到解决 3、 Genymotion 模拟器,搞开发...
Parsing Data Binding Errors Learning something new is always fun and exciting. That is, until seemingly cryptic error messages start creeping up. For the past year, I have bee...
Android Ble蓝牙开发(客户端) 最近项目里面需要集成一个蓝牙的连接功能,作为一枚刚刚毕业不久的新生,大学几年又白过的。只好在几天内搜搜百度,脑补一下。文章部分内容摘至各大Blog,加上本dust的见解,写了一份Client端和Service端的小呆毛。 参考链接–Link: http://m.blog.csdn.net...
Android性能优化:手把手带你全面了解 绘制优化... 前言 在 Android 开发中,性能优化策略十分重要 本文主要讲解性能优化中的 绘制优化 ,希望你们会喜欢。 目录 1. 影响的性能 绘制性能的好坏 主要影响 : Android 应用中的 页面显示速度 2. 如何影响性能 绘制影...