【UI篇5】Drawable引发的思考

1,153 阅读9分钟

只要想改变,那今天就是最来得及的一天。

写在前面的话
近来涉及到一些 UI 的需求,越来越多的用到各种 Drawable,于是打算停下来思考一下:为什么会有这么多种 Drawable 呢?每一种当时定义的初衷是什么? 为了弄清楚这个答案,便开始回忆以往使用 Drawable 的场景。

Drawable 是可绘制对象的常规抽象。在设置 View 的背景时,我们会用到各种各样的 Drawable,那么常用的Drawable有哪几种呢,Drawable是如何获取的,如何存储的,带着这些引发的思考,下面我们进行一下说明。

1 分类

关于分类,可参考如图所示继承Drawable的子类

import android.graphics.drawable.Drawable

image.png

在上图中,我们可以看到BitmapDrawableGradientDrawableLayerDrawableShapeDrawable等我们常用到的一些Drawable,以下选择一些常用的做下介绍:

1.1 Shape drawables [形状可绘制对象]

ShapeDrawable 使用场景

  1. 动态绘制二维图形作为 View 的背景
  2. 绘制形状作为其自定义View

XML 中定义语法

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape=["rectangle" | "oval" | "line" | "ring"] >
    <corners
        android:radius="integer"
        android:topLeftRadius="integer"
        android:topRightRadius="integer"
        android:bottomLeftRadius="integer"
        android:bottomRightRadius="integer" />
    <gradient
        android:angle="integer"
        android:centerX="float"
        android:centerY="float"
        android:centerColor="integer"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type=["linear" | "radial" | "sweep"]
        android:useLevel=["true" | "false"] />
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <size
        android:width="integer"
        android:height="integer" />
    <solid
        android:color="color" />
    <stroke
        android:width="integer"
        android:color="color"
        android:dashWidth="integer"
        android:dashGap="integer" />
</shape>

GradientDrawable

带有按钮、背景等颜色渐变的 Drawable,它可以在带有元素的 XML 文件中定义。

A Drawable with a color gradient for buttons, backgrounds, etc. It can be defined in an XML file with the element. For more information, see the guide to Drawable Resources.

生成渐变图片

/**
 * Create a new gradient drawable given an orientation and an array
 * of colors for the gradient.
 */
public GradientDrawable(Orientation orientation, @ColorInt int[] colors) {
    this(new GradientState(orientation, colors), null);
}

//生成渐变图片,设置成蒙版    
val colors = intArrayOf(Color.parseColor("#00000000", "#00000000"))
view.background = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, colors)    

1.2 NinePatch drawables [NinePatch 可绘制对象]

使用场景

  1. View 的内容大小不确定,且可拉伸

定义
通过 Draw 9-patch工具生成使用9.png扩展名保存

使用Android工具制作点九图

  1. 创建图片:在drawable-xxhdpi文件夹下,选择一张图,右键这张图片,选择create 9-Patch file...

image.png

  1. 显示界面:双击点九图,可以看到左边是操作界面,右边是实时填充内容的显示界面;

image.png 3. 描边:按下鼠标左键放在图片边界点击就能描边,若要去掉黑边,可以按下shift键,然后点击鼠标左键,请记住:左上控制拉伸位置,右下控制内容显示位置

描边时一定要注意,选中边距时,有如图所示的箭头线,可以清楚地看到边距,此时描边比较容易,描边描边,一定是要找到边,才好描;

image.png

1.3 Bitmap Drawables [Bitmap 可绘制对象]

A bitmap graphic file (.png.webp.jpg, or .gif). Creates a BitmapDrawable

语法

<?xml version="1.0" encoding="utf-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@[package:]drawable/drawable_resource"
    android:antialias=["true" | "false"]
    android:dither=["true" | "false"]
    android:filter=["true" | "false"]
    android:gravity=["top" | "bottom" | "left" | "right" | "center_vertical" |
                      "fill_vertical" | "center_horizontal" | "fill_horizontal" |
                      "center" | "fill" | "clip_vertical" | "clip_horizontal"]
    android:mipMap=["true" | "false"]
    android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"] />

1.4 Layer list [图层列表]

A Drawable that manages an array of other Drawables. These are drawn in array order, so the element with the largest index is be drawn on top. Creates a LayerDrawable

语法

<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:drawable="@[package:]drawable/drawable_resource"
        android:id="@[+][package:]id/resource_name"
        android:top="dimension"
        android:right="dimension"
        android:bottom="dimension"
        android:left="dimension" />
</layer-list>

1.5 State list [状态列表]

An XML file that references different bitmap graphics for different states (for example, to use a different image when a button is pressed). Creates a StateListDrawable.

StateListDrawable 使用场景

  1. 点击、禁用等多种状态可选

XML 定义语法

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    android:constantSize=["true" | "false"]
    android:dither=["true" | "false"]
    android:variablePadding=["true" | "false"] >
    <item
        android:drawable="@[package:]drawable/drawable_resource"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_hovered=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_activated=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

