首先感謝關於仿寫者劉金偉:
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對你有幫助的話順手給個星吧!
碼字不易,期待各位的讚賞!!!