前言

本文的目的有兩個:
- 大多數(shù)時候,自定義View并不會被用到,但一旦用到,通常都是很炫酷的效果。App的開發(fā)本身并不酷,讓它們變酷的是設計師們的想象力與創(chuàng)造力。對于開發(fā)工程師而言,要做的,就是把他們的想象力與創(chuàng)造力變成現(xiàn)實。
- Kotlin結合自定義View效果的實現(xiàn),只要是 Java 能做的事情,Kotlin 都可以做,甚至還可以做得更好。
- 案例代碼已上傳Github,案例代碼詳情可戳—>代碼案例內(nèi)容傳送門
接下來就是本文的主題核心內(nèi)容:
一、儀表盤

圖1-1 儀表盤效果的實現(xiàn)
1、思路分析:
①拆分為外層圓弧
- 外層圓弧可通過canvas.drawArc()的形式進行實現(xiàn),在本文中我通過Path首先添加了最外層的圓弧。
- 當前的控件,我使其填充屏幕,對于圓弧首先需指定其所在的矩形范圍,再指定圓弧的占有角度。
以下為關鍵實現(xiàn)內(nèi)容:
//先onDraw()繪制內(nèi)容中,畫外層的圓弧
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
//刻度線數(shù)量
val mDashCount: Int = 20
- 然后在初始化代碼塊中,先定義一個小矩形的寬和高;
在這里我設置路徑的寬高分別為3dp、8dp,并做了相關的適配:
強調(diào)一點: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的四個參數(shù)中,在上面的官網(wǎng)貼圖中我已經(jīng)展示,簡單做總結:
| 參數(shù) | 意義 |
|---|---|
| shape | 繪制路徑 |
| advance | 繪制間距 |
| phase | 繪制偏移 |
| style | 繪制樣式 |
根據(jù)原生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節(jié)選):
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,詳情查看—>官網(wǎng)文檔傳送門
圖1-2-1 PathDashPathEffect的官網(wǎng)說明
③拆分為內(nèi)層指向線
繪制內(nèi)層的指向線就很簡單了,在這里我指向了第5個刻度線,根據(jù)角度進行換算獲取
//畫指示器
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
)
至此,刻度盤的效果實現(xiàn),總體實現(xiàn)代碼如下:
/**
* @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
//刻度線數(shù)量
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.對于上半部分的圖片,不做任何轉換,以下為實現(xiàn)部分;
//在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對視角做變換,變換完畢后為了可以正常顯示,對畫布進行平移轉換,以下為代碼實現(xiàn)。
//下半部分
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()
}
- 在這里需要強調(diào),對視圖設置的角度,關鍵性參數(shù):mBottomAngle,提供給用戶設置;
- 在Kotlin中,我將mBottomAngle定義為成員變量,提供給調(diào)用者進行改變,在調(diào)用屬性的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()
最后效果即實現(xiàn),如下圖,其中間的變化效果參考效果演示的Gif圖:

圖2-1-3 折頁圖最終效果
至此,折頁效果已實現(xiàn),附上實現(xiàn)代碼的類文件內(nèi)容:
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,案例代碼詳情可戳—>代碼案例內(nèi)容傳送門
- 小米聯(lián)合創(chuàng)始人黎萬強《參與感》中提到:互聯(lián)網(wǎng)是注意力經(jīng)濟,一個品牌和事件的關注度,一定要有碰撞,有矛盾,有張力才起得來。所以,傳播途中有不同聲音不但正常,還可能是好事,在其中因勢利導,抓主流就可以了。一個傳播事件中,如果有七成是正面聲音就很好了,剩下的三成負面的其實也無所謂。
- 希望看完內(nèi)容的你提出最真實的建議和意見,這是促進我更博的最大動力?,希望能提供優(yōu)質的內(nèi)容與你分享!
