Android 自定义View之咖啡杯动画

效果

CoffeeView

CoffeeView

大概思路

  • 自定义view,直接继承view
  • 复写onSizeChanged()方法,在此计算杯垫,杯子,烟雾效果的path
  • 在onDraw()方法中,描绘杯垫,杯子
  • 处理烟雾动画效果

画杯子

这里需要画两部分内容,第一部分是杯子,第二部分是杯耳(提手的地方)

我们可以使用addRoundRect来描绘圆角矩形,并且可指定每个圆角的半径即圆角的程度

/**
 * Add a closed round-rectangle contour to the path. Each corner receives
 * two radius values [X, Y]. The corners are ordered top-left, top-right,
 * bottom-right, bottom-left
 *
 * @param rect The bounds of a round-rectangle to add to the path
 * @param radii Array of 8 values, 4 pairs of [X,Y] radii
 * @param dir  The direction to wind the round-rectangle's contour
 */
public void addRoundRect(RectF rect, float[] radii, Direction dir) {
    if (rect == null) {
        throw new NullPointerException("need rect parameter");
    }
    addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
}

以下代码注释:

//计算view的中心点坐标
mCenterX = w / 2;
mCenterY = h / 2;

//杯子的宽,为view的宽度的2/3
float cupWidth  = w * 2 / 3f;

//杯子的高,为view的高度3/8
float cupHeight = (h / 2) * 3 / 4f;

//计算出杯子的中心点坐标
float cupCenterX = mCenterX;
float cupCenterY = mCenterY + cupHeight / 2;

//计算杯子矩形的左上右上的圆角半径
float cupTopRoundRadius = Math.min(cupWidth, cupHeight) / 20f;

//计算杯子矩形的左下右下的圆角半径
float cupBottomRoundRadius = cupTopRoundRadius * 10;

//重置杯子path
mCupPath.reset();

//添加杯子(杯身)轨迹
mCupPath.addRoundRect(new RectF(cupCenterX - cupWidth / 2, cupCenterY - cupHeight / 2 - cupHeight / 10, cupCenterX + cupWidth / 2, cupCenterY + cupHeight / 2), new float[]{cupTopRoundRadius, cupTopRoundRadius, cupTopRoundRadius, cupTopRoundRadius,              cupBottomRoundRadius, cupBottomRoundRadius, cupBottomRoundRadius, cupBottomRoundRadius}, Path.Direction.CW);

//计算杯耳宽度
float cupEarWidth  = (w - cupWidth) * 3 / 4f;
//计算杯耳高度
float cupEarHeight = cupHeight / 3;

//计算杯耳的中心点坐标
float cupEarCenterX = mCenterX + cupWidth / 2;
float cupEarCenterY = mCenterY + cupHeight / 2;

//计算杯耳的圆角半径
float cupEarRoundRadius = Math.min(cupEarWidth, cupEarHeight) / 2f;

//设置杯耳画笔的描边宽度
mCupEarPaint.setStrokeWidth(Math.min(cupEarWidth, cupEarHeight) / 3f);

//重置杯耳path
mCupEarPath.reset();

//添加杯耳轨迹
mCupEarPath.addRoundRect(new RectF(cupEarCenterX - cupEarWidth / 2, cupEarCenterY - cupEarHeight / 2 - cupHeight / 10, cupEarCenterX + cupEarWidth / 2,
                cupEarCenterY + cupEarHeight / 2),
        new float[]{cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius,
                cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius}, Path.Direction.CW);

在onDraw方法中

canvas.drawPath(mCupEarPath, mCupEarPaint);
canvas.drawPath(mCupPath, mCupPaint);

画杯垫

首先计算杯垫path轨迹

//计算杯垫宽度
float coasterWidth = cupWidth;
//计算杯垫高度
float coasterHeight = (h / 2 - cupHeight) * 1 / 3f;

//计算杯垫中心点坐标
float coasterCenterX = mCenterX;
float coasterCenterY = mCenterY + cupHeight + (h / 2 - cupHeight) / 2f;

//计算杯垫圆角半径
float coasterRoundRadius = Math.min(coasterWidth, coasterHeight) / 2f;

//重置杯垫path
mCoasterPath.reset();

//添加杯垫轨迹
mCoasterPath.addRoundRect(new RectF(coasterCenterX - coasterWidth / 2, coasterCenterY - coasterHeight / 2,
                coasterCenterX + coasterWidth / 2, coasterCenterY + coasterHeight / 2),
        coasterRoundRadius, coasterRoundRadius, Path.Direction.CW);

在onDraw方法中

canvas.drawPath(mCoasterPath, mCoasterPaint);

画烟雾

烟雾原理

  • 根据贝塞尔曲线添加波浪轨迹
  • 根据LinearGradient实现颜色渐变效果

每条烟雾大概如下效果
CoffeeView

当移动至烟雾底部的时候,重新将其移动至头部,这样循环动画,就会显示无线的滚动效果

代码注释如下:

//计算烟雾的宽度
float vaporsStrokeWidth = cupWidth / 15f;

//计算烟雾相隔距离大小
float vaporsGapWidth = (cupWidth - VAPOR_COUNT * vaporsStrokeWidth) / 4f;
mVaporsHeight   = cupHeight * 4 / 5f;

//设置烟雾画笔描边大小
mVaporPaint.setStrokeWidth(vaporsStrokeWidth);
float startX, startY, stopX, stopY;