1.6 Ripple Drawables [波纹可绘制对象]

Drawable that shows a ripple effect in response to state changes. The anchoring position of the ripple for a given state may be specified by calling setHotspot(float, float) with the corresponding state attribute identifier.

使用场景

  1. 水波纹/涟漪效果

XML 语法定义

 <!-- A red ripple masked against an opaque rectangle. -->
 <ripple android:color="#ffff0000">
   <item android:id="@android:id/mask"
         android:drawable="@android:color/white" />
 </ripple>

1.7 Vector Drawables [矢量可绘制对象]

VectorDrawable 使用场景

  1. 适配多分辨率

将SVG 文件转换成矢量可绘制格式,具体可参考添加多密度矢量图形

1.8 Rotate drawable [旋转可绘制对象]

A Drawable that can rotate another Drawable based on the current level value. The start and end angles of rotation can be controlled to map any circular arc to the level values range.

It can be defined in an XML file with the <rotate> element. For more information, see the guide to Animation Resources.

语法

    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:shareInterpolator="false">
        <scale
            android:interpolator="@android:anim/accelerate_decelerate_interpolator"
            android:fromXScale="1.0"
            android:toXScale="1.4"
            android:fromYScale="1.0"
            android:toYScale="0.6"
            android:pivotX="50%"
            android:pivotY="50%"
            android:fillAfter="false"
            android:duration="700" />
        <set
            android:interpolator="@android:anim/accelerate_interpolator"
            android:startOffset="700">
            <scale
                android:fromXScale="1.4"
                android:toXScale="0.0"
                android:fromYScale="0.6"
                android:toYScale="0.0"
                android:pivotX="50%"
                android:pivotY="50%"
                android:duration="400" />
            //参考rotate 部分
            <rotate
                android:fromDegrees="0"
                android:toDegrees="-45"
                android:toYScale="0.0"
                android:pivotX="50%"
                android:pivotY="50%"
                android:duration="400" />
        </set>
    </set>
    

1.9 Inset drawable [插入可绘制对象]

在 XML 文件中定义,以指定距离插入其他可绘制对象的可绘制对象。当视图需要小于视图实际边界的背景时,此类可绘制对象很有用。

语法

<?xml version="1.0" encoding="utf-8"?>
<inset
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_resource"
    android:insetTop="dimension"
    android:insetRight="dimension"
    android:insetBottom="dimension"
    android:insetLeft="dimension" />

1.10 Color Drawables [颜色可绘制对象]

<resources>
    <drawable name="view_pager_item_bg">#00ffffff</drawable>
    <drawable name="pop_up_transparent_bg">#66000000</drawable>
    <drawable name="transparent_bg">#00000000</drawable>
</resources>

1.11 自定义Drawable

  1. 实现draw(Canvas)方法
    class MyDrawable : Drawable() {
        private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }

        override fun draw(canvas: Canvas) {
            // Get the drawable's bounds
            val width: Int = bounds.width()
            val height: Int = bounds.height()
            val radius: Float = Math.min(width, height).toFloat() / 2f

            // Draw a red circle in the center
            canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
        }

        override fun setAlpha(alpha: Int) {
            // This method is required
        }

        override fun setColorFilter(colorFilter: ColorFilter?) {
            // This method is required
        }

        override fun getOpacity(): Int =
            // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
            PixelFormat.OPAQUE
    }
    

有时我也在想,为什么会有这么多种Drawable,但仔细看看使用的场景和实现,便可知道Android 这样设计主要是为了满足开发者的不同需求,与其让开发者造轮子不规范导致各种问题,还不如满足开发者的需求造好轮子给大家使用。

一个具有渐变区域的Drawable,可以实现线性渐变、发散渐变和平铺渐变效果

当然你也可以造一些更好的轮子,自定义Drawable像自定义View一样有趣。

2 Drawable的其他使用

在使用Drawable之前,我们首先要明确当前是何种场景,使用哪种Drawable最优,除了上述一些通用的子类外,Android官方还提供了一些其他的功能,介绍如下:

2.1 向可绘制对象添加色调

您可以使用 setTint() 方法对 BitmapDrawableNinePatchDrawable 或 VectorDrawable 对象着色。您还可以使用 android:tint 和 android:tintMode 属性在布局中设置着色颜色和模式。

2.2 从图片中萃取突出颜色

Android 支持库包含 Palette 类,可让您从图片中萃取突出颜色。您可以将可绘制对象作为 Bitmap 进行加载,并将其传递到 Palette 以访问其颜色。如需了解详情,请参阅使用 Palette API 选择颜色

val builder = Palette.from(bitmap)
builder.maximumColorCount(16).clearFilters()
    .addFilter { rgb: Int, hslColor: FloatArray ->
    }
builder.setRegion(0, masktop, bitmap.width, bitmap.height).generate { -> paletter
    val lightVibrantColor = this.getDominantColor(Color.BLACK)
    Color.colorToHSV(lightVibrantColor, hsv)
    val vibrantColor = Color.HSVToColor(hsv)
}    
    

