Android 中的各種 Drawable 你都知道了嗎?

Drawable

Drawable 翻譯成中文就是可繪製的意思,是“可繪製東西”的抽象(實際繪製通過 Canvas 進行)。Drawable 是一個抽象類,Andriod 也實現了很多具體的類型,下面一一介紹一下。

在這裏插入圖片描述

BitmapDrawable

BitmapDrawable 表示一張圖片,可以從文件路徑、輸入流、通過 XML 填充或從 Bitmap 對象中創建 BitmapDrawable。

在實際的開發中,可以直接在 XML 中引用圖片,或者通過 <bitmap>標籤在 XML 文件中定義它,通過 XML 定義比直接在 XML 中引用圖片資源可以有更多的設置選項,例如:


<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:antialias="true"
    android:dither="true"
    android:filter="true"
    android:src="@drawable/icon" />

XML 中可定義的屬性有:

  • antialias:是否啓用抗鋸齒。旋轉時可以使用抗鋸齒來平滑位圖的邊緣。默認值爲 false。

  • dither:是否開啓抖動效果。如果圖片的像素與屏幕像素不同,開啓這個選項可以讓高質量的圖片在低質量的屏幕上保持較好的顯示效果。默認值爲 true。

  • filter:是否開啓過濾效果。當圖片尺寸被拉伸或壓縮時,開啓該效果會有更好的顯示效果。默認值爲 true。

  • gravity:當圖片的小於容器的大小時(例如給佈局設置背景),設置該選項可以讓圖片相對於容器進行定位。默認值是 fill。

    • top:將圖片放在容器的頂部,不改變圖片的大小。

    • bottom:將圖片放在容器的底部,不改變圖片的大小。

    • left:將圖片放在容器的左側,不改變圖片的大小。

    • right:將圖片放在容器的右側,不改變圖片的大小。

    • start:將圖片放在容器的開頭,不改變圖片的大小。

    • end:將圖片放在容器的末端,不改變圖片的大小。

    • center:將圖片放在容器的中心,不改變圖片的大小。

    • center_horizontal:將圖片放在容器的水平中心,不改變圖片的大小。

    • center_vertical:將圖片放在容器的垂直中心,不改變圖片的大小。

    • fill:將圖片的水平和垂直方向進行填充,直到填滿容器。

    • fill_horizontal:將圖片的水平方向進行填充,直到填滿容器。

    • fill_vertical:將圖片的垂直方向進行填充,直到填滿容器。

    • clip_horizontal:可設置爲附加的選項,基於水平方向的 gravity 的設置將超出的部分進行裁剪。

    • clip_vertical:可設置爲附加的選項,基於垂直方向的 gravity 的設置將超出的部分進行裁剪。

  • mipMap:爲負責渲染此圖片的渲染器設置一個提示,表示在按比例縮小圖片時,渲染器應該嘗試使用 mipmap,如果我們事先知道會以比原圖更小的比例繪製圖片時,可以設置該屬性以獲得更高的圖片質量,但會使用額外的內存。這僅僅是對渲染器的一個建議,不能保證效果。與 Bitmap.setHasMipMap(boolean) 方法是一樣的效果。

  • src:設置圖片的資源 ID,必須設置的。

  • tileMode:設置平鋪的模式。默認爲 disabled。

    • clamp:複製邊緣的顏色進行平鋪。
      在這裏插入圖片描述
    • mirror:以鏡像效果平鋪。
      在這裏插入圖片描述
    • repeat:對原圖進行重複平鋪。
      在這裏插入圖片描述
    • disabled:不平鋪。
  • tileModeX:與 tileMode 效果類似,只不過只對水平方向進行平鋪。

  • tileModeY:與 tileMode 效果類似,只不過只對垂直方向進行平鋪。

NinePatchDrawable

表示一張 .9 圖,可調整大小,拉伸定義的區域。更多關於 .9 圖的信息可以看官方文檔。XML 中定義示例:


