Android实战——一步一步实现流动的炫彩边框
目錄
- 1 前言
- 2 正文
- 2.1 方案選擇
- 2.2 給布局增加前景邊框
- 2.2.1 根據繪制順序產生前景效果
- 2.2.2 自定義 Drawable 繪制邊框
- 2.3 讓邊框炫彩
- 2.4 讓邊框流動起來
- 3 最后
1 前言
本文打算一步一步地實現流動的炫彩邊框,用來裝飾一個布局,如廣告布局,圖片,使它們可以看起來更加地醒目,更加地吸引用戶。
流動的炫彩邊框就是這樣的效果:
2 正文
2.1 方案選擇
先不考慮具體的實現細節,從大的方面來說,可以選擇的方案有:
- 使用自定義 View 來實現;
- 使用自定義 Drawable 來實現。
流動的炫彩邊框僅僅是為了裝飾一個布局,它自身并不需要處理觸摸反饋事件,所以,這種情況下,使用自定義 Drawable 來實現是比較合適的。
如果使用自定義 View 來實現,就需要把自定義的邊框 View 和原有的待裝飾的布局疊加在一起顯示,這會使得布局變得復雜一些;但是,自定義 View 的實現方式,如果使用到 SurfaceView 這種雙緩沖技術,會比自定義 Drawable 有一定的性能優勢。
本文采用自定義 Drawable 的方式來實現。
2.2 給布局增加前景邊框
2.2.1 根據繪制順序產生前景效果
這里我們以一個圖片的布局來舉例子,也就說,我們要做的是給圖片控件增加前景邊框。
自然地,會想到有沒有官方支持的 setForeground() 這樣的 API 呢?這種想法是合理的,如果官方有相應支持的 API,就應該去使用官方提供的 API;如果沒有的話,自己再去想辦法。
但是,官方確實沒有提供這樣的 API。只好自己去想辦法了。
我們打算創建一個繼承于 ImageView 的子類,重寫它的 onDraw() 方法,在這個方法里面的 super.onDraw(canvas) 之后添加繪制邊框的代碼,這樣不就是前景邊框的效果了。
class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) {override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 在這里添加繪制邊框的代碼,就是前景邊框效果了。} }在 activity_main.xml 中使用自定義的 MyImageView:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:padding="16dp"tools:context=".MainActivity"><com.example.fluidcolorfulframe.MyImageViewandroid:id="@+id/iv"app:srcCompat="@drawable/road"android:scaleType="fitXY"app:layout_constraintDimensionRatio="h,16:9"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintEnd_toEndOf="parent"android:layout_width="0dp"android:layout_height="0dp" /></androidx.constraintlayout.widget.ConstraintLayout>好了,我們已經知道在哪里添加前景邊框了。
運行程序,只可以看到一張圖片顯示:
2.2.2 自定義 Drawable 繪制邊框
下面就要寫繪制前景邊框的代碼了,前面已經說過要使用自定義 Drawable 的方式來做。
創建繼承于 Drawable 的子類 FluidColorfulFrameDrawable:
class FluidColorfulFrameDrawable: Drawable() {override fun draw(canvas: Canvas) {TODO("Not yet implemented")}override fun setAlpha(alpha: Int) {TODO("Not yet implemented")}override fun setColorFilter(colorFilter: ColorFilter?) {TODO("Not yet implemented")}override fun getOpacity(): Int {TODO("Not yet implemented")} }居然有 4 個方法還未實現。別擔心,我們現在就去實現它們吧。
class FluidColorfulFrameDrawable: Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)override fun draw(canvas: Canvas) {// 這里就是繪制邊框的地方}override fun setAlpha(alpha: Int) {paint.alpha = alpha}override fun setColorFilter(colorFilter: ColorFilter?) {paint.colorFilter = colorFilter}override fun getOpacity(): Int {return PixelFormat.TRANSLUCENT} }對于自定義 Drawable 來說,還需要重寫一個 setBounds 方法,用來決定繪制的范圍,也就是說當它的 draw() 方法被調用時,Drawable 要繪制在哪里。
class FluidColorfulFrameDrawable : Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)private lateinit var bounds: RectFprivate val rectF = RectF()// 10.dp 是代碼里面對 Int 類型定義的擴展屬性,把 dp 值轉為 px。private val defaultRadius: Float = 10.dpprivate val defaultStrokeWidth: Float = 5.dpinit {// 配置畫筆paint.color = Color.REDpaint.style = Paint.Style.STROKEpaint.strokeWidth = defaultStrokeWidth}override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)// 記錄 Drawable 的繪制范圍在 bounds 對象里面bounds = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())}override fun draw(canvas: Canvas) {// 繪制帶圓角的矩形邊框canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint)}... }現在直接去運行,是看不到效果的。因為我們還沒有去使用自定義的 Drawable。
在 MyImageView 里面使用自定義 Drawable:
class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) {// 創建自定義 Drawable 對象private val drawable = FluidColorfulFrameDrawable()override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)// 設置 Drawable 的范圍drawable.setBounds(0, 0, w, h)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 繪制 Drawabledrawable.draw(canvas)} }運行程序,查看效果如下:
可以看到,邊框是作為前景顯示的,這點是沒有問題的。
但是,有兩個不對的地方:圖片的每個角都在圓角邊框之外了;邊框的線寬顯示偏細。
圖片的每個角都在圓角邊框之外了:這是因為圖片的顯示區域和邊框的邊界是一樣大的,而邊框是有圓角的,這樣圖片的四個角就一定是在邊框之外了。這個問題可以通過給圖片添加 padding 來解決。代碼如下:
class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) {...init {// 這里的 5dp 是和邊框的線寬保持一致的。setPadding(5.dp.toInt())}... }運行效果如下:
邊框的線寬顯示偏細:這是因為繪制邊框時使用的矩形區域是 MyImageView 傳入的邊界矩形,而畫筆是有 5dp 的寬度的。這個問題可以通過創建新的矩形對象,給這個矩形對象設置抵消掉畫筆寬度的左上右下值,并使用新的矩形對象來繪制。代碼如下:
class FluidColorfulFrameDrawable : Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)private lateinit var bounds: RectFprivate val rectF = RectF()private val defaultRadius: Float = 10.dpprivate val defaultStrokeWidth: Float = 5.dp...override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)bounds = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())rectF.left = defaultStrokeWidth / 2rectF.top = defaultStrokeWidth / 2rectF.right = bounds.width() - defaultStrokeWidth / 2rectF.bottom = bounds.height() - defaultStrokeWidth / 2}override fun draw(canvas: Canvas) {canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint)}... }運行效果如下:
2.3 讓邊框炫彩
由一種顏色組成的邊框,看著實在單調。我們希望邊框可以由多種顏色組成,看著流光溢彩一樣地。
這可以通過給畫筆設置一個 SweepGradient 類型對象的著色器來實現。
class FluidColorfulFrameDrawable : Drawable() {...private val colors: IntArrayprivate val positions: FloatArrayinit {paint.style = Paint.Style.STROKEpaint.strokeWidth = defaultStrokeWidthcolors = intArrayOf("#FF0000FF".toColorInt(), // 藍 0f"#FF000000".toColorInt(), // 黑 0.02f"#FF000000".toColorInt(), // 黑 0.25f"#FFFF0000".toColorInt(), // 紅 0.27f"#FFFF0000".toColorInt(), // 紅 0.37f"#FF00FF00".toColorInt(), // 綠 0.39f"#FF0000FF".toColorInt(), // 藍 0.49f"#FFFFFF00".toColorInt(), // 黃 0.51f"#FF000000".toColorInt(), // 黑 0.53f"#FF000000".toColorInt(), // 黑 0.75f"#FFFF0000".toColorInt(), // 紅 0.77f"#FFFF0000".toColorInt(), // 紅 0.87f"#FFFFFF00".toColorInt(), // 黃 0.91f"#FF0000FF".toColorInt(), // 藍 0.96f)positions = floatArrayOf(0f,0.02f,0.25f,0.27f,0.37f,0.39f,0.49f,0.51f,0.53f,0.75f,0.77f,0.87f,0.91f,0.96f,)}override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)...paint.shader = SweepGradient(bounds.centerX(), bounds.centerY(), colors, positions)}... }這里使用的是 SweepGradient 可以配置多個顏色,多個位置的構造方法。
public SweepGradient(float cx, float cy, @NonNull @ColorInt int[] colors,@Nullable float[] positions)需要說明的是:
colors 參數是不可以為 null 的,并且至少要包括兩個顏色值。
positons 參數是可以為 null 的:
-
如果 positions 為 null,那么 colors 中的顏色值會自動均勻分布開來。
-
如果 positions 不為 null,那么它的長度必須與 colors 的長度保持一致;而且,它的元素值需要是依次遞增的,范圍在 0f 到 1f 之間。另外,官方文檔里面說:
The relative position of each corresponding color in the colors array, beginning with 0 and ending with 1.0.
positions 數組里的元素以 0 開始,以 1.0 結束。
官方文檔的說法是不對的。實際上,positions 數組的元素并非要以 0 開始,以 1.0 結束。
運行程序,查看效果:
2.4 讓邊框流動起來
讓邊框流動起來,就是讓邊框旋轉起來。這里要使用到屬性動畫和 Shader 的本地矩陣方法來處理。
class FluidColorfulFrameDrawable : Drawable() {...private val mtx = Matrix()private var degree: Float = 0fset(value) {field = value// 刷新自己invalidateSelf() }...override fun draw(canvas: Canvas) {// 設置本地矩陣mtx.reset()mtx.setRotate(degree, bounds.centerX(), bounds.centerY())(paint.shader as SweepGradient).setLocalMatrix(mtx)canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint)}...private var fluidAnim: ObjectAnimator? = null// 開始流動fun startFluid() {fluidAnim = ObjectAnimator.ofFloat(this, "degree", 0f, 360f).apply {duration = 2000Linterpolator = LinearInterpolator()repeatCount = ValueAnimator.INFINITEstart()}}// 取消流動fun cancelFluid() {fluidAnim?.cancel()} }在 MyImageView 里面調用開始流動和取消流動的方法:
class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) {private val drawable = FluidColorfulFrameDrawable()...override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)drawable.setBounds(0, 0, w, h)drawable.startFluid()}...override fun onDetachedFromWindow() {super.onDetachedFromWindow()drawable.cancelFluid()} }運行程序,查看流動效果:
居然沒有流動效果!
需要具體分析一下,這里在 degree 的 setter 方法里面和 draw 方法里面增加日志打印:
private var degree: Float = 0fset(value) {field = valueLog.d(TAG, "degree setter called")invalidateSelf()} override fun draw(canvas: Canvas) {Log.d(TAG, "draw: ")mtx.reset()mtx.setRotate(degree, bounds.centerX(), bounds.centerY())(paint.shader as SweepGradient).setLocalMatrix(mtx)canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint) }運行程序后,可以看到下面這行日志在不停地打印:
D/FluidColorfulFrame: draw: D/FluidColorfulFrame: degree setter called D/FluidColorfulFrame: degree setter called D/FluidColorfulFrame: degree setter called ... // 后面全是重復 degree setter called 的日志這就說明 invalidateSelf() 方法并沒有觸發 draw 方法的調用了。現在去看一下 Drawable 類的 invalidateSelf 方法的源碼:
public void invalidateSelf() {final Callback callback = getCallback();if (callback != null) {callback.invalidateDrawable(this);} }內部是通過 getCallback() 方法獲取一個 Callback 對象;如果 Callback 對象不為 null,則調用其 invalidateDrawable 方法并且把 Drawable 對象傳入這個方法。
繼續查看 getCallback() 方法以及相關的字段和方法:
private WeakReference<Callback> mCallback = null; public final void setCallback(@Nullable Callback cb) {mCallback = cb != null ? new WeakReference<>(cb) : null; } public Callback getCallback() {return mCallback != null ? mCallback.get() : null; }我們并沒有調用自定義 Drawable 對象的 setCallback 方法,所以 getCallback() 方法的返回值是 null,在 invalidateSelf 方法里面就不會回調 invalidateDrawable 方法了。
在 MyImageView 中設置 Drawable 對象的 setCallback 方法并重寫 invalidateDrawable 方法:
class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) {private val drawable = FluidColorfulFrameDrawable()init {setPadding(5.dp.toInt())// 設置 callbackdrawable.callback = this}...// 重寫 invalidateDrawableoverride fun invalidateDrawable(dr: Drawable) {super.invalidateDrawable(dr)// 如果回調的 dr 就是 drawable,就調用重繪方法。if (dr === drawable) {invalidate()}} }重新運行程序,查看效果:
3 最后
本文一步一步地展示了流動炫彩邊框的實現過程,用到了自定義 Drawable,畫筆的著色器(掃描漸變,本地矩陣),屬性動畫,Drawable 與 View 的刷新回調等知識。
本文并沒有演示如何給一個 ViewGroup 類型的控件添加邊框,但是相信這個是難不倒大家了。
代碼已經上傳到 Github。希望可以幫助到大家,也歡迎大家點贊分享。
總結
以上是生活随笔為你收集整理的Android实战——一步一步实现流动的炫彩边框的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 修复鹏城开发者云硬盘扩容报错 fdisk
- 下一篇: 一种很强的对联,看了让我想起高中时期那会