3 引发 Bitmap 的思考

在Android中,我们既会用到Drawable,也会用到Bitmap,那么两者是什么关系呢?

从两个类的路径来看,Drawable 与 Bitmap 似乎没有多大关系,但在Drawable中有如下介绍,Bitmap 是最简单的一种 Drawable。

image.png

根据两个类的定义,我们做个如下的对比:

对比BitmapDrawable
定义位图,drawable的一种可绘制对象
类名android.graphics.Bitmapandroid.graphics.drawable.Drawable
生成BitmapFactory.decodeResource(...)getResources.getDrawable(...)
宽、高getWidth
getHeight
getIntrinsicWidth
getIntrinsicHeight
转换BitmapDrawable(res, bitmap)Drawable通过Canvas画出来
Bitmap bitmap = Bitmap.createBitmap(w, h, config)
Canvas canvas = new Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
drawable.draw(canvas)
传输可传输不可传输

下面分析Resources.getDrawable获取图片资源的流程如下,根据流程可知,整个过程Drawable先是读取为Bitmap,然后再转为BitmapDrawable的

sequenceDiagram
Resources->>ResourcesImpl: loadDrawable
ResourcesImpl->>ResourcesImpl: loadDrawableForCookie
ResourcesImpl->>ImageDecoder: decodeImageDrawable
ImageDecoder->>ImageDecoder: decodeBitmap
ImageDecoder->>BitmapFactory: decodeBitmap
BitmapFactory-)Resources: decodeResourcesStream
//将png等drawable资源转为输入流
final InputStream is = mAssets.openNonAsset(
        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
final AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);


public static Drawable decodeDrawable(@NonNull Source src,
        @Nullable OnHeaderDecodedListener listener) throws IOException {
    Bitmap bitmap = decodeBitmap(src, listener);
    return new BitmapDrawable(src.getResources(), bitmap);
}

// 通过输入流读取的Bitmap,转为BitmapDrawable
public static Bitmap decodeBitmap(@NonNull Source src,
        @Nullable OnHeaderDecodedListener listener) throws IOException {
    TypedValue value = new TypedValue();
    value.density = src.getDensity();
    ImageDecoder decoder = src.createImageDecoder();
    if (listener != null) {
        listener.onHeaderDecoded(decoder, new ImageInfo(decoder), src);
    }
    return BitmapFactory.decodeResourceStream(src.getResources(), value,
            ((InputStreamSource) src).mInputStream, decoder.mOutPaddingRect, null);
}

经过上面的对比和流程跟踪,我们可以大致的认为

1、Drawable是对一些可绘制对象的统称,可以理解为是表象,是给用户展示的;
2、Drawable最终还是通过Bitmap来进行存储,来跟机器打交道的;

3.1 Bitmap裁剪适配广色域

Bitmap裁剪适配广色域需要在原图数据上进行操作,如果draw到ImageView上再裁剪保存则会失真。下面为常见的原图裁剪保存操作:

//设置颜色空间
val options = BitmapFactory.Options().apply {
    options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3)
}

val originBitmap = BitmapFactory.decodeFile(data.path, options)
val scaleBitmap = Bitmap.createScaleBitmap(originBitmap, currentWidth, currentHeight, false)
//按照屏幕位置裁剪Bitmap, x, y为要裁剪的bitmap中圆点的像素位置
val cropBitmap = Bitmap.createBitmap(scaleBitmap, x, y, width, height)

val outputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle()

3.2 Bitmap裁剪的方法

裁剪方法1
/**
 * Returns a bitmap from the specified subset of the source
 * bitmap. The new bitmap may be the same object as source, or a copy may
 * have been made. It is initialized with the same density and color space
 * as the original bitmap.
 *
 * @param source   The bitmap we are subsetting
 * @param x        The x coordinate of the first pixel in source
 * @param y        The y coordinate of the first pixel in source
 * @param width    The number of pixels in each row
 * @param height   The number of rows
 * @return A copy of a subset of the source bitmap or the source bitmap itself.
 * @throws IllegalArgumentException if the x, y, width, height values are
 *         outside of the dimensions of the source bitmap, or width is <= 0,
 *         or height is <= 0
 */
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height) {
    return createBitmap(source, x, y, width, height, null, false);
}
按比例缩放
Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);

4 阅读的思考

关于Drawable,还有很多要了解的东西,比如抗锯齿、抖动、滤波、纹理映射、平铺模式、渐变等等,这些是如何实现的?

Bitmap是如何存储的,绘制的,底层的实现逻辑又是如何的?

关于Drawable的思考才刚刚开始,由于时间的关系,来不及一一的刨根问底,待后续抽空再补上这有意思的一部分吧

val pfd: ParcelFileDescriptor? = context.contentResolver.openFileDescriptor(Uri.parse(data?.path ?: ""), "r")
val fd = pfd?.fileDescriptor
val fis = FileInputStream(fd)
val bitmap = BitmapFactory.decodeStream(fis, null, BitmapFactory.Options())