作者:彭也

链接:

https://www.jianshu.com/p/4f0844c72e8a

模拟液体流动的展开特效,适合一些需要侧边展开进行辅助说明的页面,如用户在填写某个表单,需要操作很多步骤,有这么一个侧边栏控件,用户可以随时展开查看操作指引。

适合app首次启动的宣传引导图

效果还不错,体验比较新奇。

市面上在应用中模拟液体流动的效果大部分都是一个正弦函数式的波浪循环滚动,没有交互灵魂,宛如一个没有感情的复读机。

为了使交互更新鲜,设计了这款具备展开、收缩状态的液体流动控件,收缩状态下,控件收缩在屏幕右侧;展开过程中,跟随用户手指的滑动模拟液体流动效果。

实现方案

2.0  类设计

顶点的移动本质上是坐标的移动,坐标的移动本质上是横坐标和纵坐标的移动,定义一个坐标类Coordinate

open class Coordinate {

    constructor() {    }

    var x: Float = 0F

    var y: Float = 0F

    var xFunc: IFunc? = null

    var yFunc: IFunc? = null

    override fun toString(): String {        return "Coordinate(x=$x, y=$y, xFunc=$xFunc, yFunc=$yFunc)"    }}

横纵坐标的移动本质上是随一个或几个输入变量进行变化的函数,运用远古人传下来的设计模式中的策略模式思想进行设计,定义IFunc接口,坐标值通过execute方法计算得出,类关系如下:

IFunc类:

interface IFunc {

    /**     * 初始值     */    var initValue: Float

    /**     * 入参的阈值     */    var inParamMax: Float

    /**     * 入参的阈值     */    var inParamMin: Float

    /**     * 出参的阈值     */    var outParamMax:Float

    /**     * 出参的阈值     */    var outParamMin:Float

    fun execute(inParam: Float): Float}

2.1 UI拆解

2.1.1 形状分析

从形状上看,应该是由收缩状态下一个带有突起的波纹形状和展开状态下的全屏矩形构成,状态切换的过程就是由波纹形状变成矩形形状的过程,有点类似SVG动画

2.1.2 方案参考

从形状上看大致可以猜到应该和贝斯尔曲线有关,也可能是某个数学函数的函数图。这里采用贝塞尔曲线,可以更好的运用坐标值计算框架。找好贝塞尔曲线的关键坐标点,针对每个点进行做坐标值变换计算

2.2 UI绘制

2.2.1 绘制path

定义关键点


代码如下

/*** 构成波浪的关键点坐标*/var pointA: Coordinate = Coordinate()var pointB: Coordinate = Coordinate()var pointC: Coordinate = Coordinate()var pointD: Coordinate = Coordinate()var pointE: Coordinate = Coordinate()var pointF: Coordinate = Coordinate()var pointG: Coordinate = Coordinate()//当前路径var path: Path = Path()

生成路径


代码如下

private fun configPath(): Path {    path.reset()    path.moveTo(width.toFloat(), 0F)    path.lineTo(pointA.x, 0F)    path.lineTo(pointA.x, pointA.y)

    path.quadTo(pointB.x, pointB.y, pointC.x, pointC.y)    path.quadTo(pointD.x, pointD.y, pointE.x, pointE.y)    path.quadTo(pointF.x, pointF.y, pointG.x, pointG.y)

    path.lineTo(pointG.x, pointG.y)    path.lineTo(pointG.x, height.toFloat())    path.lineTo(width.toFloat(), height.toFloat())

    path.close()

    return path}

2.2.2 绘制指示器


可以看到,在控件收缩状态下,有一个向左的箭头指示器,这里采用bitmap

private fun drawIndicator(canvas: Canvas?) {    if (isNeedDrawBackBm == false) {        return    }    canvas?.apply {        if (backBm == null) {            backBm = BitmapFactory.decodeResource(resources, R.drawable.img_back)            backBm?.setHasAlpha(true)        }        val backBmCenterX: Int = (width - oriWaveHeight / 2).toInt()        val backBmCenterY: Int = height / 2        this.drawBitmap(backBm!!, Rect(0, 0, backBm!!.width, backBm!!.height), Rect(backBmCenterX - (oriWaveHeight / 8).toInt(), backBmCenterY - (oriWaveHeight / 8).toInt(), backBmCenterX + (oriWaveHeight / 8).toInt(), backBmCenterY + (oriWaveHeight / 8).toInt()), null)    }}

