图2 Demo效果,测试各种属性值
注意,本控件非继承自ViewGroup,而是纯自定义View实现。理由如下:
<!--使用默认UI属性--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" app:maxCount="3"/> <!--设置了两圆间距--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" app:count="3" app:gapBetweenCircle="90dp" app:maxCount="99"/> <!--仿饿了么--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btnEle" android:layout_width="wrap_content" android:layout_height="wrap_content" app:addEnableBgColor="#3190E8" app:addEnableFgColor="#ffffff" app:hintBgColor="#3190E8" app:hintBgRoundValue="15dp" app:hintFgColor="#ffffff" app:maxCount="99"/>注意:
onDelClick()和onAddClick()
方法,并在合适的时机回调onCountAddSuccess()和onCountDelSuccess()
以执行动画。name | format | description | 中文解释 |
---|---|---|---|
isAddFillMode | boolean | Plus button is opened Fill mode default is stroke (false) | 加按钮是否开启fill模式 默认是stroke(false) |
addEnableBgColor | color | The background color of the plus button | 加按钮的背景色 |
addEnableFgColor | color | The foreground color of the plus button | 加按钮的前景色 |
addDisableBgColor | color | The background color when the button is not available | 加按钮不可用时的背景色 |
addDisableFgColor | color | The foreground color when the button is not available | 加按钮不可用时的前景色 |
isDelFillMode | boolean | Plus button is opened Fill mode default is stroke (false) | 减按钮是否开启fill模式 默认是stroke(false) |
delEnableBgColor | color | The background color of the minus button | 减按钮的背景色 |
delEnableFgColor | color | The foreground color of the minus button | 减按钮的前景色 |
delDisableBgColor | color | The background color when the button is not available | 减按钮不可用时的背景色 |
delDisableFgColor | color | The foreground color when the button is not available | 减按钮不可用时的前景色 |
radius | dimension | The radius of the circle | 圆的半径 |
circleStrokeWidth | dimension | The width of the circle | 圆圈的宽度 |
lineWidth | dimension | The width of the line (+ - sign) | 线(+ - 符号)的宽度 |
gapBetweenCircle | dimension | The spacing between two circles | 两个圆之间的间距 |
numTextSize | dimension | The textSize of draws the number | 绘制数量的textSize |
maxCount | integer | max count | 最大数量 |
count | integer | current count | 当前数量 |
hintText | string | The hint text when number is 0 | 数量为0时,hint文字 |
hintBgColor | color | The hint background when number is 0 | 数量为0时,hint背景色 |
hintFgColor | color | The hint foreground when number is 0 | 数量为0时,hint前景色 |
hingTextSize | dimension | The hint text size when number is 0 | 数量为0时,hint文字大小 |
hintBgRoundValue | dimension | The background fillet value when number is 0 | 数量为0时,hint背景圆角值 |
@Override protected void onDraw(Canvas canvas) { if (isHintMode) {//hint 展开//背景mHintPaint.setColor(mHintBgColor);RectF rectF = new RectF(mLeft, mTop , mWidth - mCircleWidth, mHeight - mCircleWidth);canvas.drawRoundRect(rectF, mHintBgRoundValue, mHintBgRoundValue, mHintPaint);//前景文字mHintPaint.setColor(mHintFgColor);// 计算Baseline绘制的起点X轴坐标int baseX = (int) (mWidth / 2 - mHintPaint.measureText(mHintText) / 2);// 计算Baseline绘制的Y坐标int baseY = (int) ((mHeight / 2) - ((mHintPaint.descent() + mHintPaint.ascent()) / 2));canvas.drawText(mHintText, baseX, baseY, mHintPaint); } else {//左边//背景 圆if (mCount > 0) {mDelPaint.setColor(mDelEnableBgColor);} else {mDelPaint.setColor(mDelDisableBgColor);}mDelPaint.setStrokeWidth(mCircleWidth);mDelPath.reset();mDelPath.addCircle(mLeft + mRadius, mTop + mRadius, mRadius, Path.Direction.CW);mDelRegion.setPath(mDelPath, new Region(mLeft, mTop, mWidth - getPaddingRight(), mHeight - getPaddingBottom()));canvas.drawPath(mDelPath, mDelPaint);//前景 -if (mCount > 0) {mDelPaint.setColor(mDelEnableFgColor);} else {mDelPaint.setColor(mDelDisableFgColor);}mDelPaint.setStrokeWidth(mLineWidth);canvas.drawLine(-mRadius / 2, 0, +mRadius / 2, 0, mDelPaint);//数量//是没有动画的普通写法,x left, y baseLinecanvas.drawText(mCount + "", mLeft + mRadius * 2, mTop + mRadius - (mFontMetrics.top + mFontMetrics.bottom) / 2, mTextPaint);//右边//背景 圆if (mCount < mMaxCount) {mAddPaint.setColor(mAddEnableBgColor);} else {mAddPaint.setColor(mAddDisableBgColor);}mAddPaint.setStrokeWidth(mCircleWidth);float left = mLeft + mRadius * 2 + mGapBetweenCircle;mAddPath.reset();mAddPath.addCircle(left + mRadius, mTop + mRadius, mRadius, Path.Direction.CW);mAddRegion.setPath(mAddPath, new Region(mLeft, mTop, mWidth - getPaddingRight(), mHeight - getPaddingBottom()));canvas.drawPath(mAddPath, mAddPaint);//前景 +if (mCount < mMaxCount) {mAddPaint.setColor(mAddEnableFgColor);} else {mAddPaint.setColor(mAddDisableFgColor);}mAddPaint.setStrokeWidth(mLineWidth);canvas.drawLine(left + mRadius / 2, mTop + mRadius, left + mRadius / 2 + mRadius, mTop + mRadius, mAddPaint);canvas.drawLine(left + mRadius, mTop + mRadius / 2, left + mRadius, mTop + mRadius / 2 + mRadius, mAddPaint); } }根据isHintMode 布尔值变量,区分是绘制第二层(Hint层)或者第一层(加减按钮层)。
canvas.drawRoundRect
,绘制圆角矩形,然后canvas.drawText
绘制hint。 /** * Return true if the region contains the specified point */ public native boolean contains(int x, int y);知道了这一点,再写这部分代码就相当简单了:
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) {case MotionEvent.ACTION_DOWN://hint模式if (isHintMode) { onAddClick(); return true;} else { if (mAddRegion.contains((int) event.getX(), (int) event.getY())) { onAddClick(); return true; } else if (mDelRegion.contains((int) event.getX(), (int) event.getY())) { onDelClick(); return true; }}break;case MotionEvent.ACTION_MOVE:break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:break; } return super.onTouchEvent(event); }hint模式时,我们可以认为控件所有范围都是“+”的有效区域。
onDelClick()和onAddClick()
方法,并在合适的时机回调onCountAddSuccess()和onCountDelSuccess()
以执行动画。protected void onDelClick() { if (mCount > 0) {mCount--;onCountDelSuccess(); } } protected void onAddClick() { if (mCount < mMaxCount) {mCount++;onCountAddSuccess(); } else { } } /** * 数量增加成功后,使用者回调 */ public void onCountAddSuccess() { if (mCount == 1) {cancelAllAnim();mAnimReduceHint.start(); } else {mAnimFraction = 0;invalidate(); } } /** * 数量减少成功后,使用者回调 */ public void onCountDelSuccess() { if (mCount == 0) {cancelAllAnim();mAniDel.start(); } else {mAnimFraction = 0;invalidate(); } }动画的实现
//动画的基准值 动画:减 0~1, 加 1~0// 普通状态下是0 protected float mAnimFraction; //提示语收缩动画 0-1 展开1-0 //普通模式时,应该是1, 只在 isHintMode true 才有效 protected float mAnimExpandHintFraction;依次分析有哪些动画:
//背景mHintPaint.setColor(mHintBgColor);RectF rectF = new RectF(mLeft + (mWidth - mRadius * 2) * mAnimExpandHintFraction, mTop , mWidth - mCircleWidth, mHeight - mCircleWidth);canvas.drawRoundRect(rectF, mHintBgRoundValue, mHintBgRoundValue, mHintPaint);减按钮动画
//动画 mAnimFraction :减 0~1, 加 1~0 ,//动画位移Max,float animOffsetMax = (mRadius * 2 +mGapBetweenCircle);//透明度动画的基准int animAlphaMax = 255;int animRotateMax = 360;//左边//背景 圆mDelPaint.setAlpha((int) (animAlphaMax * (1 - mAnimFraction)));mDelPath.reset();//改变圆心的X坐标,实现位移mDelPath.addCircle(animOffsetMax * mAnimFraction + mLeft + mRadius, mTop + mRadius, mRadius, Path.Direction.CW);canvas.drawPath(mDelPath, mDelPaint);对于前景的“-”号来说,旋转、位移、透明度都需要做。
canvas.translate() canvas.rotate
做旋转和位移动画,别忘了 canvas.save()
和 canvas.restore()
恢复画布的状态。(透明度在上面已经设置过了。) //前景 -//旋转动画canvas.save();canvas.translate(animOffsetMax * mAnimFraction + mLeft + mRadius, mTop + mRadius);canvas.rotate((int) (animRotateMax * (1 - mAnimFraction)));canvas.drawLine(-mRadius / 2, 0, +mRadius / 2, 0, mDelPaint);canvas.restore();数量的动画
canvas.translate() canvas.rotate
做旋转和位移动画。 //数量canvas.save();//平移动画canvas.translate(mAnimFraction * (mGapBetweenCircle / 2 - mTextPaint.measureText(mCount + "") / 2 + mRadius), 0);//旋转动画,旋转中心点,x 是绘图中心,y 是控件中心canvas.rotate(360 * mAnimFraction,mGapBetweenCircle / 2 + mLeft + mRadius * 2 , mTop + mRadius);//透明度动画mTextPaint.setAlpha((int) (255 * (1 - mAnimFraction)));//是没有动画的普通写法,x left, y baseLinecanvas.drawText(mCount + "", mGapBetweenCircle / 2 - mTextPaint.measureText(mCount + "") / 2 + mLeft + mRadius * 2, mTop + mRadius - (mFontMetrics.top + mFontMetrics.bottom) / 2, mTextPaint);canvas.restore();动画的定义:
//动画 + mAnimAdd = ValueAnimator.ofFloat(1, 0); mAnimAdd.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mAnimFraction = (float) animation.getAnimatedValue();invalidate();} }); mAnimAdd.setDuration(350); //提示语收缩动画 0-1 mAnimReduceHint = ValueAnimator.ofFloat(0, 1); mAnimReduceHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mAnimExpandHintFraction = (float) animation.getAnimatedValue();invalidate();} }); mAnimReduceHint.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mCount == 1) { //然后底色也不显示了 isHintMode = false;}if (mCount == 1) { Log.d(TAG, "现在还是1 开始收缩动画"); if (mAnimAdd != null && !mAnimAdd.isRunning()) { mAnimAdd.start(); }}}@Overridepublic void onAnimationStart(Animator animation) {if (mCount == 1) { //先不显示文字了 isShowHintText = false;}} }); mAnimReduceHint.setDuration(350); //动画 - mAniDel = ValueAnimator.ofFloat(0, 1); mAniDel.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mAnimFraction = (float) animation.getAnimatedValue();invalidate();} }); //1-0的动画 mAniDel.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mCount == 0) { Log.d(TAG, "现在还是0onAnimationEnd() called with: animation = [" + animation + "]"); if (mAnimExpandHint != null && !mAnimExpandHint.isRunning()) { mAnimExpandHint.start(); }}} }); mAniDel.setDuration(350); //提示语展开动画 //分析这个动画,最初是个圆。 就是left 不断减小 mAnimExpandHint = ValueAnimator.ofFloat(1, 0); mAnimExpandHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mAnimExpandHintFraction = (float) animation.getAnimatedValue();invalidate();} }); mAnimExpandHint.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mCount == 0) { isShowHintText = true;}}@Overridepublic void onAnimationStart(Animator animation) {if (mCount == 0) { isHintMode = true;}} }); mAnimExpandHint.setDuration(350);针对复用机制的处理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int wMode = MeasureSpec.getMode(widthMeasureSpec); int wSize = MeasureSpec.getSize(widthMeasureSpec); int hMode = MeasureSpec.getMode(heightMeasureSpec); int hSize = MeasureSpec.getSize(heightMeasureSpec); switch (wMode) {case MeasureSpec.EXACTLY:break;case MeasureSpec.AT_MOST://不超过父控件给的范围内,自由发挥int computeSize = (int) (getPaddingLeft() + mRadius * 2 +mGapBetweenCircle + mRadius * 2 + getPaddingRight() + mCircleWidth * 2);wSize = computeSize < wSize ? computeSize : wSize;break;case MeasureSpec.UNSPECIFIED://自由发挥computeSize = (int) (getPaddingLeft() + mRadius * 2 + mGapBetweenCircle + mRadius * 2 + getPaddingRight() + mCircleWidth * 2);wSize = computeSize;break; } switch (hMode) {case MeasureSpec.EXACTLY:break;case MeasureSpec.AT_MOST:int computeSize = (int) (getPaddingTop() + mRadius * 2 + getPaddingBottom() + mCircleWidth * 2);hSize = computeSize < hSize ? computeSize : hSize;break;case MeasureSpec.UNSPECIFIED:computeSize = (int) (getPaddingTop() + mRadius * 2 + getPaddingBottom() + mCircleWidth * 2);hSize = computeSize;break; } setMeasuredDimension(wSize, hSize); //复用时会走这里,所以初始化一些UI显示的参数 mAnimFraction = 0; initHintSettings(); } /** * 根据当前count数量 初始化 hint提示语相关变量 */ private void initHintSettings() { if (mCount == 0) {isHintMode = true;isShowHintText = true;mAnimExpandHintFraction = 0; } else {isHintMode = false;isShowHintText = false;mAnimExpandHintFraction = 1; } }在改变count时
/** * 设置当前数量 * @param count * @return */ public AnimShopButton setCount(int count) { mCount = count; //先暂停所有动画 if (mAnimAdd != null && mAnimAdd.isRunning()) {mAnimAdd.cancel(); } if (mAniDel != null && mAniDel.isRunning()) {mAniDel.cancel(); } //复用机制的处理 if (mCount == 0) {// 0 不显示 数字和-号mAnimFraction = 1; } else {mAnimFraction = 0; } initHintSettings(); return this; }总结