<?xml version="1.0" encoding="utf-8"?>
<nine-patch xmlns:android="http://schemas.android.com/apk/res/android"
    android:antialias="true"
    android:dither="true"
    android:filter="true"
    android:src="@drawable/bg_chat" />

支持的屬性同 BitmapDrawable 一樣。

ShapeDrawable

繪製基本形狀的 Drawable 對象。可以使用 <shape>標籤在 XML 文件中定義此對象,例如:


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/colorAccent" />

</shape>

需要注意的是,雖然我們將通過 shape 標籤定義的 Drawable 對象稱爲 ShapeDrawable,但是實際上構造出來的是 GradientDrawable 對象。

XML 中可定義的屬性有:

  • shape:定義圖形的形狀。默認值是 rectangle。

    • rectangle:矩形。

    • oval:橢圓。

    • line:橫線。

    • ring:圓環。針對圓環形狀,還有配套的幾個屬性。

      • innerRadius:圓環的半徑。

      • thickness:圓環的厚度。

      • innerRadiusRatio:圓環的半徑佔整個 Drawable 寬度的比例。

      • thicknessRatio:圓環的厚度佔整個 Drawable 寬度的比例。

      • useLevel:是否使用 level 屬性,一般都應該爲 false,除非它被當做 LevelListDrawable 來使用。

  • corners:設置 shape 四個角的角度,只適用於 rectangle。

    • radius:爲四個角同時設置相同的角度。

    • topLeftRadius:設置左上角的角度。

    • topRightRadius:設置右上角的角度。

    • bottomLeftRadius:設置左下角的角度。

    • bottomRightRadius:設置右下角的角度。

  • solid:使用顏色填充形狀,通過 color 可以指定填充的顏色。

  • gradient:使用漸變顏色填充形狀,與 solid 互斥。

    • type:漸變的類型。可以設置爲 line(線性漸變,默認值)、radial(輻射漸變)、sweep(掃描漸變)三種。

    • angle:漸變的角度,默認爲 0,值必須是 45 的倍數,僅當 type 爲 line 時纔有效。

    • gradientRadius:漸變半徑,僅當 type 爲 radial 時纔有效。

    • centerX:漸變的中心點橫座標。

    • centerY:漸變的中心點縱座標。

    • startColor:漸變的起始顏色。

    • centerColor:漸變的中間色。

    • endColor:漸變的結束色。

  • stroke:設置形狀的描邊。

    • width:描邊的寬度。

    • color:描邊的顏色。

    • dashWidth:虛線線段的寬度。

    • dashGap:虛線線段間的間隔。

  • padding:設置四周的邊距,具體就不說了,大家都很熟悉。

  • size:設置 shape 的固有大小。並不代表最終顯示的大小就是該大小,在作爲 View 背景的情況下,Drawable 的大小會拉伸或縮小到和 View 大小一致。

LayerDrawable

LayerDrawable 表示一種層次化的 Drawable,可以使用 <layer-list>標籤在 XML 文件中定義此對象,例如:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <stroke
                android:width="2dp"
                android:color="@color/colorAccent" />
        </shape>
    </item>
    <item
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <bitmap android:src="@drawable/icon" />
    </item>
</layer-list>

在上面的示例中,就通過疊加的特性加上偏移,爲圖片的四周增加了一圈邊框。

StateListDrawable

StateListDrawable 表示一組 Drawable 集合,根據 View 不同的狀態切換到不同的 Drawable,非常常用,例如做 Button 的按下效果等。可以在 XML 文件中使用 <selector>標籤定義:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/colorPrimaryDark" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/colorPrimary" />
        </shape>
    </item>
</selector>

View 會根據狀態在 selector 中從上到下查找符合的狀態,並切換到該狀態對應的 Drawable,因此,默認的 Drawable 應該放在最下面,當找不到對應狀態時進行兜底。