2.2.3 ImageView方案

一开始我思考应该可以用继承ImageView的进行图片绘制,只需裁剪canvas即可,onDraw中一行代码搞定,还可以在xml布局中使用所有ImageView的属性配置

class FlowView : View {    fun onDraw(canvas:Canvas?){        canvas?.let{            it.clipPath(path)        }        super.onDraw(canvas)    }}

但此时会带来个问题,此时的path并未和paint进行共同操作,对画布裁剪时可能会出现毛刺感, 无论你是否设置过抗锯齿。


至此,大部分屏幕分辨率较高的实机上都可以较好的运行了,看不出毛刺感。但低分辨率的机器上毛刺感也是需要解决的。

2.2.4 解决毛刺感

采用非clipPath方案,使用图形叠加效果的设置解决形状边缘的毛刺感。通过Paint.setXfermode进行设置,参数通过PorterDuff.Mode枚举进行选取。


代码如下:

private fun clipSrcBm() {    paint.xfermode = null    if (tempBm == null) {        tempBm = Bitmap.createBitmap(srcBm?.width!!, srcBm?.height!!, Bitmap.Config.ARGB_8888)    }    if (tempCanvas == null) {        tempCanvas = Canvas(tempBm!!)    }    tempCanvas?.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)    tempCanvas?.drawPath(path, paint)    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)    tempCanvas?.drawBitmap(srcBm!!, Rect(0, 0, srcBm?.width!!, srcBm?.height!!), Rect(0, 0, width, height), paint)}

边缘的毛刺感瞬间就木有了,对比放大看下


2.2.6 解决卡顿

需要注意到的是,绘制bitmap是个需要考虑性能的操作,android上设计图片的操作都需要谨慎处理。对于一些低端机器,如果该控件用于app引导图场景,可能会卡顿掉帧,解决方案是采用继承自SurfaceView的方案

class FlowSurfaceView : SurfaceView, SurfaceHolder.Callback, Runnable { override fun run() {  while (isDrawing) {  canvas = holder.lockCanvas()  canvas?.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);  drawWave(canvas)  drawSrcBm(canvas)  drawIndicator(canvas)  canvas?.apply {  holder.unlockCanvasAndPost(this)  }  } }

2.3 交互实现

2.3.1 配置关键点坐标变化公式

代码如下,以展开过程的坐标变换公式为例

fun configExpandFunc() {    pointA.xFunc = Func5(pointA.x, pointA.x)    val pointAyFunc = Func7(pointA.y, pointA.y)    pointAyFunc.rate = 3 * width / height.toFloat()    pointA.yFunc = pointAyFunc

    pointB.xFunc = Func5(pointB.x, pointB.x)    val pointByFunc = Func7(pointB.y, pointB.y)    pointByFunc.rate = 2 * width / height.toFloat()    pointB.yFunc = pointByFunc

    pointC.xFunc = Func5(pointC.x, pointC.x)    val pointCyFunc = Func7(pointC.y, pointC.y)    pointCyFunc.rate = width / height.toFloat()    pointC.yFunc = pointCyFunc

    pointE.xFunc = Func5(pointE.x, pointE.x)    val pointEyFunc = Func8(pointE.y, height.toFloat())    pointEyFunc.rate = width / height.toFloat()    pointEyFunc.inParamMin = pointE.y    pointE.yFunc = pointEyFunc

    pointF.xFunc = Func5(pointF.x, pointF.x)    val pointFyFunc = Func8(pointF.y, height.toFloat())    pointFyFunc.rate = 2 * width / height.toFloat()    pointFyFunc.inParamMin = pointF.y    pointF.yFunc = pointFyFunc

    pointG.xFunc = Func5(pointG.x, pointG.x)    val pointGyFunc = Func8(pointG.y, height.toFloat())    pointGyFunc.rate = 3 * width / height.toFloat()    pointGyFunc.inParamMin = pointG.y    pointG.yFunc = pointGyFunc}

