高級 UI 成長之路 (六) PathMeasure 製做路徑動畫

前言

咱們來回顧一下前面 5 篇文章咱們講解的內容,先是從 View 基礎 ,事件分發,View 工做流程,而後是 Paint, Canvas 講解,該篇咱們仍是分 2 個部分講解,先是 Path, 而後是 PathMeaure ,若是想作出比較炫的動畫,那麼必不可少是離不開這 2 個類的,下面仍是以 API 的使用和 demo 實戰來說解。css

Path

Path 又叫路徑,它是一個比較重要的概念,在自定義 View 中它的重要程度基本上跟 Paint 差很少,那麼它能夠用來幹什麼勒, 能夠說 Path 是萬能的也不爲過,爲何這麼說呢,由於只要給我任何一個 Path 路徑,我就能把它繪製出來。下面咱們先來熟悉一下它有哪些 API 吧,請看下錶:html

API 功能 說明
moveTo 移動起點 移動下一次操做的起點位置
setLastPoint 設置終點 重置當前 path 中最後一個點位置,若是在繪製以前調用,效果和 moveTo 相同
lineTo 鏈接直線 添加上一個點到當前點
close 閉合路徑 鏈接第一個點到最後一個點,造成一個閉合區間
addRect,addRoundRect,addOval,addCircle,addPath,addArc,arcTo 添加內容 添加(矩形,圓角矩形,橢圓,圓,路徑,圓弧)到當前 Path 中
isEmpty 是否爲空 判斷當前 Path 是不是空的
isRect 是否爲矩形 判斷 Path 是不是一個矩形
set 替換路徑 用新的路徑替換到當前路徑的全部內容
offset 偏移路徑 對當前路徑以前的操做進行偏移(不會影響以後的操做)
quadTo,cubicTo 貝塞爾曲線 分別爲二次和三次貝塞爾取消的方法
rMoveTo, rLineTo,rQuadTo,rCubicTo rXXX方法 不帶 r 的方法時基於遠點的座標系(偏移量),rXXX 方法是基於當前點座標系(偏移量)
setFillType, getFillType,isInverseFilltype,toggleInverseFilltype 填充模式 設置,獲取,判斷和切換填充模式
incReserve 提示方法 提示 Path 還有多少個點等待加入
op 布爾操做 對2個Path進行布爾運算(取交集並集)
computeBounds 計算Path 的邊界 計算邊界
reset,rewind 重置路徑 清除Path中的內容,reset不保留內部數據結構,但會保留 Filltype,rewind會保留內部的數據結構,但不保留 FillType
transform 矩陣操做 矩陣變換
  1. moveTo,lineTo,setLastPoint,closejava

    //從0.0 鏈接 400,600
    mPath.lineTo(400f,600f)
    //重置上一點至關於 0,0 到 600,200, 設置以前操做的最後一個點位置(會影響以前跟以後的起始點)
    //mPath.setLastPoint(600f,200f)
    //從 400,600 鏈接 900,100
    mPath.lineTo(900f,100f)
    //開始繪製
    anvas!!.drawPath(mPath,mPathPaint)
    複製代碼

    咱們把上面註釋放開,以下代碼:git

    //從0.0 鏈接 400,600
    mPath.lineTo(400f,600f)
    //重置上一點至關於 0,0 到 600,200, 設置以前操做的最後一個點位置(會影響以前跟以後的起始點)
    mPath.setLastPoint(600f,200f)
    //從 600,200 鏈接 900,100
    mPath.lineTo(900f,100f)
    //開始繪製
    anvas!!.drawPath(mPath,mPathPaint)
    複製代碼

    實現效果以下:github

    經過上圖咱們發現 setLastPoint 設置了以後改變了以前的最後一次的座標點,能夠理解爲更新最後一點的座標,咱們發現每次都是從座標角 (0,0) 開始繪製,那麼有沒有一個方法指定從哪一個起點開始繪製,正好,你能夠試試 moveTo 它能夠指定 path 的起點,以下代碼:canvas

    //moveTo 設置起點
            mPath.moveTo(600f,200f)
            //從0.0 鏈接 400,600
            mPath.lineTo(400f,600f)
            mPath.lineTo(800f,300f)
            //最後一點和起點封閉
            mPath.close()
    複製代碼

    經過上圖咱們先利用 Path#moveTo 將 Path 起點設置爲 (400,600) 開始繪製,最後調用了 Path#close 將爲閉合的鏈接線閉合。數據結構

  2. addXxx 系列ide

    咱們就以 矩形,圓角矩形,橢圓,圓,圓弧 的順序繪製函數

    //Path.Direction.CW/CCW 順時針/逆時針
    
    //1. 添加矩形到 Path
    void addRect (float left, float top, float right, float bottom, Path.Direction dir) //2. 添加 圓角矩形到 Path void addRoundRect (RectF rect, float[] radii, Path.Direction dir) void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir) //3. 添加 橢圓 到 Path void addOval (RectF oval, Path.Direction dir) //4. 添加 圓 到 Path void addCircle (float x, float y, float radius, Path.Direction dir) //5. 添加 圓弧 到 Path ,直接添加一個圓弧到path中 void addArc (RectF oval, float startAngle, float sweepAngle) //添加一個圓弧到 path,若是圓弧的起點和上次最後一個座標點不相同,就鏈接兩個點 void arcTo (RectF oval, float startAngle, float sweepAngle) void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) 複製代碼
    mPathPaint.textSize = 50f
            //1. 添加矩形到 Path
            mPath.addRect(100f,300f,400f,700f,Path.Direction.CW)//順時針
            canvas!!.drawText("1",200f,500f,mPathPaint)
    
            //2. 添加 圓角矩形到 Path
            mPath.addRoundRect(100f + 500,300f,1000f ,700f,30f,30f,Path.Direction.CCW)//逆時針
            canvas!!.drawText("2",800f,500f,mPathPaint)
    
            //3. 添加 橢圓 到 Path
            mPath.addOval(100f,1300f,600f ,1000f,Path.Direction.CCW)//逆時針
            canvas!!.drawText("3",300f,1150f,mPathPaint)
    
            //4. 添加 圓 到 Path
            mPath.addCircle(850f,1200f ,150f,Path.Direction.CCW)//逆時針
            canvas!!.drawText("4",850f,1200f,mPathPaint)
    
            //5. 添加 圓弧 到 Path ,直接添加一個圓弧到path中
            //添加一個圓弧到 path,若是圓弧的起點和上次最後一個座標點不相同,就鏈接兩個點
            mPath.addArc(100f,1500f,600f,1800f,0f,300f)
            canvas!!.drawText("5",300f,1550f,mPathPaint)
    				
            mPath.arcTo(650f,1500f,800f,1800f,0f,180f,true)
            canvas!!.drawText("6",750f,1550f,mPathPaint)
    
            canvas!!.drawPath(mPath, mPathPaint)
    複製代碼

    這裏注意一點 addTo 最後一個參數 forceMoveTo 它的意思爲「是否強制使用 moveTo 」,也就是說,是否使用 moveTo 將變量移動到圓弧的起點位移 ,也就意味着:oop

    forceMoveTo 含義 等價方法
    true 將最後一個點移動到圓弧起點,即不鏈接最後一個點與圓弧起點 public void addArc (RectF oval, float startAngle, float sweepAngle)
    false 不移動,而是鏈接最後一個點與圓弧起點 public void arcTo (RectF oval, float startAngle, float sweepAngle)
  3. computeBounds,set,setPath

    1. 先繪製一個 圓和矩形

      mPath.addCircle(500f, 500f, 150f, Path.Direction.CW)
      
      
      canvas!!.drawPath(mPath, mPathPaint)    // 繪製Path
      
      
      canvas.drawRect(300f,800f,800f,1300f ,mPathPaint)   // 繪製矩形
      複製代碼
    2. 重寫 onTouchEvent 實現點擊事件

      override fun onTouchEvent(event: MotionEvent): Boolean {
          when (event.action) {
            	//須要按下事件
              MotionEvent.ACTION_DOWN -> return true
              MotionEvent.ACTION_UP -> {
                  val rectF = RectF()
                	//計算 Path 邊界
                  mPath.computeBounds(rectF, true)
                	//將邊界放入矩形區域內
                  region.setPath(
                      mPath,
                      Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt())
                  )
                  if (region.contains(event.x.toInt(), event.y.toInt())) {
                      Toast.makeText(context, "點擊了圓", Toast.LENGTH_SHORT).show()
                  }
      						//用新的路徑替換到當前路徑全部內容
                  region.set(300, 800, 800, 1300)
                  if (region.contains(event.x.toInt(), event.y.toInt())) {
                      Toast.makeText(context, "點擊了矩形", Toast.LENGTH_SHORT).show()
                  }
              }
          }
          return super.onTouchEvent(event)
      }
      複製代碼
    3. 效果

