First of all, thanks to liu Jinwei, the copywriter:
The author of https://github.com/arvinljw/ThumbUpSample, get inspired.
Let’s take a look at a rendering (no balls in the picture)
General idea:
The control above, PraiseView, I split into two parts: an ImageView on the left which has a zoomed in animation when clicked. The control on the right, ScrollTextView, replicates the addition and subtraction of digits and text scrolling. The advantage of this is to avoid complex sizing and drawing logic, and to split the code into two simultaneously is not too tedious and easy to understand.
Key code parsing:
public class PraiseView extends LinearLayout implements View.OnClickListener { private static final int DIP_8 = DisplayUtil.dip2px(8); Private final static int padding = DIP_8; 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 (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(); } /** * Generates a scaled animation on the X and Y axes ** @param View needs to play the animated view * @param scaleValue scale track * @returnPublic 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);returnanimatorSet; } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCurrentRadius, mCirclePaint); } /** * Calculates the maximum radius of the ripple animation */ private voidcalculateRadius() { mCircleMaxRadius = Math.min(getWidth(), getHeight()) / 2 - DIP_8; } public interface IPraiseListener { void like(boolean isPraise, int praiseCount); } /*** * ripple animation */ private voidgenerateCircleAnim() {
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(); }}Copy the code
As you can see PraiView inherits the LinearLayout, so you don’t need to do complex sizing and drawing, just use the default. / * * *
* Private void */ private voidgenerateCircleAnim() {
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(); }}Copy the code
ValueAnimator constantly changes the radius of the circle and redraws it to create a ripple effect. Note that:
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCurrentRadius, mCirclePaint);
}Copy the code
The code for drawing ripples must be placed on super.dispatchDraw(canvas); Is the back of the operation, that is to say, corrugated prospects, it will be more beautiful, otherwise becomes the background, another suizhou ripples spread corrugated color gradually transparent here in ColorUtils. SetAlphaComponent ().
Now look at the ScrollTextView control. There are two important points:
- How to handle carry offsetting? / * *
* The calculation is the same, the original, and the changed parts of the number * here is only for the calculation of one and one, */ 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; }Copy the code
Here, an array of length 3 is used to store unchanged numbers, original numbers, and changed numbers. Such as:
87 to 88, so the elements of the array are “8”,”7″,”8″; 99 to 100, so the elements of the array are “”,”99″,”100″. Constant numbers in draw directly spend once, the original number and the changed number need to constantly change the Y value to form a scrolling animation.
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) {// font scrollfloat 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); }}Copy the code
It’s worth noting here:
private int getContentWidth() {/** * + 1 to prevent the display of the */ when the carry width is insufficientreturn (int) (getPaddingRight() + getPaddingLeft() + mSingleTextWidth * (String.valueOf(mOriginValue).length() + 1));
}Copy the code
The width of the control is the width of the current character plus one character width to avoid incomplete display problems in carry cases.
Code Github to help you handy to a star bar!
Code word is not easy, look forward to your appreciation!!