android自定义view--SearchView

前言

上一篇Path特效功臣----PathMesure我们讲了PathMesure中api的详细方法和测试。本文就用我们学到的PathMeasure实现一个动态效果的SearchView,先瞄一下好不好看

searchView图片来源于网上

思路(由简到繁)

绘制静态的路径(放大镜和外圆)
1、onSizeChang()中得到组件大小
2、填充Path的放大镜和外圆
为静态图添加动态效果
1、采用ValueAnimtor提供实时变量
2、采用PathMeasure根据实时变量去绘制
3、动态效果分为四种状态(初始化,放大镜动画,外圆动画,结束动画)

绘制静态的路径

绘制的路径全部由Path去填充,放大镜可以拆分为一个圆和一条斜线,需要注意的是这里addArc的起始角度和终点角度


红色为Path的起点45度
class MySearchView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    constructor(context: Context?) : this(context, null)

    private val mPaint = Paint()//画笔
    private var offestFactoty = 0.9f //偏移因子画布是组件的0.9
    private var mWidth: Float = 0.0f  //组件宽度
    private var mHeight: Float = 0.0f //组件高度
    private lateinit var searchRecf: RectF
    private lateinit var cicleRecf: RectF
    private val searchPath: Path = Path() //放大镜的path
    private val ciclePath: Path = Path()  //外圆的path

    init {
        initPaint()
    }

  
    private fun initPaint() {
        mPaint.color = Color.BLUE
        mPaint.isAntiAlias = true
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.strokeWidth = 8f
    }
    //1、onSizeChang()中得到组件大小
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = w.toFloat()
        mHeight = h.toFloat()
        initPath()
    }
    //2、填充Path的放大镜和外圆
    private fun initPath() {
        cicleRecf = RectF(-mWidth / 2 * offestFactoty, -mHeight / 2 * offestFactoty, mWidth / 2 * offestFactoty, mHeight / 2 * offestFactoty)
        searchRecf = RectF(-mWidth / 5, -mHeight / 5, mWidth / 5, mHeight / 5)
        searchPath.addArc(searchRecf, 45f, 359.9f) //填充内圆
        ciclePath.addArc(cicleRecf, 45f, 359.9f)  //填充外圆
        val pathMeasure = PathMeasure(ciclePath, false)
        val floatArray = FloatArray(2)
        val posTan = pathMeasure.getPosTan(0f, floatArray, null)//拿到手柄的终点
        searchPath.lineTo(floatArray[0], floatArray[1])//为内圆添加手柄路径形成放大镜
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.translate(mWidth / 2, mHeight / 2)//移动坐标到组件中心
        canvas?.drawPath(searchPath, mPaint)
        canvas?.drawPath(ciclePath,mPaint)
    }
}
静态图

为静态图添加动态效果

package com.hzb.myutils.view

import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import com.hzb.utilsbox.utils.LogUtil