Path 咱們就先學習到這裏,該篇文章後面會有 Path 實戰。

PathMeasure

上部分咱們講解了 Path 路徑的知識,如今來看 Path 除了繪製圖形好像也沒什麼做用,固然若是隻是單純顯示 Path 繪製的圖形,那我也就不介紹該篇的重點了。Android SDK 提供了一個很是有用的 API 來幫組開發者實現一個 Path 路徑追蹤,這個 API 就是 PathMeasure , 經過它能夠實現複雜切絢麗的效果。

概念

PathMeasure 相似一個計算器,能夠計算出指定路徑的一些信息,好比路徑總長、指定長度所對應的座標點等。

API 使用

構造方法

//1.空參
 public PathMeasure()
//2.path 表明一個已經完成的 Path,forceClosed 表明是否最後閉合
 public PathMeasure(Path path, boolean forceClosed)
複製代碼

簡單函數使用

  1. getLength() 函數

    PathMeasure#getLength() 函數的使用很是普遍,其做用就是獲取計算的路徑長度,下面以一個例子來看下它的用法。

    效果:

    代碼:

    override fun draw(canvas: Canvas) {
            super.draw(canvas)
            /** * 1. getLength */
            //將起點移動到 100,100 的位置
            mPath.moveTo(100f,100f)
            //繪製鏈接線
            mPath.lineTo(100f,450f)
            mPath.lineTo(450f,500f)
            mPath.lineTo(500f,100f)
            mPathMeasure.setPath(mPath,false)//不被閉合
            mPathMeasure2.setPath(mPath,true)//閉合
            println("forceClosed false pathLength =${mPathMeasure.length}")
            println("forceClosed true pathLength =${mPathMeasure2.length}")
            canvas.drawPath(mPath,mPathPaint)
        }
    複製代碼

    輸出:

    System.out: forceClosed false pathLength =1106.6663
    System.out: forceClosed true pathLength =1506.6663
    複製代碼

    能夠看見,若是 forceClosed 設置爲 true/false 測量的是各自的 path 。

  2. isClosed() 函數

    該函數用於判斷測量 Path 時是否計算閉合。因此,若是在關聯 Path 的時候設置 forceClosed 爲 true ,那麼這個函數的返回值也必定爲 true.

  3. nextContour() 函數

    咱們知道,Path 能夠由多條曲線構成,但不管是 getLength()、getSegment() 仍是其它函數,都只會對針對其中第一條線段進行計算。而 nextContour 就是用於跳轉到下一條曲線的函數,若是跳轉成功,則返回 true ; 若是跳轉失敗,則返回 false.下面看一個示例,分別建立 3 條閉合 Path,而後利用 PathMeasure 來依次測量。

    效果:

    代碼:

    /** * 2. nextContour */
            mPath.addCircle(500f,500f,10f,Path.Direction.CW)
            mPath.addCircle(500f,500f,80f,Path.Direction.CW)
            mPath.addCircle(500f,500f,150f,Path.Direction.CW)
            mPath.addCircle(500f,500f,200f,Path.Direction.CW)
    
            mPathMeasure.setPath(mPath,false)//不被閉合
    
            canvas.drawPath(mPath,mPathPaint)
    
            do {
                println("forceClosed pathLength =${mPathMeasure.length}")
            }while (mPathMeasure.nextContour())
    複製代碼

    輸出:

    2019-12-03 22:37:22.340 18501-18501/? I/System.out: forceClosed  pathLength =62.42697
    2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed  pathLength =501.84265
    2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed  pathLength =942.0967
    2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed  pathLength =1256.1292
    複製代碼

    在這裏,咱們經過 do...while 循環和 measure.nextContour() 函數相結合,依次拿到 Path 中全部的曲線

    經過這個例子咱們能夠知道,經過 PathMeasure#nextContour 函數獲得的曲線順序與 Path 添加的順序相同

