前言
本文的目的有两个:
- 大多数时候,自定义View并不会被用到,但一旦用到,通常都是很炫酷的效果。App的开发本身并不酷,让它们变酷的是设计师们的想象力与创造力。对于开发工程师而言,要做的,就是把他们的想象力与创造力变成现实。
- Kotlin结合自定义View效果的实现,只要是 Java 能做的事情,Kotlin 都可以做,甚至还可以做得更好。
- 案例代码已上传Github,案例代码详情可戳—>代码案例内容传送门
接下来就是本文的主题核心内容:
一、仪表盘
图1-1 仪表盘效果的实现1、思路分析:
①拆分为外层圆弧
- 外层圆弧可通过canvas.drawArc()的形式进行实现,在本文中我通过Path首先添加了最外层的圆弧。
- 当前的控件,我使其填充屏幕,对于圆弧首先需指定其所在的矩形范围,再指定圆弧的占有角度。
以下为关键实现内容:
//先onDraw()绘制内容中,画外层的圆弧
mPath.addArc(
width / 2 - mRadius,
height / 2 - mRadius,
width / 2 + mRadius,
height / 2 + mRadius,
(90 + mArcAngle / 2).toFloat(),
(360 - mArcAngle).toFloat()
)
canvas.drawPath(mPath, mPaint)
- 在Kotlin中,对自定义View的处理中,不需要再像Java的getWidth()、getHeight()的方式指定获取屏幕宽、高,直接通过width、height 获取即可。
//扇形角度
val mArcAngle = 120
对于扇形角度,我在这里定义为120°,也是下图所示起始角度,代码中对于起始角度设置为90 + mArcAngle / 2,圆弧扫过的角度为360°减去起始点的起始角度,扫过角度即为360 - mArcAngle。
图1-1-1 外层圆弧绘制图解②拆分为中层矩形刻度尺
- 先定义PathDashPathEffect变量:
//路径改变器
lateinit var mPathDashPathEffect: PathDashPathEffect
//刻度线数量
val mDashCount: Int = 20
- 然后在初始化代码块中,先定义一个小矩形的宽和高;
在这里我设置路径的宽高分别为3dp、8dp,并做了相关的适配:
强调一点:CCW为counter-clockwise,逆时针方向绘制
init {
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = 3f
//对仪表盘添加每一个小刻度矩形
mPath.addRect(
0F,
0F,
DimensionUtils.dp2px(3f),
DimensionUtils.dp2px(8f),
Path.Direction.CCW
)
}
- 然后在onSizeChanged()中,对于PathDashPathEffect进行实例化,PathDashPathEffect的四个参数中,在上面的官网贴图中我已经展示,简单做总结:
参数 | 意义 |
---|---|
shape | 绘制路径 |
advance | 绘制间距 |
phase | 绘制偏移 |
style | 绘制样式 |
根据原生Api要求,需注意的是画笔样式需为STROKE、STROKE_AND_FILL两种样式,如果画笔设置为FILL的样式,PathDashPathEffect在路径上设置无效。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mPathDashPathEffect = PathDashPathEffect(
mPath,
PathMeasure(mPath, false).length - DimensionUtils.dp2px(3f) / mDashCount,
0F,
PathDashPathEffect.Style.ROTATE
)
}
- 最后在onDraw()中对刻度条进行绘制;
刻度尺也是需要借助于当前绘制的圆弧。核心点在于mPaint.setPathEffect(mPathDashPathEffect):
//设置刻度条
mPaint.setPathEffect(mPathDashPathEffect)
//然后再画刻度条
mPath.addArc(
width / 2 - mRadius,
height / 2 - mRadius,
width / 2 + mRadius,
height / 2 + mRadius,
(90 + mArcAngle / 2).toFloat(),
(360 - mArcAngle).toFloat()
)
canvas.drawPath(mPath, mPaint)
mPaint.setPathEffect(null)
- 在这里,需要对PathEffect做详细介绍解释(Android Api节选):
PathEffect is the base class for objects in the Paint that affectthe geometry of a drawing primitive before it is transformed by thecanvas' matrix and drawn.
译:PathEffect是Paint中的对象的基类,这些对象在被canvas的矩阵变换和绘制之前影响了原始的绘制对象。 - PathEffect有多个子类,在这里不做赘述,我所使用的是PathDashPathEffect,详情查看—>官网文档传送门
图1-2-1 PathDashPathEffect的官网说明
③拆分为内层指向线
绘制内层的指向线就很简单了,在这里我指向了第5个刻度线,根据角度进行换算获取
//画指示器
canvas.drawLine(
(width / 2).toFloat(),
(height / 2).toFloat(),
width / 2 + Math.cos(Math.toRadians(getAngle(5))).toFloat() * DimensionUtils.dp2px(
60f
),
height / 2 + Math.sin(Math.toRadians(getAngle(5))).toFloat() * DimensionUtils.dp2px(
60f
), mPaint
)
至此,刻度盘的效果实现,总体实现代码如下:
/**
* @author Alex
* @date 2019/9/3.
* GitHub:https://github.com/wangshuaialex
*/
class DashBoardView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
//半径
val mRadius = DimensionUtils.dp2px(100f)
//椭圆外矩形
val mRectF =
RectF(width / 2 - mRadius, height / 2 - mRadius, width / 2 + mRadius, height / 2 + mRadius)
//扇形角度
val mArcAngle = 120
//刻度条所依赖的线
var mPath = Path()
//刻度条
lateinit var mPathDashPathEffect: PathDashPathEffect
//刻度线数量
val mDashCount: Int = 20
init {
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = 3f
//对仪表盘添加每一个小刻度矩形
mPath.addRect(
0F,
0F,
DimensionUtils.dp2px(3f),
DimensionUtils.dp2px(8f),
Path.Direction.CCW
)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mPathDashPathEffect = PathDashPathEffect(
mPath,
PathMeasure(mPath, false).length - DimensionUtils.dp2px(3f) / mDashCount,
0F,
PathDashPathEffect.Style.ROTATE
)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
var resources = resources
if (canvas != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//先画原始的圆
mPath.addArc(
width / 2 - mRadius,
height / 2 - mRadius,
width / 2 + mRadius,
height / 2 + mRadius,
(90 + mArcAngle / 2).toFloat(),
(360 - mArcAngle).toFloat()
)
canvas.drawPath(mPath, mPaint)
//设置刻度条
mPaint.setPathEffect(mPathDashPathEffect)
//然后再画刻度条
mPath.addArc(
width / 2 - mRadius,
height / 2 - mRadius,
width / 2 + mRadius,
height / 2 + mRadius,
(90 + mArcAngle / 2).toFloat(),
(360 - mArcAngle).toFloat()
)
canvas.drawPath(mPath, mPaint)
mPaint.setPathEffect(null)
//画指示器
canvas.drawLine(
(width / 2).toFloat(),
(height / 2).toFloat(),
width / 2 + Math.cos(Math.toRadians(getAngle(5))).toFloat() * DimensionUtils.dp2px(
60f
),
height / 2 + Math.sin(Math.toRadians(getAngle(5))).toFloat() * DimensionUtils.dp2px(
60f
), mPaint
)
}
}
}
fun getAngle(pCurrentPosition: Int): Double {
return (90 + mArcAngle / 2 + (360 - mArcAngle) / mDashCount * pCurrentPosition).toDouble()
}
}
二、折页效果
图2-1 折页效果1、思路分析
①图片拆分为上部,只做图片切割
图2-1-1 图片上下两部分的拆分1.使用canvas.clipRec()系列方法对原始图片做切割,对上下两个部分分别做切割;
2.对于上半部分的图片,不做任何转换,以下为实现部分;
//在onDraw()绘制方法中进行处理
//上半部分
if (canvas != null) {
canvas.save()
mCamera.save()
canvas.clipRect(0f, 0f, mImageWidth, mImageWidth / 2)
//图片绘制
var avatarBitmap = BitmapConvertUtils.getAvatarBitmap(
resources,
DimensionUtils.dp2px(mImageWidth).toInt()
)
canvas.drawBitmap(avatarBitmap, 0f, 0f, mPaint)
mCamera.restore()
canvas.restore()
}
②图片拆分为下部,做图片切割与图像转换
1.对于下半部分的图片,做切割后,借助于Camera的Api对视角做变换,变换完毕后为了可以正常显示,对画布进行平移转换,以下为代码实现。
//下半部分
if (canvas != null) {
canvas.save()
mCamera.save()
mCamera.rotateX(mBottomAngle)
//画布右下平移,位置重新变换,坐标系位置改动
canvas.translate(mImageWidth / 2, mImageWidth / 2)
mCamera.applyToCanvas(canvas)
canvas.clipRect(-mImageWidth / 2, 0F, mImageWidth / 2, mImageWidth / 2)
canvas.translate(-mImageWidth / 2, -mImageWidth / 2)
//图片绘制
var avatarBitmap =
BitmapConvertUtils.getAvatarBitmap(
resources,
DimensionUtils.dp2px(mImageWidth).toInt()
)
canvas.drawBitmap(avatarBitmap, 0f, 0f, mPaint)
mCamera.restore()
canvas.restore()
}
- 在这里需要强调,对视图设置的角度,关键性参数:mBottomAngle,提供给用户设置;
- 在Kotlin中,我将mBottomAngle定义为成员变量,提供给调用者进行改变,在调用属性的set()方法时,进行invalidate()设置。
//定义折页顶部动画属性
var mBottomAngle: Float = 0f
set(value) {
field = value
invalidate()
}
get() = field
③做动画转场处理
最后,在Fragment的展示中,对于折页效果进行角度的改变处理,这里我使用属性动画进行展示,关键代码如下:
//底部折页动画
var bottomAngleAnimator = ObjectAnimator.ofFloat(ccv_convertView, "mBottomAngle", 120f)
var animatorSet = AnimatorSet()
animatorSet.startDelay = 1000
animatorSet.duration = 800
animatorSet
.playSequentially(bottomAngleAnimator)
animatorSet.start()
最后效果即实现,如下图,其中间的变化效果参考效果演示的Gif图:
图2-1-3 折页图最终效果
至此,折页效果已实现,附上实现代码的类文件内容:
class CameraConvertView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
var mPaint: Paint
var mCamera: Camera
//定义宽度动画属性
var mImageWidth: Float = 600F
//手动设置set方法
set(value) {
field = value
invalidate()
}
get() = field
//定义折页顶部动画属性
var mTopAngle: Float = 0f
set(value) {
field = value
invalidate()
}
get() = field
//定义折页顶部动画属性
var mBottomAngle: Float = 0f
set(value) {
field = value
invalidate()
}
get() = field
//定义画布的折叠角度动画属性
var mCanvasAngle: Float = 0f
set(value) {
field = value
invalidate()
}
get() = field
init {
mPaint = Paint()
mCamera = Camera()
//mCamera.setLocation(0f, 0f, -6 * resources.displayMetrics.scaledDensity)
//mCamera.rotateX(45f)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//上半部分
if (canvas != null) {
canvas.save()
mCamera.save()
canvas.clipRect(0f, 0f, mImageWidth, mImageWidth / 2)
//图片绘制
var avatarBitmap = BitmapConvertUtils.getAvatarBitmap(
resources,
DimensionUtils.dp2px(mImageWidth).toInt()
)
canvas.drawBitmap(avatarBitmap, 0f, 0f, mPaint)
mCamera.restore()
canvas.restore()
}
//下半部分
if (canvas != null) {
canvas.save()
mCamera.save()
mCamera.rotateX(mBottomAngle)
//画布右下平移,位置重新变换,坐标系位置改动
canvas.translate(mImageWidth / 2, mImageWidth / 2)
mCamera.applyToCanvas(canvas)
canvas.clipRect(-mImageWidth / 2, 0F, mImageWidth / 2, mImageWidth / 2)
canvas.translate(-mImageWidth / 2, -mImageWidth / 2)
//图片绘制
var avatarBitmap =
BitmapConvertUtils.getAvatarBitmap(
resources,
DimensionUtils.dp2px(mImageWidth).toInt()
)
canvas.drawBitmap(avatarBitmap, 0f, 0f, mPaint)
mCamera.restore()
canvas.restore()
}
}
}
三、结语
- 案例代码已上传Github,案例代码详情可戳—>代码案例内容传送门
- 小米联合创始人黎万强《参与感》中提到:互联网是注意力经济,一个品牌和事件的关注度,一定要有碰撞,有矛盾,有张力才起得来。所以,传播途中有不同声音不但正常,还可能是好事,在其中因势利导,抓主流就可以了。一个传播事件中,如果有七成是正面声音就很好了,剩下的三成负面的其实也无所谓。
- 希望看完内容的你提出最真实的建议和意见,这是促进我更博的最大动力☺,希望能提供优质的内容与你分享!
网友评论