Android 炫酷自定义 View - 剑气加载

效果图

原理分析

这个效果仔细看,就是有三个类似月牙形状的元素进行循环转动,我们只需要拆解出一个月牙来做效果即可,最后再将三个月牙组合起来就可以达到最终效果了

月牙

先画一个圆:

再画个大一丢丢的:

再把这个大圆往右移一丢丢,裁切出来的左右两个都是月牙:

实现

老司机应该一眼就能看出来,只要在两次绘制圆中只要使用一个叠加模式就能达到裁剪出一个月牙的效果了。那么是什么模式呢,我去搜索一下~

当当当,就是它~ PorterDuff.Mode.DST_OUT

相关源码如下:

canvas.drawColor(Color.BLACK)

val layerId =
    canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG)
val halfW = width / 2f
val halfH = height / 2f
val radius = min(width, height) / 3f

paint.color = Color.WHITE
canvas.drawCircle(halfW, halfH, radius, paint)
paint.color = Color.BLACK
paint.xfermode = xfermode
canvas.drawCircle(halfW, halfH - 0.05f * radius, radius * 1.01f, paint)
canvas.restoreToCount(layerId)
paint.xfermode = null

运行起来我们就得到了一弯浅浅的月牙:

立体空间变化

我们可以看出效果图里的每一个月牙并不是那么方正,而是有一定的空间旋转,再加上绕着 Z 轴旋转。这里需要利用 Camera 与 Matrix 实现3D效果。相关知识可参考:

https://www.jianshu.com/p/34e0fe5f9e31

我们先给它在 x 轴转 35 度 ,y 轴转 -45 度。(参考开头文章的数据)

rotateMatrix.reset()
camera.save()
camera.rotateX(35F)
camera.rotateY(-45F)
camera.getMatrix(rotateMatrix)
camera.restore()
val halfW = width / 2f
val halfH = height / 2f

rotateMatrix.preTranslate(-halfW, -halfH)
rotateMatrix.postTranslate(halfW, halfH)
canvas.concat(rotateMatrix)

运行效果如下,从普通的月牙变成了帅气的剑气。

动画

我们上面做了固定角度的 X, Y 轴的旋转,这个时候我们只要加上一个 Z 轴的调转动画,这个剑气就动起来了。

val anim = ValueAnimator.ofFloat(0f, -360f).apply {
    // Z 轴是逆时针,取负数,得到顺时针的旋转
    interpolator = null
    repeatCount = RotateAnimation.INFINITE
    duration = 1000

    addUpdateListener {
        invalidate()
    }
}
// 省略前面已写代码...
camera.rotateZ(anim.animatedValue as Float)

// 在合适的地方启动动画
view.anim.start()

运行效果:

举一反三

有了这一个完整的剑气旋转,只要再来两道,组成完整的剑气加载就可以了。

将前面的代码抽象成一个方法

private fun drawSword(canvas: Canvas, rotateX: Float, rotateY: Float) {
    val layerId =
        canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG)
    rotateMatrix.reset()
    camera.save()
    camera.rotateX(rotateX)
    camera.rotateY(rotateY)
    camera.rotateZ(anim.animatedValue as Float)
    camera.getMatrix(rotateMatrix)
    camera.restore()

    val halfW = width / 2f
    val halfH = height / 2f

    rotateMatrix.preTranslate(-halfW, -halfH)
    rotateMatrix.postTranslate(halfW, halfH)
    canvas.concat(rotateMatrix)
    canvas.drawCircle(halfW, halfH, radius, paint)
    paint.xfermode = xfermode
    canvas.drawCircle(halfW, halfH - 0.05f * radius, radius * 1.01f, paint)
    canvas.restoreToCount(layerId)
    paint.xfermode = null
}

绘制三道剑气

verride fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   canvas.drawColor(Color.BLACK)
   // 偏移角度来源开关文章
   drawSword(canvas,35f, -45f)
   drawSword(canvas,50f, 10f)
   drawSword(canvas,35f, 55f)
}

跑起来看看:

Emm... 这动画也太整齐划一了。

错开三道剑气

在 Z 轴旋转上,我们给每道剑气一个初始值的旋转值(360/3 = 120),这样它们就能均匀的错开了。

相关实现如下:

private fun drawSword(canvas: Canvas, rotateX: Float, rotateY: Float, startValue: Float) {
   //... 省略未改动代码
   camera.rotateZ(anim.animatedValue as Float + startValue)
   //... 省略未改动代码
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.drawColor(Color.BLACK)
    drawSword(canvas,35f, -45f, 0f)
    drawSword(canvas,50f, 10f, 120f)
    drawSword(canvas,35f, 55f, 240f)
}