getSegment() 函數

//startD:開始截取位置距離 Path 起始點的長度
//stopD: 結束截取位置距離 Path 起始點的長度
//dst: 截取的 Path 將會被添加到 dst 中,注意是添加,而不是替換
//startWithMoveTo: 起始點是否使用 moveTo
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 複製代碼

getSegment 用於截取整個 Path 中的某個片斷,經過參數 startD 和 stopD 來控制截取的長度,並將截取後的 Path 保存到參數 dst 中。最後一個參數 startWithMoveTo 表示起始點是否使用 moveTo 將路徑的新起始點移到結果 Path 的起始點,一般設置爲 true ,以保證每次截取的 Path 都是正常完整的,一般和 dst 一塊兒使用,由於 dst 中保存的 Path 是被不斷添加的,而不是每次被覆蓋的;若是設置爲 false ,則新增的片斷會從上一次 Path 終點開始計算,這樣能夠保證截取的 Path 片斷是連續的。

注意:

  • 若是 startD ,stopD 數值不在取值範圍內,或者 startD == stopD ,那麼就會返回 false ,而且 dst 不會有 Path 數據。
  • 開啓硬件加速後,繪圖會出現問題,所以,在使用 getSegment 是須要 在構造函數中調用 setLayerType(LAYER_TYPE_SOFTWARE,null) 函數來禁用硬件加速

