自定义LoadingView——kotlin实现

前言

自己动手实现了一个自定义加载视图,感觉效果还可以,这里分享一波,供大家学习和交流。如果有写得不好的地方,还请各位大佬指正。

效果图

废话不多说,先上效果图:

自定义LoadingView——kotlin实现_第1张图片

分析

  • 这个自定义LoadingView由三个大小不断变化的小球构成,三个小球大小的不同步变化由ValueAnimation的startDelay方法实现。
  • 其中变化的最大半径、最小半径、小球间距、颜色等属性都可以进行自定义;
  • 在onMeasure方法中需要对高度为wrap_content时进行处理;
  • 在onDetachedFromWindow方法中需要将动画停止,防止在退出activity时发生内存泄漏

代码实现

写代码的过程中碰到了许多坑,下面会在注释中说道。

LoadingView.kt



class LoadingView @JvmOverloads constructor(
    context: Context,
    private val attrs: AttributeSet? = null,     //这儿有个坑,使用@JvmOverloads时候这两个参数必须指定默认值
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var maxRadius = 30        //最大半径

    private var minRadius = 5        //最小半径

    private var leftRadius = 5        //左边小球当前半径

    private var midRadius = 5        //中间小球当前半径

    private var rightRadius = 5        //右边小球当前半径

    private var duration = 1000        //小球由最小半径变为最大半径的时间

    private var color = Color.BLUE     //小球颜色

    private var internal = 100          //两球间距

    //三个属性动画分别控制左中右三个小球半径
    private val leftRadiusAnimator = ValueAnimator()

    private val midRadiusAnimator = ValueAnimator()

    private val rightRadiusAnimator = ValueAnimator()

    private lateinit var mPaint: Paint

    init {
        initParams()
    }
	
	//初始化各种属性
    private fun initParams() {
        val a = this.context.obtainStyledAttributes(attrs, R.styleable.LoadingView)
        //获取自定义属性
        a.apply {
            maxRadius = getInt(R.styleable.LoadingView_max_radius, 30)
            if (maxRadius > 100) maxRadius = 100       //最大半径不超过100
            minRadius = getInt(R.styleable.LoadingView_min_radius, 5)
            if (minRadius < 1) minRadius = 1         //最小半径不小于1
            duration = getInt(R.styleable.LoadingView_duration, 1000)
            color = getColor(R.styleable.LoadingView_ballColor, Color.BLUE)
            internal = getInt(R.styleable.LoadingView_internal, internal)
        }
        a.recycle()		//需要将TypedArray进行回收

        leftRadiusAnimator.run {
            setIntValues(minRadius, maxRadius)
            repeatCount = ValueAnimator.INFINITE	//动画无限循环
            repeatMode = ValueAnimator.REVERSE		//小球变到最大后再逐渐变为最小
            duration = 1000
            addUpdateListener {
                leftRadius = it.animatedValue as Int
            }
        }

        midRadiusAnimator.run {
            setIntValues(minRadius, maxRadius)
            repeatCount = ValueAnimator.INFINITE
            repeatMode = ValueAnimator.REVERSE
            duration = 1000
            startDelay = duration * 2 / 3	//延迟动画开始时间,这样就实现了三个小球不同步的变化
            addUpdateListener {
                midRadius = it.animatedValue as Int
            }
        }

        rightRadiusAnimator.run {
            setIntValues(minRadius, maxRadius)
            repeatCount = ValueAnimator.INFINITE
            repeatMode = ValueAnimator.REVERSE
            duration = 1000
            startDelay = duration * 4 / 3
            addUpdateListener {
                rightRadius = it.animatedValue as Int
            }
        }

        leftRadiusAnimator.start()
        midRadiusAnimator.start()
        rightRadiusAnimator.start()

        mPaint = Paint()
        mPaint.style = Paint.Style.FILL
        mPaint.color = color
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)

        //处理高度为wrap_content的情况,宽度如果为wrap_content时默认为屏幕宽度
        if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, maxRadius * 2)
        } else {
            setMeasuredDimension(widthSpecSize, heightSpecSize)
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val pivotX = width / 2      //中心小球横坐标
        val pivotY = height / 2     //中心小球纵坐标

        canvas?.apply {
        	//绘制三个小球
            drawCircle(pivotX.toFloat(), pivotY.toFloat(), leftRadius.toFloat(), mPaint)
            drawCircle((pivotX + internal).toFloat(), pivotY.toFloat(), midRadius.toFloat(), mPaint)
            drawCircle((pivotX - internal).toFloat(), pivotY.toFloat(), rightRadius.toFloat(), mPaint)
        }

        invalidate()
    }

	//退出activity时停止动画
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        leftRadiusAnimator.end()
        midRadiusAnimator.end()
        rightRadiusAnimator.end()
    }
}

LoadingViewStyle.xml
文件中声明了一些自定义属性

<resources>
    <declare-styleable name="LoadingView">
        <attr name="min_radius" format="integer" />
        <attr name="max_radius" format="integer" />
        <attr name="ballColor" format="color" />
        <attr name="duration" format="integer" />
        <attr name="internal" format="integer" />
    </declare-styleable>
</resources>

activiy_main.xml

主活动中使用

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.customviewdemo.LoadingView
        android:id="@+id/loading_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:ballColor="@color/colorPrimary"
        app:max_radius="30"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

你可能感兴趣的:(移动应用开发之路)