//设置渐变效果
LinearGradient linearGradient = new LinearGradient(mCenterX, mCenterY, mCenterX, mCenterY - mVaporsHeight,
        new int[]{mVaporColor, Color.TRANSPARENT},
        null, Shader.TileMode.CLAMP);
        
//烟雾画笔增加渐变效果的渲染器
mVaporPaint.setShader(linearGradient);

//增加每条烟雾的path的贝塞尔波浪
for (int i = 0; i < VAPOR_COUNT; i++) {
    mVaporsPath[i].reset();

    startX = (mCenterX - cupWidth / 2) + vaporsStrokeWidth / 2 + i * vaporsStrokeWidth + (i + 1) * vaporsGapWidth;
    startY = mCenterY + mVaporsHeight;

    stopX = startX;
    stopY = mCenterY - mVaporsHeight;


    mVaporsPath[i].moveTo(startX, startY);
    mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, startY - mVaporsHeight / 4,
            startX, startY - mVaporsHeight / 2);

    mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, startY - mVaporsHeight * 3 / 4,
            startX, mCenterY);

    mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, mCenterY - mVaporsHeight / 4,
            startX, mCenterY - mVaporsHeight / 2);

    mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, mCenterY - mVaporsHeight * 3 / 4,
            stopX, stopY);

    //add twice the bezier curve
    mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, stopY - mVaporsHeight / 4,
            startX, stopY - mVaporsHeight / 2);

    mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, stopY - mVaporsHeight * 3 / 4,
            startX, stopY - mVaporsHeight);

    mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, stopY - mVaporsHeight - mVaporsHeight / 4,
            startX, stopY - mVaporsHeight - mVaporsHeight / 2);

    mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, stopY - mVaporsHeight - mVaporsHeight * 3 / 4,
            stopX, stopY - 2 * mVaporsHeight);
}

烟雾动画处理:
每条烟雾都有一个path记录其轨迹,利用path的transform方法可移动path

/**
 * Transform the points in this path by matrix, and write the answer
 * into dst. If dst is null, then the the original path is modified.
 *
 * @param matrix The matrix to apply to the path
 * @param dst    The transformed path is written here. If dst is null,
 *               then the the original path is modified
 */
public void transform(Matrix matrix, Path dst) {
    long dstNative = 0;
    if (dst != null) {
        dst.isSimplePath = false;
        dstNative = dst.mNativePath;
    }
    nTransform(mNativePath, matrix.native_instance, dstNative);
}
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, -vaporHeight);
    final int finalI = i;
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            mAnimatorValues[finalI] = (float) valueAnimator.getAnimatedValue();
            invalidate();
        }
    });

    valueAnimator.setDuration(1000);
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
    valueAnimator.setRepeatMode(ValueAnimator.RESTART);
    mValueAnimators.add(valueAnimator);

在onDraw方法中,描绘烟雾

private void drawVapors(Canvas canvas){
for (int i = 0; i < VAPOR_COUNT; i++){
    mCalculateMatrix.reset();
    mCalculatePath.reset();

    float animatedValue = mAnimatorValues[i];
    mCalculateMatrix.postTranslate(0, animatedValue);
    mVaporsPath[i].transform(mCalculateMatrix, mCalculatePath);

    canvas.drawPath(mCalculatePath, mVaporPaint);
}
}

我这里设置了每条烟雾的移动速度是一样的,你们可以下载源码来修改,看看不同的移动速度的效果

Github

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TypeArray 是 Android 中的一个特殊的资源类型,用于在 XML 中声明自定义 View 的属性。使用 TypeArray 可以方便地在 XML 布局中指定 View 的属性,而不需要在 Java 代码中进行硬编码。 使用 TypeArray 的步骤如下: 1. 在 res/values/attrs.xml 文件中定义定义 View 的属性。 ```xml <resources> <declare-styleable name="MyCustomView"> <attr name="customAttr1" format="integer" /> <attr name="customAttr2" format="string" /> <attr name="customAttr3" format="boolean" /> </declare-styleable> </resources> ``` 2. 在自定义 View 的构造函数中获取 TypedArray 对象,并从中获取属性值。 ```java public class MyCustomView extends View { private int customAttr1; private String customAttr2; private boolean customAttr3; public MyCustomView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView); customAttr1 = a.getInt(R.styleable.MyCustomView_customAttr1, 0); customAttr2 = a.getString(R.styleable.MyCustomView_customAttr2); customAttr3 = a.getBoolean(R.styleable.MyCustomView_customAttr3, false); a.recycle(); } } ``` 在上面的代码中,`context.obtainStyledAttributes(attrs, R.styleable.MyCustomView)` 用于获取 TypedArray 对象,`R.styleable.MyCustomView` 是在 attrs.xml 文件中定义的自定义属性集合,`a.getInt()`、`a.getString()`、`a.getBoolean()` 用于从 TypedArray 对象中获取属性值,最后需要调用 `a.recycle()` 来回收 TypedArray 对象。 3. 在 XML 布局中使用自定义 View,并设置属性值。 ```xml <com.example.MyCustomView android:layout_width="match_parent" android:layout_height="wrap_content" app:customAttr1="123" app:customAttr2="hello" app:customAttr3="true" /> ``` 在上面的代码中,`app:customAttr1`、`app:customAttr2`、`app:customAttr3` 是在 attrs.xml 文件中定义的自定义属性名,可以在 XML 布局中使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值