首先感谢关于仿写者刘金伟:
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这个控件,有两个比较重要的点:
- 怎么处理进位退位?/**
* 计算不变,原来,和改变后各部分的数字 * 这里是只针对加一和减一去计算的算法,因为直接设置的时候没有动画 */ 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对你有帮助的话顺手给个星吧!
码字不易,期待各位的赞赏!!!