仿饿了么加入购物车旋转控件 - 自带闪转腾挪动画 的按钮

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”/>

注意:

加减点击后,具体的操作,要根据业务的不同来编写了,设计到实际的购物车可能还有写数据库操作,或者请求接口等,要操作成功后才执行动画、或者修改count,这一块代码每个人写法可能不同。

使用时,可以重写onDelClick()onAddClick()方法,并在合适的时机回调onCountAddSuccess()onCountDelSuccess()以执行动画。

效果图如图2.

支持的属性

| 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背景圆角值 |

这么多属性够你用了吧。

下面看重点的实现吧,Let’s Go!.

实现解剖


关于自定义View的基础,这里不再赘述。

如果阅读时有不明白的,建议下载源码边看边读,或者学习自定义View基础知识后再阅读本文。

代码传送门:喜欢的话,随手点个star。多谢

https://github.com/mcxtzhang/AnimShopButton


我们捡重点说,无非是绘制。

绘制的重点,这里分三块:

  • 静态绘制。(分两块:加减按钮和数量、hint提示文字和背景)

  • 第一层。(加减按钮和数量)以及它的旋转、位移、透明度动画

  • 第二层。(hint区域)以及它的伸展收缩动画

除了绘制以外的重点是:

  • 由于采用了完全的自定义View去实现这么一个“组合控件效果”,则点击事件的监听需要自己处理。

  • 在回收复用的列表中使用时,列表滑动,如何正确显示UI。

静态绘制

静态绘制就是最基本的自定义View知识,绘制圆圈(Circle)、线段(Line)、数字(Text)以及圆角矩形(RoundRect),值得注意的是,

要考虑到 避免overDraw和动画的需求,

我们要绘制的两层应该是互斥关系。

剥离掉动画代码,大致如下(基本都是draw代码,可以快速阅读):

@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 baseLine

canvas.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。

(如果圆角的值足够大,矩形的宽度足够小,就变成了圆形。)

绘制第一层时,要根据当前的数量选择不同的颜色,注意在绘制加减按钮的圆圈时,我们是用Path绘制的,这是因为我们还需要用Path构建Region类,这个类就是我们监听点击区域的重点

点击事件的监听


在讲解动画之前,我们先说说如何监听点击的区域,因为本控件的动画是和加减数量息息相关的,而数量的加减是由点击相应”+ - 仿饿了么加入购物车旋转控件 - 自带闪转腾挪动画 的按钮
按钮”区域触发
的。

所以我们的监听按钮的点击事件,其实就是监听相应的”+ - 按钮”区域

上一节中,我们在绘制”+ - 按钮”区域时,通过Path,构建了两个Region类,Region类有个contains(int x, int y)方法如下,通过传入对应触摸的x、y坐标,就可知道知否点击了相应区域

/**

  • 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模式时,我们可以认为控件所有范围都是“+”的有效区域

而在非hint模式时,根据上一节构建的mAddRegionmDelRegion去判断。

判断确认点击后,具体的操作,要根据业务的不同来编写了,设计到实际的购物车可能还有写数据库操作,或者请求接口等,要操作成功后才执行动画、或者修改count,这一块代码每个人写法可能不同。

使用时,可以重写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;

依次分析有哪些动画:

Hint动画

主要是圆角矩形的展开、收缩

固定right、bottom,当展开时,不断减少矩形的左起点left坐标值,则整个矩形宽度变大,呈现展开。收缩时相反。

代码:

//背景

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 baseLine

canvas.drawText(mCount + “”, mGapBetweenCircle / 2 - mTextPaint.measureText(mCount + “”) / 2 + mLeft + mRadius * 2, mTop + mRadius - (mFontMetrics.top + mFontMetrics.bottom) / 2, mTextPaint);

canvas.restore();

动画的定义:

动画是在View初始化时就定义好的,执行顺序:

  • 数量增加,0-1时,先收缩Hint(第二层)mAnimReduceHint执行,完毕后执行减按钮(第一层)进入的动画mAnimAdd

  • 数量减少,1-0时,先执行减按钮退出的动画mAniDel,再伸展Hint动画mAnimExpandHint,完毕后,显示hint文字。

代码如下:

//动画 +

mAnimAdd = ValueAnimator.ofFloat(1, 0);

mAnimAdd.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

mAnimAdd.setDuration(350);

//提示语收缩动画 0-1

mAnimReduceHint = ValueAnimator.ofFloat(0, 1);

mAnimReduceHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimExpandHintFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

mAnimReduceHint.addListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationEnd(Animator animation) {

if (mCount == 1) {

//然后底色也不显示了
imator.ofFloat(1, 0);

mAnimAdd.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

mAnimAdd.setDuration(350);

//提示语收缩动画 0-1

mAnimReduceHint = ValueAnimator.ofFloat(0, 1);

mAnimReduceHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimExpandHintFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

mAnimReduceHint.addListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationEnd(Animator animation) {

if (mCount == 1) {

//然后底色也不显示了

上一篇:canvas:绘制简单春联demo


下一篇:html + js 下载图片