手把手教你写酷炫Android自定义view

Posted 摘星猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你写酷炫Android自定义view相关的知识,希望对你有一定的参考价值。

 

1、先看效果 如果你对 以下内容不感兴趣 直接下载demo

简单的用法:

allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
dependencies {
	        implementation 'com.github.zhoulinxue:animatedTabView:1.0.2'
	}
package org.zhx.commom.widgets.animatedtabview.demo

import android.app.ActionBar
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import org.zhx.commom.widgets.AnimatedTabView
import kotlin.random.Random

class MainActivity : AppCompatActivity(), AnimatedTabView.OnItemChangeLisenter {
    private val images = arrayOf(
        R.drawable.ic_home_white_36dp,
        R.drawable.ic_visibility_white_36dp,
        R.drawable.ic_shopping_cart_white_36dp,
        R.drawable.ic_perm_identity_white_36dp
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var builder = AnimatedTabView.Builder(this)
        builder.height = 120
        builder.arrays = resources.getStringArray(R.array.tab_item_array)
        builder.images = images
        builder.selectedTextColor = Color.GREEN  // default Color.WHITE
        builder.unSelectedTextColor = Color.WHITE // default Color.WHITE
        builder.backgroundColor =
            resources.getColor(R.color.black_30) // default #30000000   不设置 就不绘制 背景
        builder.setOnItemClick(this)
        var view: AnimatedTabView = builder.build()
        test_table_container.addView(view)
        tab_btn.setOnClickListener {
            var position = Random.nextInt(images.size + 1) - 1
            view.setSelection(position)
            bottom_tabView.setSelection(position)
        }

        var builder2 = AnimatedTabView.Builder(this)
        builder2.height = 120
        builder2.arrays = resources.getStringArray(R.array.tab_item_array)
        builder2.images = images
        builder2.selectedTextColor = Color.GREEN  // default Color.WHITE
        builder2.unSelectedTextColor = Color.WHITE // default Color.WHITE
//        builder2.backgroundColor =resources.getColor(R.color.black_30) // default #30000000   不设置 就不绘制 背景
        builder2.setOnItemClick(this)

        bottom_tabView.setBuilder(builder2)
    }

    override fun onItemSelected(position: Int) {
        Log.e("AnimatedTabView", "itemclick    $position")
    }
}

2、自定义一个动画效果  这里我们使用 Android 系统类:ValueAnimator

   var  valueAnimator = ValueAnimator()
        valueAnimator.setFloatValues(0f, totalValus.toFloat())
        valueAnimator.duration = ANIMATION_DURATION
        valueAnimator.start()
        valueAnimator.addUpdateListener(this)
        valueAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                //TODO  重置状态参数
              
            }

            override fun onAnimationStart(animation: Animator) {
               //TODO  保存状态参数
              
            }
        })
       

 

3、监听动画切换状态 核心代码片段

  override fun onAnimationUpdate(animation: ValueAnimator) {
        mProcessValus = animation.animatedValue as Float
        if (mProcessValus != 0f) {
            mProcess = mProcessValus / (targetX - currentX)
            currentAlpha = (255 * mProcess).toInt();
            invalidate()
        }
    }

4、全部代码

package org.zhx.commom.widgets

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.util.SparseArray
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import java.util.*


class AnimatedTabView : View, ValueAnimator.AnimatorUpdateListener {