getSegment 舉例:

/** * 3. getSegment */
        mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
        mPathMeasure.setPath(mPath,false)//不被閉合
        val segment = mPathMeasure.getSegment(50f, 500f, mTempPath, true)
        println("是否截取成功:$segment")
        canvas.drawPath(mTempPath,mPathPaint)
複製代碼

效果:

注意:

若是 startWithMoveTo 爲 true,則被截取出來的 path 片斷保持原狀;若是 startWithMoveTo 爲 false ,則會將截取出來的 Path 片斷的起始點移動到 dst 的最後一個點,以保證 dst 路徑的連續性。

實現一個實時截取的動畫:

代碼實現:

  1. 定義一個值動畫

    val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
            valueAnimator.addUpdateListener {
                animation -> stopValues = animation.animatedValue as Float
                invalidate()
            }
            valueAnimator.repeatCount  = ValueAnimator.INFINITE
            valueAnimator.setDuration(1500)
            valueAnimator.start()
    複製代碼
  2. 實時截取繪製

    mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
            mPathMeasure.setPath(mPath,false)//不被閉合
            mTempPath.rewind()
            stop =   mPathMeasure.length * stopValues
            val start = (stop - (0.5 - Math.abs(stopValues - 0.5)) * mPathMeasure.length).toFloat()
            val segment = mPathMeasure.getSegment(start, stop, mTempPath, true)
            println("總長度:${mPathMeasure.length} 是否截取成功:$segment + start:$start stop:$stop")
            canvas.drawPath(mTempPath,mPathPaint)
    複製代碼

getPosTan

這個方法是用於獲得路徑上某一長度的位置以及該位置的正切值

//distance:距離 Path 起點的長度,取值範圍: 0 <= distance <= getLength
//pos:該點的座標值 , 當前點在畫布上的位置,有兩個數值,分別爲x,y座標。
//tan:該點的正切值, 當前點在曲線上的方向,使用 Math.atan2(tan[1], tan[0]) 獲取到正切角的弧度值。
boolean getPosTan (float distance, float[] pos, float[] tan) 複製代碼

下面以一個 demo 來說解 getPosTan 具體使用,先來看一個效果圖:

感受是否是很炫,那麼咱們是怎麼實現的呢?先來看一下核心代碼,以下:

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //清楚 path 數據
        mTempPath.rewind()
        //繪製一個模擬公路
        addLineToPath()
        //測量 path,閉合
        mPathMeasure!!.setPath(mTempPath, true)
        //動態變化的值
        mCurValues += 0.002f
        if (mCurValues >= 1) mCurValues = 0f
        //拿到當前點上的 正弦值座標
        mPathMeasure!!.getPosTan(mPathMeasure!!.length * mCurValues, pos, tan)
        //經過正弦值拿到當前角度
        val y = tan!![1].toDouble()
        val x = tan!![0].toDouble()
        var degrees = (Math.atan2(y, x) * 180f / Math.PI).toFloat()
        println("角度:$degrees")
        mMatrix!!.reset()
      //拿到 bitmap 須要旋轉的角度,以後將矩陣旋轉
        mMatrix!!.postRotate(degrees, mBitmap!!.width / 2.toFloat(), mBitmap!!.height / 2.toFloat())
      	//拿到 path 上的 pos 點隨着點移動
        mMatrix!!.postTranslate(pos!![0] - mBitmap!!.getWidth() / 2, pos!![1] - mBitmap!!.getHeight() / 2)
        //繪製Bitmap和path
        canvas!!.drawPath(mTempPath, mTempPaint)
        canvas!!.drawBitmap(mBitmap!!, mMatrix!!, mTempPaint)

        //重繪
        postInvalidate()
    }