View 的各種狀態就不細說了,因爲大家都比較熟悉。來看看 StateListDrawable 支持的屬性:

  • constantSize:設置 Drawable 的大小是否不隨着狀態的改變而改變,當切換狀態時,也會切換到對應的 Drawable,而 Drawable 的大小可能不同,如果設置爲 true 的話,那麼就會固定大小爲所有 Drawable 的最大大小。默認爲 false。

  • variablePadding:設置 Drawable 的 padding 是否隨着狀態的改變而改變,如果設置爲 true,則會隨着改變。默認爲 false, 固定 padding 爲所有 Drawable 的最大 padding。

LevelListDrawable

LevelListDrawable 也是將一組 Drawable 集合在一起,並且每個 Drawable 都可以設置 level 的範圍,然後當我們在代碼中設置 level 時,就會切換到對應 level 的 Drawable。舉個實際的場景就是電池電量,可以將電量對應 level 並且設置不同的 Drawable:


<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/electric_quantity_0"
        android:maxLevel="10"
        android:minLevel="0" />
    <item
        android:drawable="@drawable/electric_quantity_70"
        android:maxLevel="70"
        android:minLevel="11" />
    <item
        android:drawable="@drawable/electric_quantity_100"
        android:maxLevel="100"
        android:minLevel="71" />
</level-list>

代碼中修改 level:


val drawable = electricQuantity.background as LevelListDrawable
drawable.level = level

需要注意的是,level 這個屬性是在抽象類 Drawable 中,所以在其他的 Drawable 中也有可能利用該屬性做一些效果。另外,默認情況下, minLevel 爲 0,maxLevel 爲 10000。

TransitionDrawable

TransitionDrawable 是 LayerDrawable 的擴展類,在第一層和第二層 Drawable 的切換之間加了一層過渡的動畫效果。可以在 XML 文件中使用 <transition>標籤定義:


<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/electric_quantity_0" />
    <item android:drawable="@drawable/electric_quantity_100" />
</transition>

通過下面的代碼進行轉換:


val drawable = imageView.drawable as TransitionDrawable
drawable.startTransition(100) // 開始轉換,從第一層轉換到第二層

還有其他的方法:


drawable.reverseTransition(100) // 翻轉轉換
drawable.resetTransition() // 僅顯示第一層

InsetDrawable

InsetDrawable 可以以指定的間距將另外的 Drawable 內嵌進來,當我們希望 View 的背景比自己實際的區域小的時候就可以用到它。可以在 XML 文件中使用 <inset>標籤定義:


<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:inset="10dp">

    <shape android:shape="rectangle">
        <solid android:color="@color/colorAccent" />
    </shape>
</inset>

除了 inset 屬性直接設置四周的間距大小之外,還有單獨設置不同方向間距的屬性,就不贅述了。

ScaleDrawable

ScaleDrawable 可以將內部定義的 Drawable 進行縮放,根據自身的 scaleWidth 和 scaleHeight 屬性定義的縮放比例還有 level 的級別進行縮放,level 必須設置爲大於 0,否則不會顯示。可以在 XML 文件中使用 <scale>標籤定義:


<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/icon"
    android:scaleWidth="50%"
    android:scaleHeight="50%"
    android:scaleGravity="center" />

在上面的示例中,將內部的 Drawable 縮小了 50%,並且居中。

別忘了設置 level,level 越大,內部的 Drawable 看起來就越大:

(scaleView.background as ScaleDrawable).level = 1

ClipDrawable

ClipDrawable 可以根據 level 對內部的 Drawable 進行裁剪,可以用來實現進度條等效果。可以在 XML 文件中使用 <clip>標籤定義:


<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:clipOrientation="horizontal"
    android:drawable="@drawable/icon"
    android:gravity="left" />

上面的示例代碼中定義了一個從左到右展開的 Drawable。別忘了在代碼中設置 level,最小爲 0,表示不顯示,最大爲 10000,完全顯示。

  • clipOrientation:設置裁剪的方向,可設置水平 horizontal 或垂直 vertical 方向。

  • gravity:該屬性根據 clipOrientation 的不同而不同,例如上面的例子中,設置爲水平方向爲 left,就是從左到右進行展開,如果設置爲 right 就是從右到左進行展開。

VectorDrawable