最终效果:

和我们开头预期的效果图一模一样。

完整代码

class SwordLoadingView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        color = Color.WHITE
    }

    val anim = ValueAnimator.ofFloat(0f, -360f).apply {
        // Z 轴是逆时针,取负数,得到顺时针的旋转
        interpolator = null
        repeatCount = RotateAnimation.INFINITE
        duration = 1000

        addUpdateListener {
            invalidate()
        }
    }

    var radius = 0f
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        radius = min(w, h) / 3f
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawColor(Color.BLACK)
        drawSword(canvas,35f, -45f, 0f)
        drawSword(canvas,50f, 10f, 120f)
        drawSword(canvas,35f, 55f, 240f)
    }

    private val camera = Camera()
    private val rotateMatrix = Matrix()

    val xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)

    private fun drawSword(canvas: Canvas, rotateX: Float, rotateY: Float, startValue: Float) {
        val layerId =
            canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG)
        rotateMatrix.reset()
        camera.save()
        camera.rotateX(rotateX)
        camera.rotateY(rotateY)
        camera.rotateZ(anim.animatedValue as Float + startValue)
        camera.getMatrix(rotateMatrix)
        camera.restore()

        val halfW = width / 2f
        val halfH = height / 2f

        rotateMatrix.preTranslate(-halfW, -halfH)
        rotateMatrix.postTranslate(halfW, halfH)
        canvas.concat(rotateMatrix)
        canvas.drawCircle(halfW, halfH, radius, paint)
        paint.xfermode = xfermode
        canvas.drawCircle(halfW, halfH - 0.05f * radius, radius * 1.01f, paint)
        canvas.restoreToCount(layerId)
        paint.xfermode = null
    }

}

基本使用

public class CustomViewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        testRoundCorners();

        setContentView(swordLoadingView());


//        String androidID = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
//        String id = androidID + Build.SERIAL;
//        Log.e(" sam ", "onCreate: " + androidID + "," + id);

    }

    private View swordLoadingView() {
        SwordLoadingView view = new SwordLoadingView(this);
        FrameLayout.LayoutParams fl = new FrameLayout.LayoutParams(Utils.getDp(200), Utils.getDp(200));
        fl.gravity = Gravity.CENTER;
        view.setLayoutParams(fl);
        view.getAnim().start();
        return view;
    }


    /**
     * 测试两种圆角绘制的差异
     */
    private void testRoundCorners() {
        setContentView(R.layout.activity_round);
        RectRoundView view = (RectRoundView) findViewById(R.id.rectRoundView);
        view.setFillStyle();

//        RectRoundView view2 = (RectRoundView) findViewById(R.id.rectRoundView2);
//        view2.getPaint().setColor(Color.RED);
//        view2.setStrokeWidth(2);
    }


    private View test() {
        RoundRectMask view = new RoundRectMask(this);
        view.setCornerRadiusDp(100);
        view.setCorners(true,true,true,true);
        setLayout(view);
        return view;
    }
    private View testShadow() {
        ShadowView shadowView = new ShadowView(this);
        setLayout(shadowView);
        return shadowView;
    }

    private View testCircleStore() {
        View view = new StrokeCircleView(this);
        view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        return view;
    }
    private View testScaleView() {
        ScaleView view = new ScaleView(this);
        setLayout(view);
        return view;
    }

    private View testRectRoundView() {
        RectRoundView view = new RectRoundView(this);
        setLayout(view);
        return view;
    } private View testPaintStyleTest() {
        View view = new PaintStyleTest(this);
        setLayout(view);
        return view;
    }

    private View testCurveView() {
        CubicBezierView view = new CubicBezierView(this);
        view.getPoints().add(new PointF(0f, 0.1f));
        view.getPoints().add(new PointF(0.25f, 0.25f));
        view.getPoints().add(new PointF(0.5f, 0.5f));
        view.getPoints().add(new PointF(1f, 1f));
        view.setBackgroundColor(Color.BLACK);
        view.setPointEvent(new CubicBezierView.Event() {
            @Override
            public void onEvent(float scaleX, float scaleY, int pointIndex) {
                Log.i("Sam", scaleX + " , " + scaleY + ", " + pointIndex);
            }
        });
        setLayout(view);
        return view;
    }

    private void setLayout(View view) {
        view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Utils.getDp(200)));
    }

}

源码地址:

https://github.com/samwangds/DemoFactory/blob/master/app/src/main/java/demo/com/sam/demofactory/view/SwordLoadingView.kt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金戈鐡馬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值