您现在的位置是:首页 > 正文

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

2024-02-29 15:35:29阅读 1

效果图

原理分析

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

月牙

先画一个圆:

再画个大一丢丢的:

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

实现

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

当当当,就是它~ 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

网站文章

  • 分布式事务(3)TCC方案

    分布式事务(3)TCC方案

    分布式事务(3)TCC

    2024-02-29 15:35:00
  • [Portal参考手册]Portlet核心API

    Portlet 类 Portlet 类是一个Portlet 的代码表示,它从PortletAdapter 继承而来。Portlet instance (portlet实例) Portlet类实例是一个Portlet 类的实例,由PortletConfig 中提供的一系列参数参数化的结果,每一个Portlet类实例中都包括一个PortletConfi...

    2024-02-29 15:34:54
  • 中兴服务器bios启动顺序设置,主板四大品牌BIOS设置开机第一启动项图文教程

    中兴服务器bios启动顺序设置,主板四大品牌BIOS设置开机第一启动项图文教程

    这篇文章主要介绍了主板四大品牌BIOS设置开机第一启动项图文教程,本文讲解了AMI BIOS、Award BIOS、Phoenix Bios、Insyde Bios等品牌BIOS设置开机第一启动项的方...

    2024-02-29 15:34:41
  • 查找指定字符_Excel常用查找函数

    查找指定字符_Excel常用查找函数

    对于数据分析师来讲Excel的重要性不言而喻。Excel因为其简化的界面和强大的功能至今都是各个企业进行数据分析高频的使用工具。为了实现分析者的目的,Excel中拥有强大的函数功能,可以通过函数的组合...

    2024-02-29 15:34:13
  • 泛型、约束(where)

    泛型、约束(where)

    泛型 C#中的泛型也就是C++中的模板,泛型允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法,程序的入...

    2024-02-29 15:34:06
  • 【偷偷卷死小伙伴Pytorch20天-day01-结构化数据建模流程范例】

    【偷偷卷死小伙伴Pytorch20天-day01-结构化数据建模流程范例】

    目录1.准备数据2.定义模型3.训练模型4.模型评估5.使用模型6.模型保存小结import osimport datetime#打印时间def printbar(): nowtime = da...

    2024-02-29 15:33:24
  • MySQL主从复制

    MySQL主从复制

    要同步之前的,先把之前的数据导出到SQL脚本中,SQL语句在从的执行了,达到此时和主的一样,在进行复制。io线程 SQL线程是否正常。从的里面写主库的 信息。

    2024-02-29 15:33:15
  • 开源堡垒机Jumpserver

    开源堡垒机Jumpserver

    文章目录1 Jumpserver介绍2 Jumpserver部署2.1 关闭防火墙与SELINUX2.2 部署环境2.3 下载组件2.4 处理配置文件2.5 启动Jumpserver2.6 配置开机自...

    2024-02-29 15:33:09
  • c语言-消息队列

    c语言-消息队列

    一、消息队列的介绍消息队列的实现原理是将消息存储在一个队列中,生产者将消息放入队列的尾部,消费者从队列的头部取出消息进行处理。消息队列通常采用先进先出(FIFO)的方式进行消息的存储和处理。消息队列可以实现异步通信,提高系统的可靠性和可扩展性。具体实现上,消息队列通常由以下几个组件构成:消息队列:用于存储消息的队列。生产者:向消息队列中添加消息的组件。消费者:从消息队列中取出消息并进行处理...

    2024-02-29 15:32:40
  • 记一次Nginx使用过程中遇到的问题和解决方案

    https代理到http 场景 解决方案 转发特殊的请求头 场景 解决方案 特殊的文件上传请求 场景 解决方案 最近项目中使用到了Nginx作为反向代理的一些简单功能,但是还是遇到了很多的问题,特在此记录下来,方便后期复看。 https代理到http 场景 服务器只对外暴露https端口,客户端通过https访问到nginx服务器,nginx服务器将对应路径...

    2024-02-29 15:32:32