自定義View實戰(zhàn)——Kotlin綜合效果篇

前言

本文的目的有兩個:

  1. 大多數(shù)時候,自定義View并不會被用到,但一旦用到,通常都是很炫酷的效果。App的開發(fā)本身并不酷,讓它們變酷的是設計師們的想象力與創(chuàng)造力。對于開發(fā)工程師而言,要做的,就是把他們的想象力與創(chuàng)造力變成現(xiàn)實。
  2. Kotlin結合自定義View效果的實現(xiàn),只要是 Java 能做的事情,Kotlin 都可以做,甚至還可以做得更好。

一、儀表盤

圖1-1 儀表盤效果的實現(xiàn)

1、思路分析:

①拆分為外層圓弧

  1. 外層圓弧可通過canvas.drawArc()的形式進行實現(xiàn),在本文中我通過Path首先添加了最外層的圓弧。
  2. 當前的控件,我使其填充屏幕,對于圓弧首先需指定其所在的矩形范圍,再指定圓弧的占有角度。
    以下為關鍵實現(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 外層圓弧繪制圖解

②拆分為中層矩形刻度尺

  1. 先定義PathDashPathEffect變量:
    //路徑改變器
    lateinit var mPathDashPathEffect: PathDashPathEffect
    //刻度線數(shù)量  
    val mDashCount: Int = 20
  1. 然后在初始化代碼塊中,先定義一個小矩形的寬和高;
    在這里我設置路徑的寬高分別為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
        )
    }
  1. 然后在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
        )
    }
  1. 最后在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()
        }
    }
}

三、結語

  1. 案例代碼已上傳Github,案例代碼詳情可戳—>代碼案例內(nèi)容傳送門
  2. 小米聯(lián)合創(chuàng)始人黎萬強《參與感》中提到:互聯(lián)網(wǎng)是注意力經(jīng)濟,一個品牌和事件的關注度,一定要有碰撞,有矛盾,有張力才起得來。所以,傳播途中有不同聲音不但正常,還可能是好事,在其中因勢利導,抓主流就可以了。一個傳播事件中,如果有七成是正面聲音就很好了,剩下的三成負面的其實也無所謂。
  • 希望看完內(nèi)容的你提出最真實的建議和意見,這是促進我更博的最大動力?,希望能提供優(yōu)質的內(nèi)容與你分享!
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容