自定义view步骤:
- 获取xml里设置的颜色,宽度等资源
- 初始化画笔paint
- 重写onDraw.(还有其他绘制方法)
- canvas.drawXXX
- 进阶:
- 暴露方法, 通过postInvalidate()重绘.
ps: 重绘切记开子线程
低版本的适配用 new RectF(坐标X...), 再传入使用的draw方法即可
path类简述
详细请移步HenCoder1-1
path 的方法分为两种, 直接描述路径和辅助的设置/计算, path预先画完草稿后最后用canvas.drawPath
来绘制
- Path
- 直接描述路径
- 添加子图形, addXXX()
- addCircle(); 添加圆
- addArc; 强制无痕迹到新起点的arcTo, 画弧形
- addOval(); 添加椭圆
- addRect(); 添加矩形
- addRoundRect(); 添加圆角矩形
- addPath(); 添加另一个path
- 画线, XXXTo()
- lineTo(); 画线
- arcTo(); 画弧形
- close(); 封闭子图形(有痕迹的回到最初起点)
- 添加子图形, addXXX()
- 辅助的设置或计算,
- moveTo(); 位移起点
- 直接描述路径
path 在绘制 Rect 时无法设置起点, 强制默认起点左上角, 可设置绘制方向顺时针/逆时针
path 在绘制 RoundCirdle 时可以设置.
View 中方法执行顺序
- onFinishInflate
- onMeasure
- onSizeChanged
- onLayout
- onDraw
invalidate 和 requestLayout 区别
invalidate(): 只调用 onDraw();
requestLayout(): 会调用 onMeasure(), onLayout(), 不一定调用onDraw()
各个Animator区别
ViewPropertyAnimator: 限制于使用View自带的属性
ObjectAnimator: 可用于自定义控件的自定义属性
Animator 中只应做取插值的操作, 绘制/getPosTan 等方法应该放在 onDraw里
何时获取宽高
View 的生命周期和 Activity 的 onCreate, onStart, onResume 不同步. 如果此时还在测量, 得到的height, width 可能为0.
- onWindowFocusChanged(). 注意: Activity 窗口获取/失去焦点时会被频繁调用
- view.post(runnable)
- ViewTreeObserver, 注意: onGlobalLayout 也会被多次调用
- view.measure, 需根据 LayoutParams 划分:
- match_parent: 无法获取, 理论上无法知道此刻 parentSize;
- 具体数值:
- wrap_content
画一个仿 mastodon 的转发按钮
目标
- 效果地址:mastodon殆知阁的猫站主页 https://mao.daizhige.org/web/getting-started
- 效果gif:
分析
- 使用 Chrome 的 More Tools -> Animations记录动画, 并用10%的速度播放后发现. 按钮可以拆分为两部分, 不动的圆角矩形和以圆角矩形作为轨迹移动的两个三角形
- 三角形移动的动画是一个在Animations分析下, 于450ms就超过了一半, 目测是一个减速的动画
- 实现方案 android 自定义 View.
获取参数
android 中 View 绘制时是设置的 View 整体的宽高
截图一桢并在 PS 下放大到 2000% 后如图获取到参数(单位: 格)
- 圆角矩形包含边宽时, 宽30, 高24, 边宽6.
- 三角形边宽不另算, 底边18, 高 12. 伸出圆角矩形的部分为6.(PS: 此处边宽是否计入会在后面解释)
- 所以 View 整体的宽为圆角矩形的宽 + 左右两侧三角形伸出圆角矩形的部分 = 30 + 6 * 2 = 42, 高为圆角矩形的高 + 上下两侧(三角形的高 - 圆角矩形的边宽) = 24 + (12 - 6) * 2 = 36.
- 三角形与圆角矩形交接处宽度为2.
- 三角形与圆角矩形的1/2高度处的距离为1
绘制 View
android 中 View 绘制通过继承 View 后重写
onDraw
方法.onDraw
中,canvas
理解为画布/画纸,paint
理解为画笔
android 中画笔 paint 分为两种, STORKE 和 FILL, 直译就是一笔和画满, 具体的解释是如果指定的区域是闭合的, 用 FILL 会把这个区域填满. 而 STROKE只会画边框
- 首先把坐标系从默认的左上角移动到 View 的中心.
canvas.translate(mWidth/2, mHeight/2);//坐标系原点切到控件1/2处
圆角矩形的绘制只要边框即可, 不需要填满 使用的是 STROKE, 又因为
canvas.drawRoundRect(...)
方法会把画笔的起点扯到圆角矩形的左上角, 无法实现三角形在圆角矩形的左侧中间位置开始移动, 所以最终的绘画方案是通过依次绘制四个顶点来画出圆角矩形, 使用canvas.drawPath
. paint 的宽度为6三角形的绘制, 因为需要填满三角形内部, 所以用的是 FILL. 绘制方法同样是
canvas.drawPath
透明边
此时三角形也画出来了, 但是原效果的三角形的边好像是外边框透明了? 那好, 来从画笔 paint 里找设置外边框的笔, 找了一圈。。。。。。emmm 没有.
那换一个角度, 并没有什么三角形的外边框, 而是在三角形的上面, 有一条透明的线.
好的, 那画好了透明色的线来看, 还是原来的样子.
因为 android 的绘制原则是覆盖, 所以一个区域如果有图案了,只能去覆盖它, 但是透明色的下面是原来的颜色, 所以覆盖上去了并不能透明.
所以看似透明边. 实际是背景色边而已.
但是为了保证三角形的圆角不会因误差被擦除, 需要将三角形和背景色边两个path重叠起来, 并且重叠出保留三角形. 用到了 PorterDuffXfermode
的 DST_OVER
, 保留了作为的DST三角形
动画 Animation
android 中动画的概念可以拆分为, 用一个插值器拿到 此刻进度 在动画总进度的百分比 + 此刻 的 canvas 上画了啥. 插值器可以理解为动画执行到了哪一刻
举个例子, 一条用 path 画的直线本身在一次onDraw
内就可以直接完成, 现在把它的 path 路径用一个 1000 毫秒的动画来完成, 过程是: 默认的每10ms获取一次 此刻 插值器的值, 同时调用postInvalidate()
方法触发onDraw
来绘制,onDraw
里根据插值器的值获取到执行到了总进度的多少, 来决定怎么画.
- 根据分析, 我们直接使用 DecelerateInterpolator 这个减速插值器即可
- 让三角形沿着圆角矩形滚动可以看作让三角形沿着绘制圆角矩形时的 path 路径滚动.
- 通过
mPathMeasure.setPath(path, true)
绑定 path, -
onDraw
时不断调用mPathMeasure.getPosTan
方法来得到, 此刻插值器的值 * 总进度走到的点的坐标, 以及趋势方向与x轴的夹脚
- 通过
问题
此时发现, 三角形一直保持着绘制时的标准坐标系, 并没有按照预想的旋转.
旋转三角形
我们需要在左侧边时的三角形逆时针旋转90度, 在有侧边时的三角形顺时针旋转90度, 也就是-90 和 90.
根据动画 Animation 中提到的 mPathMeasure.getPosTan(distance, pos[], tan[])
方法, 参数依次是, 此刻的进度, 此刻的点坐标数组pos[] 以及此刻的切线值. 这里通过不详细解释, 直接通过 float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI)
, 拿到趋势方向与x轴的夹脚, 将画布旋转即可canvas.rotate(degrees)
, 同理画出右侧三角形(此处可以展开讲讲, 但考虑到篇幅, 可根据注释理解)
矩形改成圆角矩形
之前绘制的是矩形, 而圆角可以用path.arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo
方法绘制. 如图的圆弧是占地 left, top 到 right, bottom 的圆的 -180 度开始划过了 90 度的部分
其中,只画left, top, right, bottom 确定的椭圆为
这次的view为
于是效果从
- [图片上传失败...(image-90462c-1523629514385)]
转换成了
最终效果
优化
1.颜色
根据"数码去色计"的"显示原生值"设置获取按钮的各种颜色
2.透明三角替换透明边
当三角形的圆角数值固定且view很小时, 三角形会矮于view导致三角形顶部多处一个"小角"
- 解决方案 将透明边改成透明三角形, 确保透明部分始终盖住底部矩形
3.添加点击逻辑
3.1.缩放
view 自带了 animate(). 直接调用来缩放整体.
- ACTION_DOWN变小
- ACTION_UP变回
- ACTION_CANCEL变回
3.2. 当手指滑出View时
普通的父布局不会管子view的 touch 事件, 但是 RecyclerView 会在 onInterceptTouchEvent 里判断当 event 区域超过子 view 后调用 setScrollStat(), 拦截掉接下来的 move 事件. 这样子 view 被ACTION_CANCEL, 父布局 RecyclerView 根据 move 来滑动
如果手指超出了 view 后发生位移(Recyclerview中), 可根据 Recyclerview 触发的 ACTION_CANCEL 获取到离开的标志
如果 view 无法位移(在普通 view 中), 只可用 onTouchEvent 根据 ACTION_UP 拿到手指离开的标志
优化后的效果
参考
感谢黄海奇的指点
Hencoder1-1
arcToMethod
getBackgroundColor
Path测量工具:PathMeasure
安卓自定义View进阶-PathMeasure
CSDN | 自定义view系列(3)--给自定义View添加点击事件
TODO
Android自定义View长按事件的实现