    constructor(context: Context) : super(context)

    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)

    constructor(context: Context, attributeSet: AttributeSet, arg: Int) : super(
        context,
        attributeSet,
        arg
    )

    /**
     * bitmap paint
     */
    private val mBitmapPaint: Paint by lazy {
        Paint().also {
            it.color = Color.WHITE
            it.style = Paint.Style.FILL
        }
    }

    /**
     * cicle paint
     */
    private val mCiclePaint: Paint by lazy {
        Paint().also {
            it.color = resources.getColor(R.color.white)
            it.style = Paint.Style.STROKE
        }

    }
    private val mTextPaint: Paint by lazy {
        var paint = Paint()
        paint.color = Color.WHITE
        paint.style = Paint.Style.FILL
        paint.textSize = 30f
        paint
    }
    private val mBackgroundPaint: Paint by lazy {
        var paint = Paint()
        paint.color = resources.getColor(R.color.black_30)
        paint.style = Paint.Style.FILL
        paint
    }
    private val TAG = AnimatedTabView::class.java.simpleName

    //item  count
    private var itemCount = 0

    // radius
    private var mRadius = 0

    // item height
    private var mHeight = mRadius * 2

    // item height
    private var mItemHeight = mRadius * 2

    //view width
    private var mWidth = mItemHeight * itemCount

    //view width
    private var mItemWidth = mItemHeight * itemCount

    //background Rect
    private var mCicleRectF: RectF? = null

    // animation process
    private var mProcess = 1f

    // translation valus
    private var mProcessValus = 0f
    private var currentX = mRadius
    private var targetX = 0
    private var currentPosition = 1
    private var mLastPosition = currentPosition
    private val MAX_ALPHA = 255
    private var currentAlpha = MAX_ALPHA
    private val valueAnimator: ValueAnimator by lazy {
        var valueAnimator = ValueAnimator()
        valueAnimator?.addUpdateListener(this)
        valueAnimator?.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                mProcess = 1f
                mProcessValus = 0f
                currentX = targetX
            }

            override fun onAnimationStart(animation: Animator) {
                mLastPosition = currentPosition
                currentPosition = getCurrentPositionByX(targetX)
                Log.e(TAG, "$mLastPosition   current : $currentPosition")
                if (mLastPosition != currentPosition) {
                    mBuilder?.onItemClick?.onItemSelected(currentPosition - 1)
                }
            }
        })
        valueAnimator
    }
    private val textRect = Rect()
    private val ANIMATION_DURATION: Long = 400
    private var mBuilder: Builder? = null
    private val sparseArray = SparseArray<Bitmap>()

    private fun getTargetX(x: Float): Int {
        return (x / mItemWidth).toInt() * mItemWidth + mItemWidth / 2
    }

    private fun isMoving(): Boolean {
        return valueAnimator != null && valueAnimator?.isRunning!!
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mWidth = WidgetUtil.measureWidth(widthMeasureSpec, mWidth)
        mHeight = WidgetUtil.measureHeight(heightMeasureSpec, mHeight)
        setMeasuredDimension(mWidth, mHeight)
        mCicleRectF = RectF(0f, 0f, mWidth.toFloat(), mHeight.toFloat())
        currentX = getXByPosition(currentPosition, 0).toInt()
        mItemWidth = mWidth / itemCount
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (itemCount == 0) {
            return
        }
        drawbackGround(canvas)
        drawSelctedTag(canvas)
        for (i in 1 until itemCount + 1) {
            val bitmap = sparseArray[i]
            val text = mBuilder?.arrays!![i - 1]
            mTextPaint?.getTextBounds(text, 0, text.length, textRect)
            if (currentPosition != i && mLastPosition != i) {
                mBitmapPaint.alpha = MAX_ALPHA
                canvas.drawBitmap(
                    bitmap, getXByPosition(i, bitmap.width / 2),
                    (mHeight / 2 - bitmap.height / 2).toFloat(),
                    mBitmapPaint
                )
            } else if (currentPosition != i && mLastPosition == i) {
                if (mBuilder?.unSelectedTextColor != 0) {
                    mTextPaint.color = mBuilder?.unSelectedTextColor!!
                } else {
                    mTextPaint.color = Color.WHITE
                }
                mTextPaint.alpha = MAX_ALPHA - currentAlpha
                canvas.drawText(
                    text,
                    getXByPosition(i, textRect.width() / 2),
                    (mHeight / 2 + textRect.height() / 2) * (1 - mProcess), mTextPaint
                )
                mBitmapPaint.alpha = currentAlpha
                canvas.drawBitmap(
                    bitmap,
                    getXByPosition(i, bitmap.width / 2),
                    mHeight / 2 - bitmap.height / 2 + bitmap.height * (1 - mProcess),
                    mBitmapPaint
                )
            } else if (currentPosition == i) {
                if (mBuilder?.selectedTextColor != 0) {
                    mTextPaint.color = mBuilder?.selectedTextColor!!
                }
                mTextPaint.alpha = currentAlpha
                canvas.drawText(
                    text, getXByPosition(i, textRect.width() / 2),
                    (mHeight / 2 + textRect.height() / 2) * mProcess,
                    mTextPaint
                )
                if (mProcess != 1f) {
                    mBitmapPaint.alpha = MAX_ALPHA - currentAlpha
                    canvas.drawBitmap(
                        bitmap, getXByPosition(i, bitmap.width / 2),
                        mHeight / 2 - bitmap.height / 2 + mProcess * mHeight,
                        mBitmapPaint
                    )
                }
            }
        }
    }

    private fun drawSelctedTag(canvas: Canvas) {
        canvas.drawCircle(
            currentX + mProcessValus,
            mHeight / 2.toFloat(),
            (mRadius - 1).toFloat(),
            mCiclePaint
        ) //item cicle
    }

    private fun drawbackGround(canvas: Canvas) {
        if (mBuilder?.backgroundColor != 0) {
            mBuilder?.backgroundColor?.let {
                mBackgroundPaint.color = it
                canvas.drawRoundRect(
                    mCicleRectF!!, mRadius.toFloat(), mRadius.toFloat(),
                    mBackgroundPaint
                ) // background
            }
        }
    }

    /**
     *get current position by x
     */
    private fun getXByPosition(i: Int, offSet: Int): Float {
        return ((mWidth / itemCount) * i - (mWidth / itemCount) / 2 - offSet).toFloat()
    }

    /**
     * get current position by x
     */
    private fun getCurrentPositionByX(targetX: Int): Int {
        return targetX / (mWidth / itemCount) + 1
    }

    /**
     * anmate view
     */
    private fun moveAnimation(toTargetX: Int) {
        targetX = toTargetX
        var totalValus = toTargetX - currentX
        valueAnimator.setFloatValues(0f, totalValus.toFloat())
        valueAnimator?.duration = ANIMATION_DURATION
        valueAnimator?.start()
    }


    override fun onAnimationUpdate(animation: ValueAnimator) {
        mProcessValus = animation.animatedValue as Float
        if (mProcessValus != 0f) {
            mProcess = mProcessValus / (targetX - currentX)
            currentAlpha = (MAX_ALPHA * mProcess).toInt()
            invalidate()
        }
    }

    private var mGestureDetector: GestureDetector =
        GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
            override fun onSingleTapUp(e: MotionEvent?): Boolean {
                val clickX: Int = getTargetX(e!!.x)
                Log.e(TAG, "$currentX  click x $clickX")
                if (!isMoving() && currentX != clickX) {
                    moveAnimation(clickX)
                }
                return true
            }
        });

    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.e(TAG, "  event " + event.x)
        mGestureDetector.onTouchEvent(event)
        return true
    }

    fun setSelection(position: Int) {
        var realPosition = position + 1
        Log.e(TAG, realPosition.toString())
        if (realPosition in 1 until itemCount + 1 && realPosition != currentPosition && !isMoving()) {
            moveAnimation(getXByPosition(realPosition, 0).toInt())
        }
    }


    class Builder(private val context: Context) {
        var height = 120
        var width = 0
        var onItemClick: OnItemChangeLisenter? = null
        var backgroundColor: Int = 0
        var selectedTextColor: Int = 0
        var unSelectedTextColor: Int = 0
        lateinit var arrays: Array<String>
        lateinit var images: Array<Int>

        fun setOnItemClick(itemClick: OnItemChangeLisenter): Builder {
            this.onItemClick = itemClick
            return this
        }

        fun setBackgroundColor(color: Int): Builder {
            backgroundColor = color
            return this
        }

        fun setSelectedTextColor(color: Int): Builder {
            selectedTextColor = color
            return this
        }

        fun setUnSelectedTextColor(color: Int): Builder {
            unSelectedTextColor = color
            return this
        }

        fun setWidth(width: Int): Builder {
            this.width = width
            return this
        }

        fun setHeight(height: Int): Builder {
            this.height = height
            return this
        }

        fun setClick(click: OnItemChangeLisenter?): Builder {
            this.onItemClick = click
            return this
        }

        fun setArrays(arrays: Array<String>): Builder {
            this.arrays = arrays
            return this
        }

        fun setImages(images: Array<Int>): Builder {
            this.images = images
            return this
        }

        fun build(): AnimatedTabView {
            val tabView = AnimatedTabView(context)
            tabView.setBuilder(this@Builder)
            return tabView
        }
    }

    public fun setBuilder(builder: Builder?) {
        mBuilder = builder
        if (builder != null) notifyParamchanage()
    }

    private fun notifyParamchanage() {
        if (mBuilder!!.arrays == null || mBuilder!!.images == null) {
            return
        }
        itemCount = mBuilder!!.arrays.size.coerceAtMost(mBuilder!!.images.size)
        mItemHeight = mBuilder!!.height
        mHeight = mItemHeight + paddingTop + paddingBottom
        mWidth = if (mBuilder!!.width == 0) {
            mItemHeight * itemCount
        } else {
            mBuilder!!.width
        }
        mRadius = mItemHeight / 2
        for (i in 0 until itemCount) {
            val bitmap = sparseArray[i + 1]
            if (bitmap == null) {
                sparseArray.put(
                    i + 1,
                    BitmapFactory.decodeResource(resources, mBuilder!!.images[i])
                )
            }
        }
        postInvalidate()
    }

    interface OnItemChangeLisenter {
        fun onItemSelected(position: Int)
    }


}

5、总结 

a、你要 写一个动画 、并且 监听动画 执行过程  分别在动画 开始 及结束监听里  设置 view的各种状态 (所有动效类的 自定义View 都有这个共性)
b、按照动效 需求 去设置  绘制动效的 物理 特性

c、如图  动效需求 是  当tab 切换时  文字 和 图片都 需要 移动和 淡化 效果   我们 就利用一个 动画 来 实现 位移及 透明度 设置 。

d、只要你耐心点  你也可以写出 很优秀的 自定义组件。

gitHub 下载

以上是关于手把手教你写酷炫Android自定义view的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你打造一个心电图效果View Android自定义View

恭喜发财! -- 手把手教你仿造一个qq下拉抢红包 Android自定义view

Android开发之手把手教你写ButterKnife框架

Android 教你亲手打造酷炫的弹幕效果

Android开发之手把手教你写ButterKnife框架

手把手教你使用Jetpack Compose完成你的自定义Layout