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

3,711 阅读4分钟

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

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<Animator> 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对你有帮助的话顺手给个星吧!

码字不易,期待各位的赞赏!!!