複製代碼

這裏涉及到了初中數學,正弦值,固然 Android SDK API 也給咱們封裝了一個求正弦值的類 Math ,咱們能夠根據 PathMeasure#getPosTan 拿到當前點上的座標 tan[] ,而後根據 Math#atan 求出 tan ,最後根據 degrees * 180 / π 公式來求出角度。而後矩陣旋轉獲得一個旋轉以後的 car 不斷重繪就是如今這個效果了。仍是很簡單把。

這裏咱們簡單回顧下三角函數的計算吧

還不會的能夠參考這個文章正弦,餘弦,正切值計算

getMatrix

這個方法是用於獲得路徑上某一長度的位置以及該位置的正切值的矩陣:

參數 做用 備註
返回值(boolean) 判斷獲取是否成功 true表示成功,數據會存入matrix中,false 失敗,matrix內容不會改變
distance 距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
matrix 根據 falgs 封裝好的matrix 會根據 flags 的設置而存入不一樣的內容
flags 規定哪些內容會存入到matrix中 可選擇 POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切)

其實這個方法就至關於咱們在前一個例子中封裝 matrix 的過程由 getMatrix 替咱們作了,咱們能夠直接獲得一個封裝好到 matrix,豈不快哉。

可是咱們看到最後到 flags 選項能夠選擇 位置 或者 正切 ,若是咱們兩個選項都想選擇怎麼辦?

若是兩個選項都想選擇,能夠將兩個選項之間用 | 鏈接起來,以下:

measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
複製代碼

咱們能夠將上面都例子中 getPosTan 替換爲 getMatrix, 看看是否是會顯得簡單不少:

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        //清除 path 數據
        mTempPath.rewind()
        //繪製一個模擬公路
        addLineToPath()
        //測量 path,閉合
        mPathMeasure!!.setPath(mTempPath, true)
        //動態變化的值
        mCurValues += 0.002f
        if (mCurValues >= 1) mCurValues = 0f


        // 獲取當前位置的座標以及趨勢的矩陣
        mPathMeasure!!.getMatrix(mPathMeasure!!.getLength() * mCurValues, mMatrix!!,
            (PathMeasure.TANGENT_MATRIX_FLAG or PathMeasure.POSITION_MATRIX_FLAG))
        // 將圖片繪製中心調整到與當前點重合(偏移加旋轉)
        mMatrix!!.preTranslate(-mBitmap!!.getWidth() / 2f, -mBitmap!!.getHeight() / 2f);



        //繪製Bitmap和path
        canvas!!.drawPath(mTempPath, mTempPaint)
        canvas!!.drawBitmap(mBitmap!!, mMatrix!!, mTempPaint)

        //重繪
        postInvalidate()

    }
複製代碼

實現效果這裏跟上圖同樣就不在貼圖了,這裏不用在求 tan 角度 什麼的,看起來比第一種簡單把,具體使用哪種看實際需求場景吧。

實戰

蜘蛛網

詳細代碼 SpiderWebView.kt 請移步 GitHub

笑臉加載進度

實現原理:

利用 Path 路徑繪製眼睛 ,嘴巴,而後在經過Path#computeBounds 拿到 RectF 矩形邊界並繪製出來,最後經過動畫來執行不斷重繪截取,就造成了上面效果了。

(Ps: 上面效果只是一個練習 demo 不建議直接在項目中使用。)

詳細代碼 FaceLoadingView 請移步 GitHub

車隨路徑行駛

詳細代碼 CarRotate 請移步 GitHub

參考

相關文章
相關標籤/搜索