2.3.2 跟随用户手指移动而变化

代码如下,其中offset为用户手指滑动的X轴方向的距离

private fun executePointFunc(point: Coordinate, offset: Float) {    point.xFunc?.let {        point.x = it.execute(offset)    }    point.yFunc?.let {        point.y = it.execute(offset)    }}

2.3.3 动画实现

代码如下,以收缩动画为例

fun startShrinkAnim() {    offsetAnimator?.cancel()    offsetAnimator = ValueAnimator.ofFloat(offsetX, width.toFloat())    offsetAnimator?.let {        it.duration = DURATION_ANIMATION        it.interpolator = AccelerateDecelerateInterpolator()        it.addUpdateListener {            val tempOffsetX: Float = it.animatedValue as Float            executePointFunc(pointA, tempOffsetX)            executePointFunc(pointB, tempOffsetX)            executePointFunc(pointC, tempOffsetX)            getPointDCoordinate(pointB, pointC)            executePointFunc(pointE, tempOffsetX)            executePointFunc(pointF, tempOffsetX)            executePointFunc(pointG, tempOffsetX)

            postInvalidate()        }

        it.addListener(object : AnimatorListenerAdapter() {            override fun onAnimationEnd(animation: Animator?) {                super.onAnimationEnd(animation)                isNeedDrawBackBm = true                //重新设置变换函数                configExpandFunc()

                resetInitValueFunc(pointA)                resetInitValueFunc(pointB)                resetInitValueFunc(pointC)                getPointDCoordinate(pointB, pointC)                resetInitValueFunc(pointE)                resetInitValueFunc(pointF)                resetInitValueFunc(pointG)            }        })        it.start()    }    isExpanded = false    listener?.onStateChanged(STATE_SHRINKED)}

2.3.4 事件传递处理

需要注意的是,当控件处于收缩状态,用户点击空白区域,应该将事件继续传递下去,封装一个判断用户点击坐标是否在path内部的方法

private fun isInWavePathRegion(x: Float, y: Float): Boolean {    val rectF = RectF()    path.computeBounds(rectF, true)    val region = Region()    region.setPath(path, Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt()))    if (region.contains(x.toInt(), y.toInt())) {        return true    }    return false}

如果不在path内部,交给父类处理

if (isInWavePathRegion(downX, downY)) {    isEffectOperation = truepostInvalidate()} else {    return super.onTouchEvent(event)}

后记

模拟液体流动效果有很多方案,可以像本文一样使用贝塞尔曲线,也可以使用指定的函数绘制曲线,无论哪种方案,本质上都是数学问题。

只可惜当年我的体育老师不给力,大部分数学知识都没塞进脑子里。使用本文中的坐标值计算框架的好处是不用研究复杂的数学函数,将数学函数图像的变化转换成每个坐标点的坐标变化。

这种由大化小的分化思想在现实中有很多应用。

源码学习地址:

https://gitee.com/null_077_5468/uidemos

关注我获取更多知识或者投稿

