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

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) {

//然后底色也不显示了

isHintMode = false;

}

if (mCount == 1) {

Log.d(TAG, “现在还是1 开始收缩动画”);

if (mAnimAdd != null && !mAnimAdd.isRunning()) {

mAnimAdd.start();

}

}

}

@Override

public void onAnimationStart(Animator animation) {

if (mCount == 1) {

//先不显示文字了

isShowHintText = false;

}

}

});

mAnimReduceHint.setDuration(350);

//动画 -

mAniDel = ValueAnimator.ofFloat(0, 1);

mAniDel.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

//1-0的动画

mAniDel.addListener(new AnimatorListenerAdapter() {

@Override

public 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() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mAnimExpandHintFraction = (float) animation.getAnimatedValue();

invalidate();

}

});

mAnimExpandHint.addListener(new Animator 《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》无偿开源 徽信搜索公众号【编程进阶路】 ListenerAdapter() {

@Override

public void onAnimationEnd(Animator animation) {

if (mCount == 0) {

isShowHintText = true;

}

}

@Override

public void onAnimationStart(Animator animation) {

if (mCount == 0) {

isHintMode = true;

}

}

});

mAnimExpandHint.setDuration(350);

针对复用机制的处理


因为我们的购物车控件肯定会用在列表中,不管你用ListView还是RecyclerView,都会涉及到复用的问题。

复用给我们带来一个麻烦的地方就是,我们要处理好一些属性状态值,否则UI上会有问题。

可以从两处下手处理:

onMeasure

列表复用时,依然会回调onMeasure()方法,所以在这里初始化一些UI显示的参数。

这里顺带将适配wrap_content 的代码也一同贴上:

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时

一般在onBindViewHolder()或者getView()时,都会对本控件重新设置count值,count改变时,当然也是需要根据count进行属性值的调整。

且此时如果View正在做动画,应该停止这些动画。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值