VectorDrawable 是一種矢量圖形,和 BitmapDrawable 一樣也是表示一張圖片,但是原理卻大不相同。

  • 普通的 Bitmap 位圖:由一個個像素組成,每個像素點都是一個很小的正方形,被分配了一種顏色。優點是表達能力強,細節可以很精細,缺點是縮放會失真。像 PNG、JPG、GIF 格式的圖片都是位圖圖像。

  • Vector 矢量圖:由直線、曲線和形狀組等信息組成。優點是可以進行無損縮放,並且佔用大小一般情況下都比位圖圖像小,缺點是不適合生成逼真的圖像,因爲逼真的圖像會有很多很多的細節,如果硬要生成,耗時的時間是一個問題,矢量圖的大小和複雜度也是一個問題。SVG 格式的圖片就是矢量圖像。

在 Android 中使用 VectorDrawable 意味着我們不用再爲不同的設備準備不同分辨率的圖片,這可以減少 APK 包的大小,節省開發時間。並且使用 AnimatedVectorDrawable 還可以利用 VectorDrawable 做一些動畫效果,這會在 AnimatedVectorDrawable 當中介紹到。

另外爲了優化重繪性能,將爲每個 VectorDrawable 創建一個位圖緩存。因此,引用相同的 VectorDrawable 意味着共享相同的位圖緩存。如果這些引用在相同的大小上不一致,則每次更改大小時都會重新創建並重新繪製位圖。換句話說,如果將 VectorDrawable 用於不同的大小,則創建多個 VectorDrawable(每個大小對應一個)的效率更高。

要在 Android 中使用 VectorDrawable 其實很簡單,因爲 AndroidStudio 已經給我們提供了創建的工具,直接右鍵 New -> Vector Asset 即可:

在這裏插入圖片描述

可以選擇直接從素材庫中創建對應的 VectorDrawable 或者從本地的 SVG、PSD 文件中導入,注意導入時如果有文件中有不支持的屬性會報錯,需要修改後再次導入。另外,還可以在創建 VectorDrawable 之前修改大小、顏色和透明度。點擊下一步選擇好放置的目錄,完成後創建的 VectorDrawable 文件如下:

<vector android:height="24dp" android:tint="#33B5E5"
    android:viewportHeight="24.0" android:viewportWidth="24.0"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

VectorDrawable 與之前介紹的 Drawable 有一點不同的是,創建時通過工具創建而不是手動創建,因爲 VectorDrawable 和其他 Drawable 相比都比較複雜,尤其是核心 pathData 屬性指定的數據,感興趣的可以自己研究下其中的意思。

使用時可以直接引用:

<androidx.appcompat.widget.AppCompatImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/ic_arrow_back_black_24dp" />

雖然 Android 的 android.graphics.drawable 包下面已經有了 VectorDrawable,但是我還是強烈推薦使用 AndroidX 的 VectorDrawableCompat 實現,因爲更加靈活,更新也會更頻繁,能得到最新的 bug 修復和向後兼容性,添加依賴:

implementation "androidx.vectordrawable:vectordrawable:1.1.0"

在佈局中應該使用帶 compat 的屬性,例如圖片應該用 srcCompat 屬性而不是 src。Button 應該用 buttonCompat 而不是 Button。如果沒有對應的 compat 屬性可以設置,那麼可以通過代碼進行設置:

AppCompatResources.getDrawable(context, R.drawable.ic_arrow_back_black_24dp)

Android 會在 API 21 到 API 23 之間使用 AndriodX 中的 VectorDrawableCompat 實現,從 API 24 開始,由於 Android 將 VectorDrawable 進行了重新實現,因此直接用的是 VectorDrawable。API 21 以下的兼容應該不用再說了吧,現在不會還有應用兼容 5.0 以下吧?

AnimatedVectorDrawable

AnimatedVectorDrawable 相當於一個“粘合劑”,將 VectorDrawable 和動畫結合在了一起,使 VectorDrawable 中的屬性可以動態變化,從而實現一些動畫效果。和 VectorDrawable 一樣,推薦引入 AndroidX 中的版本:

implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"

AnimatedVectorDrawable 使用也比較簡單,首先定義一個要做動畫的目標 VectorDrawable:


<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="64dp"
    android:width="64dp"
    android:viewportHeight="600"
    android:viewportWidth="600" >
    <group
        android:name="rotationGroup"
        android:pivotX="300.0"
        android:pivotY="300.0"
        android:rotation="45.0" >
        <path
            android:name="v"
            android:fillColor="#000000"
            android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
    </group>
</vector>

注意要給要做動畫的部分加上 name,以便讓動畫文件知道要對哪部分做動畫。

創建動畫文件 rotation.xml 和 path_morph.xml:


<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="6000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360" />

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="3000"
        android:propertyName="pathData"
        android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
        android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
        android:valueType="pathType"/>
</set>

上面的文件中分別讓 VectorDrawable 中的 group 進行旋轉,以及對 group 中的路徑做動畫,路徑動畫可以讓圖形平滑過渡到另一種圖形,是 Vector 動畫的一大亮點。

最後創建主角 AnimatedVectorDrawable 將 VectorDrawable 和動畫組合在一起:


<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/anim_vector_drawable" >
    <target
        android:name="rotationGroup"
        android:animation="@animator/rotation" />
    <target
        android:name="v"
        android:animation="@animator/path_morph" />
</animated-vector>

除此之外,還可以通過 AAPT 將多個 XML 文件結合在一起,例如:


<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <target android:name="rotationGroup">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="6000"
                android:propertyName="rotation"
                android:valueFrom="0"
                android:valueTo="360" />
        </aapt:attr>
    </target>

    <target android:name="v">
        <aapt:attr name="android:animation">
            <set>
                <objectAnimator
                    android:duration="3000"
                    android:propertyName="pathData"
                    android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
                    android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
                    android:valueType="pathType" />
            </set>
        </aapt:attr>
    </target>

    <aapt:attr name="android:drawable">
        <vector
            android:width="64dp"
            android:height="64dp"
            android:viewportWidth="600"
            android:viewportHeight="600">
            <group
                android:name="rotationGroup"
                android:pivotX="300.0"
                android:pivotY="300.0"
                android:rotation="45.0">
                <path
                    android:name="v"
                    android:fillColor="#000000"
                    android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
            </group>
        </vector>
    </aapt:attr>
</animated-vector>

單個 XML 實際效果和上面的一致。

在代碼中控制動畫的播放和停止:

val drawable = imageView.drawable as AnimatedVectorDrawable
drawable.start()
drawable.stop()

API 23 開始又增加了一個新的 reset() 方法,可以將 AnimatedVectorDrawable 恢復到初始狀態。

總的來說,通過 AnimatedVectorDrawable 進行動畫,可以只對圖像的一部分進行動畫,非常靈活,並且可以對路徑做動畫,從而實現一些很酷的效果。另外,從 API 25 開始,AnimatedVectorDrawable 在 RenderThread 上運行,這意味着即使 UI 線程上的工作量很大,AnimatedVectorDrawable 中的動畫也可以保持平滑。

自定義 Drawable

對於一些其他的效果,我們可以直接繼承自 Drawable,然後實現自己想要的效果,例如下面的示例中實現了一個豎線的 Drawable:

class CustomDrawable : Drawable() {
  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

  init {
    paint.color = Color.RED
  }

  override fun draw(canvas: Canvas) {
    val r = bounds
    val width = r.width()
    val height = r.height()
    paint.strokeWidth = width.toFloat()
    canvas.drawLine(0f, 0f, 0f, height.toFloat(), paint)
  }

  override fun setAlpha(alpha: Int) {
    paint.alpha = alpha
    invalidateSelf()
  }

  override fun getOpacity(): Int {
    return PixelFormat.TRANSLUCENT
  }

  override fun setColorFilter(colorFilter: ColorFilter?) {
    paint.colorFilter = colorFilter
    invalidateSelf()
  }
}

在設置時,直接通過代碼設置即可。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章