android 控件随手指移动_液体流动控件,隔壁产品都馋哭了相关推荐

  1. android 根据bounds坐标进行点击操作_炫酷的Android时钟UI控件,隔壁产品都馋哭了...

    废话不多说,先上效果效果酷炫,动画丰富,效果爆炸boom-设计思路看腻了市面上各种丑陋难看的时钟控件,是时候整点新活!将现实生活中的摆钟圆形表盘设计.电子手表的数显表盘设计抽象出来,提取出" ...

  2. 2021年上半年最接地气的Android面经,隔壁都馋哭了

    导语 学历永远是横在我们进人大厂的一道门槛,好像无论怎么努力,总能被那些985,211 按在地上摩擦! 不仅要被"他们"看不起,在HR挑选简历,学历这块就直接被刷下去了,连证明自己 ...

  3. 带你全面解析Android框架体系架构view篇,隔壁都馋哭了

    开篇 说一下我大概的情况.渣本毕业,工作已经有快3年了,从高中就开始玩小破站.无论是学习还是日常放松都是在b站.大学主学的软件技术专业,所以,入职bilibili是我大学时期给自己定的小目标. 在学校 ...

  4. tensorflow环境下的识别食物_研究室秒变后厨,TensorFlow被馋哭!日本团队用深度学习识别炸鸡,救急便当工厂...

    大数据文摘出品 作者:李欣月.刘俊寰 在韩国最受欢迎的外卖食品是什么? 答案毋庸置疑,一定是炸鸡! 根据韩国外卖订购软件公布的的统计数据显示,炸鸡今年再次当选韩国"最受欢迎的外卖食品&quo ...

  5. 苹果6s上市时间_苹果给6s出“福利”,网友:同期的安卓手机都馋哭了

    一般情况下,现在安卓手机的寿命大概在两年左右的时间,像如今的安卓手机,搭载骁龙8XX处理器,8GB运存,流畅个两年时间是不成问题的.不过考虑到现在安卓手机更新换代的迅速,一年时间里,一个厂商往往会有多 ...

  6. Android控件随手指的移动而移动

    Android控件随手指的移动而移动 原理:这个不是很难,首先我们要给控件设置触摸监听时间,监听按下,移动,抬起等操作,然后在移动,按下里面分别获取按下的坐标,通过移动获取的坐标减去之前按下的坐标得到 ...

  7. android如何创建spinner组件,Andriod开发之下拉列表控件(Spinner)的用法

    Spinner是Android的下拉列表控件,今天对这个控件进行了学习,发现该控件比其它简单控件使用起来稍微复杂,特地将Spinner控件的使用方法以及注意事项记录下来,以备后用. Spinner控件 ...

  8. 腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言

    3.1.4 Robotium的控件获取.操作及断言 Robotium是一款在Android客户端中的自动化测试框架,它需要模拟用户操作手机屏幕.要完成对手机的模拟操作,应该包含以下几个基本操作: (1 ...

  9. 【Android自定义View实战】之自定义评价打分控件RatingBar,可以自定义星星大小和间距...

    [Android自定义View实战]之自定义评价打分控件RatingBar,可以自定义星星大小和间距

最新文章

  1. linux驱动:音频驱动(四)ASoc之machine设备
  2. java与与短路与_Java中短路运算符与逻辑运算符示例详解
  3. (一)Audio子系统之AudioRecord.getMinBufferSize
  4. 在eclipse中安装groovy插件详细步骤
  5. 前端框架——Jquery——基础篇2__获取DOM节点的值
  6. MySQL用户管理、常用SQL语句、MySQL数据库备份恢复
  7. windows10怎么安装python第三方库_怎么在windows下安装python第三方包
  8. Parhaps you are running on a JRE rather than a JDK?
  9. 乐pad平板电脑_2020年双十一高性价比平板电脑推荐(包含苹果ipad,安卓华为,微软surface)...
  10. [转载] 夯实Java基础系列8:深入理解Java内部类及其实现原理
  11. br php 配置,无法载入 mcrypt 扩展,br /请检查 PHP 配置终极解决方案
  12. python获取数据库列名_如何用Python从SQL中提取出涉及到的表名、列名?
  13. 分布式系统阅读笔记(十九)-----移动计算和无处不在的计算
  14. 2006考研阅读Text2翻译
  15. Interop统计WORD字数
  16. 基于华为鲲鹏云的c语言程序设计,华为DevRun第四讲,华为云鲲鹏云服务移植快速入门与实践...
  17. ios7新特性--4
  18. PostgreSQL hint用法(兼容oracle)
  19. 基于CentOS7系统环境下的Snort3安装指南
  20. 计算机仿真技术生物,基于计算机仿真技术的人体生理特性和病理机制研究

热门文章

  1. latex教程详细笔记
  2. .NET 缩略图服务器 ResizingServer
  3. 13、Java菜单条、菜单、菜单项
  4. JS 判断js是加载完成!
  5. while循环,递进,linux按行读入并按数组存储
  6. python正态分布相关函数
  7. 优化算法optimization:Adam
  8. C++ 重定位输入输出
  9. 分布式文件系统HDFS 练习
  10. 【读薄Effective Java】创建和销毁对象