class MySearchView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    constructor(context: Context?) : this(context, null)

    private val mPaint = Paint()//画笔
    private var offestFactoty = 0.9f //偏移因子画布是组件的0.9
    private var mWidth: Float = 0.0f  //组件宽度
    private var mHeight: Float = 0.0f //组件高度
    private lateinit var searchRecf: RectF
    private lateinit var cicleRecf: RectF
    private val searchPath: Path = Path() //放大镜的path
    private val ciclePath: Path = Path()  //外圆的path
    private var viewStatus = AnimStatus.NONE
    private var isTopAnim:Boolean=false
    private var animatorValue: Float = 0.0f  //动画变量,0-1
    //属性动画
    private lateinit var startAnim: ValueAnimator
    private lateinit var searingAnim: ValueAnimator
    private lateinit var endAnim: ValueAnimator
  
    private enum class AnimStatus { //标志动画状态
        NONE, START, SEARING, END//初始状态,开始搜索,搜索中,结束搜索
    }


    init {
        initPaint()
        initAnimator()
        initEvent()
    }

    private fun initAnimator() {
        //AnimStatus.START状态的动画
        startAnim = ValueAnimator.ofFloat(0f, 1f)
        startAnim.duration = 1000
        startAnim.addUpdateListener { animation ->
            animatorValue = animation.animatedValue as Float
            invalidate()
        }
        //AniStatus.SEARING状态动画
        searingAnim = ValueAnimator.ofFloat(0f, 1f)
        searingAnim.interpolator = LinearInterpolator()
        searingAnim.duration=1500
        searingAnim.repeatCount=ValueAnimator.INFINITE
        searingAnim.repeatMode=ValueAnimator.RESTART
        searingAnim.addUpdateListener { animation ->
            if (viewStatus==AnimStatus.SEARING) { //这里必须添加,不然放大镜会有一刹那全部显示
                animatorValue = animation.animatedValue as Float
                invalidate()
            }
        }
        //AniStatus.END状态动画
        endAnim = ValueAnimator.ofFloat(1f, 0f)
        endAnim.duration=1000
        endAnim.addUpdateListener { animation ->
            animatorValue = animation.animatedValue as Float
            invalidate()
        }

    }
  
    private fun initPaint() {
        mPaint.color = Color.BLUE
        mPaint.isAntiAlias = true
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.strokeWidth = 8f
    }

    1、onSizeChang()中得到组件大小
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = w.toFloat()
        mHeight = h.toFloat()
        initPath()
    }

    //2、填充Path的放大镜和外圆
    private fun initPath() {
        cicleRecf = RectF(-mWidth / 2 * offestFactoty, -mHeight / 2 * offestFactoty, mWidth / 2 * offestFactoty, mHeight / 2 * offestFactoty)
        searchRecf = RectF(-mWidth / 5, -mHeight / 5, mWidth / 5, mHeight / 5)
        searchPath.addArc(searchRecf, 45f, 359.9f)
        ciclePath.addArc(cicleRecf, 45f, 359.9f)
        val pathMeasure = PathMeasure(ciclePath, false)
        val floatArray = FloatArray(2)
        val posTan = pathMeasure.getPosTan(0f, floatArray, null)
        searchPath.lineTo(floatArray[0], floatArray[1])//放大镜的手柄
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.translate(mWidth / 2, mHeight / 2)//移动坐标到组件中心
        when (viewStatus) {
            AnimStatus.NONE -> {  //初识转态
                canvas?.drawPath(searchPath, mPaint)
            }
            AnimStatus.START -> {  //开始搜索
                val pathMeasure = PathMeasure(searchPath, false)
                val dst = Path()
                pathMeasure.getSegment(pathMeasure.length * animatorValue, pathMeasure.length, dst, true)
                canvas?.drawPath(dst, mPaint)
            }
            AnimStatus.SEARING -> { //搜索中
                val pathMeasure = PathMeasure(ciclePath, false)
                val dst = Path()
                val stop = pathMeasure.length * animatorValue
                val start = (stop - (0.5 - Math.abs(animatorValue - 0.5)) * pathMeasure.length/2).toFloat()
                pathMeasure.getSegment(start,stop, dst, true)
                canvas?.drawPath(dst, mPaint)
            }
            AnimStatus.END -> {   //搜索结束
                val pathMeasure = PathMeasure(searchPath, false)
                val dst = Path()
                pathMeasure.getSegment(pathMeasure.length*animatorValue , pathMeasure.length,  dst, true)
                canvas?.drawPath(dst, mPaint)
            }
        }
    }

    /**
     * 开始动画
     */
    fun startAnim() {
        viewStatus=AnimStatus.START
        startAnim.start()
        this.isClickable = false
    }

    /**
     * 结束动画
     */
    fun stopAnim(){
        isTopAnim=true
    }
    /**
     * 监听动画结束
     */
    private fun initEvent() {

        val listener: Animator.AnimatorListener = object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
                if (isTopAnim) {
                    animation?.cancel()
                    viewStatus=AnimStatus.END
                    endAnim.start()
                }
            }

            override fun onAnimationEnd(animation: Animator?) {
                val name = Thread.currentThread().name
                LogUtil.i(name)
                when (viewStatus) {
                    AnimStatus.START -> {
                        searingAnim.start()
                        viewStatus=AnimStatus.SEARING
                    }

                    AnimStatus.END -> {
                        viewStatus = AnimStatus.NONE
                        this@MySearchView.isClickable = true
                        isTopAnim=false
                    }
                }
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }

        }

        startAnim.addListener(listener)
        searingAnim.addListener(listener)
        endAnim.addListener(listener)
    }
}

运行效果


效果gif

默认文件1631847282023.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,833评论 25 707
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,913评论 2 89
  • 不知道我心中到底怎么了 每到没事做就孤独和难过 也许我真的是单身太久了 只有羡慕嫉妒的在街边坐 我后悔没有听我父母...
    二师兄猪八戒阅读 473评论 0 0
  • 2018年6月21日。早上起来阿伟妈妈代谢正常,全是第一个好消息。早上两人一起去跑步感觉也比昨天身体轻松,看见跳舞...
    再见肥兔子阅读 549评论 0 0
  • 我的这篇文章是今日计划。我把它分为四个部分,学习,娱乐,吃饭和爱好。学习有英语,语文和记忆法。英语和语文在学而思。...
    关琋元阅读 157评论 2 1