android 自定義view 跑馬燈-光圈效果

語言: CN / TW / HK

theme: nico

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發佈[2022-12-02]

本系列自定義View全部採用kt

系統: mac

android studio: 4.1.3

kotlin version: 1.5.0

gradle: gradle-6.5-bin.zip

本篇效果:

8140FE3CF87738708E0C5D0E4F59704F

前沿

最近在bilibili看到一個跑馬燈光圈效果挺好, 參考着思路寫了一下.

bilibili地址,美中不足的是這是html代碼 QaQ

實現思路

  • 將效果分為3層

    • 第一層: 背景
    • 第二層: 跑馬燈光圈
    • 第三層: 展示區

如圖所示:

Nov-28-2022 17-19-34

tips: 圖片截取自上方bilibili視頻

換到android中直接將view當作背景層, 在利用Canvas繪製跑馬燈層即可

將View圓角化

// 設置view圓角  outlineProvider = object : ViewOutlineProvider() {    override fun getOutline(view: View, outline: Outline) {      // 設置圓角率為      outline.setRoundRect(0, 0, view.width, view.height, RADIUS)   }  }  clipToOutline = true

這段代碼網上找的,源碼還沒有看, 有機會再看吧.

image-20221128173221355

來看看當前效果:

CD09F6ED6DBE6895E487C703B7DB64F0

自定義跑馬燈光圈

這幾個字可能有點抽象,所以來看看要完成的效果:

Nov-28-2022 17-45-34

接下來只需要吧黃框外面和裏面的的去掉就完成了旋轉的效果:

去掉外面:

Nov-28-2022 17-47-38

去掉裏面:

Nov-28-2022 17-47-32

這都是html效果,接下來看看android怎麼寫:

class ApertureView @JvmOverloads constructor(      context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0  ) : View(context, attrs, defStyleAttr) {      companion object {          val DEF_WIDTH = 200.dp          val DEF_HEIGHT = DEF_WIDTH          private val RADIUS = 20.dp     }  ​      private val paint = Paint(Paint.ANTI_ALIAS_FLAG)  ​      private val rectF by lazy {          val left = 0f + RADIUS / 2f          val top = 0f + RADIUS / 2f          val right = left + DEF_WIDTH - RADIUS          val bottom = top + DEF_HEIGHT - RADIUS          RectF(left, top, right, bottom)     }  ​      override fun onDraw(canvas: Canvas) {          val left = rectF.left + rectF.width() / 2f          val right = rectF.right + rectF.width()          val top = rectF.top + rectF.height() / 2f          val bottom = rectF.bottom + rectF.height() / 2f  ​          // 繪製漸變view1          paint.color = Color.GREEN          canvas.drawRect(left, top, right, bottom, paint)  ​          // 繪製漸變view2          paint.color = Color.RED          canvas.drawRect(left, top, -right, -bottom, paint)  ​     }  }

這裏就是計算偏移量等,都比較簡單:

542DD72464B89550F97E8BAD9EFE6FD5

因為咋們是view,並且已經測量了view的寬和高,所以超出的部分就不展示了

跑馬燈動起來

這段代碼比較簡單,直接開一個animator即可

private val animator by lazy {     val animator = ObjectAnimator.ofFloat(this, "currentSpeed", 0f, 360f)     animator.repeatCount = -1     animator.interpolator = null     animator.duration = 2000L     animator   }  ​  var currentSpeed = 0f    set(value) {      field = value      invalidate()   }            override fun onDraw(canvas: Canvas) {  ​    // withSave 保存畫布    canvas.withSave {          // 畫布中心點旋轉    canvas.rotate(currentSpeed, width / 2f, height / 2f)      // 繪製漸變view1 繪製漸變view2     ...   }  }

14162A8D36FFE0BEB6CD9B9D5A67446F

'去掉'裏面

去除裏面部分有2種方式

  • 方式一: 利用 clipOutPath() 來clip掉中間區域, 這個api對版本有要求
  • 方式二: 重新繪製一個 RoundRect() 來覆蓋掉中間區域

方式一:

private val path by lazy {      Path().also { it.addRoundRect(rectF, RADIUS, RADIUS, Path.Direction.CCW) }  }  ​  override fun onDraw(canvas: Canvas) {  ​      // withSave 保存畫布      canvas.withSave {        canvas.clipOutPath(path)           // 畫布中心點旋轉        canvas.rotate(currentSpeed, width / 2f, height / 2f)                // 繪製漸變view1 ..view2...     }  }

方式二:

override fun onDraw(canvas: Canvas) {    // withSave 保存畫布    canvas.withSave {  ​      // 畫布中心點旋轉      canvas.rotate(currentSpeed, width / 2f, height / 2f)  ​      // 繪製漸變view1  ​      // 繪製漸變view2  ​   }  ​    paint.color = Color.BLACK    canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)  }

來看看當前效果:

B9B3733C51780A7AFB53CBA080582B20

但是現在看起來還是有一點生硬, 可以讓view漸變一下

private val color1 by lazy {    LinearGradient(width * 1f,height / 2f,width * 1f,height * 1f,      intArrayOf(Color.TRANSPARENT, Color.RED), floatArrayOf(0f, 1f),      Shader.TileMode.CLAMP   )  }  ​  private val color2 by lazy {    LinearGradient( width / 2f,height / 2f,width / 2f, 0f,      intArrayOf(Color.TRANSPARENT, Color.GREEN), floatArrayOf(0f, 1f),      Shader.TileMode.CLAMP   )  }  ​  override fun onDraw(canvas: Canvas) {  //    canvas.withSave {      canvas.rotate(currentSpeed, width / 2f, height / 2f)     ...      // 繪製漸變view1      paint.shader = color1      canvas.drawRect(left1, top1, right1, bottom1, paint)      paint.shader = null  ​      // 繪製漸變view2      paint.shader = color2      canvas.drawRect(left1, top1, -right1, -bottom1, paint)      paint.shader = null   }  ​    // 中間rect    canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)  }

這樣一來,就更有感覺了

效果圖:

FBFD3920C18DA5E6821CA08C9CFB8052

基本效果就完成了,那麼如何給其他view也可以輕鬆的添加這個炫酷的邊框呢?

很顯然,view是辦不到的,所以我們只能自定義viewgroup

代碼沒有改變,只是在自定義viewgroup時,onDraw() 不會回調, 因為viewgroup主要就是用來管理view的,所以要想繪製viewgroup最好是重寫dispatchDraw()方法,

在dispatchDraw()方法中,需要注意的是 super.dispatchDraw(canvas) , 這個super中會繪製children,

所以為了避免 view被跑馬燈背景覆蓋,需要將super.dispatchDraw(canvas) 寫到最後一行

#ApertureViewGroup.kt  ​  override fun dispatchDraw(canvas: Canvas) {          val left1 = width / 2f          val top1 = height / 2f  ​          val right1 = left1 + width          val bottom1 = top1 + width          canvas.withSave {              canvas.rotate(currentSpeed, width / 2f, height / 2f              // 繪製漸變view1              paint.shader = color1              canvas.drawRect(left1, top1, right1, bottom1, paint)              paint.shader = null  ​              if (mColor2 != -1) {                  // 繪製漸變view2                  paint.shader = color2                  canvas.drawRect(left1, top1, -right1, -bottom1, paint)                  paint.shader = null             }         }  ​          paint.color = mMiddleColor          canvas.drawRoundRect(rectF, mBorderAngle, mBorderAngle, paint)  ​  // 一定要寫到最後一行,否則children會被跑馬燈覆蓋掉          super.dispatchDraw(canvas)     }

最後在調用的時候直接:

<ApertureViewGroup      android:layout_width="200dp"      android:layout_height="200dp"  ​      // 邊框顏色      android:background="@color/cccccc"                                                          // 邊框寬度                                                  app:aperture_border_width="50dp"                          // 邊框角度      app:aperture_border_angle="20dp"                                                   // 漸變顏色1      app:aperture_color1="@color/purple_200"                                                                                                  // 漸變顏色2 如果不寫,默認只有一個漸變在跑馬燈      app:aperture_color2="@color/color_FFC107"                                                          // 旋轉時間      app:aperture_duration="3000"                                                          // 中間空心顏色      app:aperture_middle_color="@color/white">  ​      <XXXX View          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:gravity="center" />  </com.example.customviewproject.f.f2.ApertureViewGroup>

本篇代碼比較簡單,不過這個思路確實挺好玩的!

最終效果:

A051CC6A0481AE320B2371E271889D04

完整代碼

原創不易,您的點贊就是對我最大的幫助!