Drawable 概览

尝试 Compose 方式
Jetpack Compose 是 Android 推荐的界面工具包。了解如何在 Compose 中显示图形。

当您需要在应用中显示静态图片时,可以使用 Drawable 类及其子类来绘制形状和图片。Drawable 是一个通用抽象,代表“可以绘制的事物”。各种子类有助于应对特定的图片场景,您也可以通过扩展它们来定义具有独特行为的自定义 Drawable 对象。

除了使用类构造函数外,还有两种定义和实例化 Drawable 的方法:

  • 填充保存在项目中的图片资源(位图文件)。
  • 填充定义 Drawable 属性的 XML 资源。

注意:您可能更倾向于使用矢量 Drawable,它通过一组点、线和曲线以及关联的颜色信息来定义图片。这使得矢量 Drawable 可以在不损失质量的情况下缩放至不同尺寸。如需了解更多信息,请参阅 矢量 Drawable 概览

从资源图片创建 Drawable

您可以通过引用项目资源中的图片文件将图形添加到应用中。支持的文件类型包括 PNG(首选)、JPG(可接受)和 GIF(不推荐)。应用图标、徽标以及游戏中使用的其他图形非常适合采用这种方式。

要使用图片资源,请将文件添加到项目的 res/drawable/ 目录中。一旦放入项目中,您就可以从代码或 XML 布局中引用该图片资源。无论哪种方式,引用时使用的都是资源 ID,即不带文件扩展名的文件名。例如,引用 my_image.png 时应使用 my_image

注意:放置在 res/drawable/ 目录中的图片资源可能会在构建过程中通过 aapt 工具自动进行无损压缩优化。例如,不需要超过 256 色的真彩色 PNG 可能会被转换为带颜色调色板的 8 位 PNG。这会产生质量相同但占用内存更少的图片。因此,放置在此目录中的图片二进制文件在构建时可能会发生变化。如果您计划将图片读取为位流以将其转换为位图,请改为将图片放入 res/raw/ 文件夹中,aapt 工具不会修改那里的文件。

以下代码段演示了如何构建一个使用从 Drawable 资源创建的图片的 ImageView,并将其添加到布局中

Kotlin

private lateinit var constraintLayout: ConstraintLayout

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Instantiate an ImageView and define its properties
    val i = ImageView(this).apply {
        setImageResource(R.drawable.my_image)
        contentDescription = resources.getString(R.string.my_image_desc)

        // set the ImageView bounds to match the Drawable's dimensions
        adjustViewBounds = true
        layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT)
    }

    // Create a ConstraintLayout in which to add the ImageView
    constraintLayout = ConstraintLayout(this).apply {

        // Add the ImageView to the layout.
        addView(i)
    }

    // Set the layout as the content view.
    setContentView(constraintLayout)
}

Java

ConstraintLayout constraintLayout;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // Create a ConstraintLayout in which to add the ImageView
  constraintLayout = new ConstraintLayout(this);

  // Instantiate an ImageView and define its properties
  ImageView i = new ImageView(this);
  i.setImageResource(R.drawable.my_image);
  i.setContentDescription(getResources().getString(R.string.my_image_desc));

  // set the ImageView bounds to match the Drawable's dimensions
  i.setAdjustViewBounds(true);
  i.setLayoutParams(new ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT));

  // Add the ImageView to the layout and set the layout as the content view.
  constraintLayout.addView(i);
  setContentView(constraintLayout);
}

在其他情况下,您可能希望将图片资源作为 Drawable 对象处理,如下例所示

Kotlin

val myImage: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)

Java

Resources res = context.getResources();
Drawable myImage = ResourcesCompat.getDrawable(res, R.drawable.my_image, null);

警告:无论您为同一个项目资源实例化多少个不同的对象,每个唯一的资源都只能维持一个状态。例如,如果您从同一个图片资源实例化两个 Drawable 对象,并更改其中一个对象的属性(如 alpha 值),那么这也会影响另一个对象。处理图片资源的多个实例时,您应该执行补间动画,而不是直接转换 Drawable 对象。

下面的 XML 片段展示了如何将 Drawable 资源添加到 XML 布局中的 ImageView

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/my_image"
        android:contentDescription="@string/my_image_desc" />

有关使用项目资源的更多信息,请参阅 资源和资产

注意:将图片资源用作 Drawable 的源时,请确保图片大小适合各种像素密度。如果图片大小不正确,系统会将其缩放以进行适配,这可能会导致 Drawable 中出现伪影。如需了解更多信息,请阅读支持不同的像素密度

从 XML 资源创建 Drawable

如果您想创建一个不依赖于代码定义变量或用户交互的 Drawable 对象,那么在 XML 中定义该 Drawable 是一个不错的选择。即使您预计 Drawable 的属性会在用户与应用交互时发生变化,您也应该考虑在 XML 中定义该对象,因为您可以在对象实例化后修改属性。

在 XML 中定义 Drawable 后,请将文件保存在项目的 res/drawable/ 目录中。以下示例展示了定义 TransitionDrawable 资源的 XML,它继承自 Drawable

<!-- res/drawable/expand_collapse.xml -->
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/image_expand"/>
    <item android:drawable="@drawable/image_collapse"/>
</transition>

然后,通过调用 Resources#getDrawable() 并传入您的 XML 文件资源 ID 来检索和实例化该对象。任何支持 inflate() 方法的 Drawable 子类都可以在 XML 中定义并由您的应用实例化。

每个支持 XML 填充的 Drawable 类都会利用特定的 XML 属性来定义对象属性。以下代码实例化了 TransitionDrawable 并将其设置为 ImageView 对象的内容

Kotlin

val transition= ResourcesCompat.getDrawable(
        context.resources,
        R.drawable.expand_collapse,
        null
) as TransitionDrawable

val image: ImageView = findViewById(R.id.toggle_image)
image.setImageDrawable(transition)

// Description of the initial state that the drawable represents.
image.contentDescription = resources.getString(R.string.collapsed)

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000)

// After the transition is complete, change the image's content description
// to reflect the new state.

Java

Resources res = context.getResources();
TransitionDrawable transition =
    (TransitionDrawable) ResourcesCompat.getDrawable(res, R.drawable.expand_collapse, null);

ImageView image = (ImageView) findViewById(R.id.toggle_image);
image.setImageDrawable(transition);

// Description of the initial state that the drawable represents.
image.setContentDescription(getResources().getString(R.string.collapsed));

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000);

// After the transition is complete, change the image's content description
// to reflect the new state.

有关所支持的 XML 属性的更多信息,请参考上述类。

形状 Drawable

当您想动态绘制二维图形时,ShapeDrawable 对象是一个不错的选择。您可以在 ShapeDrawable 对象上以编程方式绘制原始形状,并应用应用所需的样式。

ShapeDrawableDrawable 的子类。因此,您可以在任何需要 Drawable 的地方使用 ShapeDrawable。例如,您可以通过将 ShapeDrawable 对象传递给视图的 setBackgroundDrawable() 方法来设置视图的背景。您还可以将形状绘制为自己的自定义视图,并将其添加到应用布局中。

由于 ShapeDrawable 拥有自己的 draw() 方法,您可以创建 View 的子类,在 onDraw() 事件期间绘制 ShapeDrawable 对象,如下面的代码示例所示

Kotlin

class CustomDrawableView(context: Context) : View(context) {
    private val drawable: ShapeDrawable = run {
        val x = 10
        val y = 10
        val width = 300
        val height = 50
        contentDescription = context.resources.getString(R.string.my_view_desc)

        ShapeDrawable(OvalShape()).apply {
            // If the color isn't set, the shape uses black as the default.
            paint.color = 0xff74AC23.toInt()
            // If the bounds aren't set, the shape can't be drawn.
            setBounds(x, y, x + width, y + height)
        }
    }

    override fun onDraw(canvas: Canvas) {
        drawable.draw(canvas)
    }
}

Java

public class CustomDrawableView extends View {
  private ShapeDrawable drawable;

  public CustomDrawableView(Context context) {
    super(context);

    int x = 10;
    int y = 10;
    int width = 300;
    int height = 50;
    setContentDescription(context.getResources().getString(
            R.string.my_view_desc));

    drawable = new ShapeDrawable(new OvalShape());
    // If the color isn't set, the shape uses black as the default.
    drawable.getPaint().setColor(0xff74AC23);
    // If the bounds aren't set, the shape can't be drawn.
    drawable.setBounds(x, y, x + width, y + height);
  }

  protected void onDraw(Canvas canvas) {
    drawable.draw(canvas);
  }
}

您可以像使用任何其他自定义视图一样使用上述代码示例中的 CustomDrawableView 类。例如,您可以以编程方式将其添加到应用中的 Activity,如下例所示

Kotlin

private lateinit var customDrawableView: CustomDrawableView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    customDrawableView = CustomDrawableView(this)

    setContentView(customDrawableView)
}

Java

CustomDrawableView customDrawableView;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  customDrawableView = new CustomDrawableView(this);

  setContentView(customDrawableView);
}

如果您想在 XML 布局中使用该自定义视图,那么 CustomDrawableView 类必须重写 View(Context, AttributeSet) 构造函数,该构造函数会在类从 XML 填充时被调用。以下示例展示了如何在 XML 布局中声明 CustomDrawableView

<com.example.shapedrawable.CustomDrawableView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />

ShapeDrawable 类(以及 android.graphics.drawable 包中的许多其他 Drawable 类型)允许您使用公共方法定义对象的各种属性。您可能想要调整的一些示例属性包括 alpha 透明度、颜色过滤器、抖动、不透明度和颜色。

您还可以使用 XML 资源定义原始 Drawable 形状。如需了解更多信息,请参阅 Drawable 资源类型中的形状 Drawable

NinePatch Drawable

NinePatchDrawable 图形是一种可拉伸的位图图片,可用作视图的背景。Android 会自动调整图形大小以适应视图内容。NinePatch 图片的一个使用示例是 Android 标准按钮使用的背景——按钮必须拉伸以适应不同长度的字符串。NinePatch 图形是一种标准的 PNG 图片,包含一个额外的 1 像素边框。它必须以 9.png 扩展名保存在项目的 res/drawable/ 目录中。

使用边框定义图片的可拉伸区域和静态区域。您可以通过在边框的左侧和顶部绘制一条(或多条)1 像素宽的黑线来指定可拉伸部分(其他边框像素应为完全透明或白色)。您可以根据需要设置任意数量的可拉伸部分。可拉伸部分的相对大小保持不变,因此最大的部分始终保持最大。

您还可以通过在右侧和底部绘制线条来定义可选的 Drawable 部分(实际上是内边距线)。如果 View 对象将 NinePatch 图形设置为其背景并指定了视图的文本,它会拉伸自身,以便所有文本仅占据右侧和底部线条所指定的区域(如果包含线条)。如果不包含内边距线,Android 会使用左侧和顶部线条来定义此 Drawable 区域。

为明确线条之间的区别:左侧和顶部线条定义了图片的哪些像素可以被复制以拉伸图片;底部和右侧线条定义了图片内视图内容允许占据的相对区域。

图 1 展示了用于定义按钮的 NinePatch 图形示例

Image of stretchable area
and padding box

图 1:定义按钮的 NinePatch 图形示例

此 NinePatch 图形使用左侧和顶部线条定义了一个可拉伸区域,并使用底部和右侧线条定义了 Drawable 区域。在顶部的图片中,虚线灰色线条标识了为了拉伸图片而需要复制的区域。底部图片中的粉色矩形标识了视图内容被允许占据的区域。如果内容不适合该区域,图片会被拉伸以容纳它们。

Draw 9-patch 工具提供了一种极其方便的方式,可以使用 WYSIWYG 图形编辑器创建您的 NinePatch 图片。如果您定义的用于可拉伸区域的范围有因像素复制而产生绘图伪影的风险,它甚至会发出警告。

以下示例布局 XML 演示了如何将 NinePatch 图形添加到几个按钮中。NinePatch 图片保存在 res/drawable/my_button_background.9.png

<Button android:id="@+id/tiny"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerInParent="true"
        android:text="Tiny"
        android:textSize="8sp"
        android:background="@drawable/my_button_background"/>

<Button android:id="@+id/big"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"
        android:text="Biiiiiiig text!"
        android:textSize="30sp"
        android:background="@drawable/my_button_background"/>

请注意,layout_widthlayout_height 属性设置为 wrap_content,以便按钮能刚好适配文本。

图 2 展示了上述 XML 和 NinePatch 图片呈现的两个按钮。请注意按钮的宽度和高度如何随文本变化,背景图片也随之拉伸以进行适配。

Image of tiny and
normal-sized buttons

图 2:使用 XML 资源和 NinePatch 图形呈现的按钮

自定义 Drawable

当您想要创建一些自定义绘图时,可以通过扩展 Drawable 类(或其任何子类)来实现。

最重要的方法是实现 draw(Canvas),因为它提供了您必须用来提供绘图指令的 Canvas 对象。

以下代码显示了 Drawable 的一个简单子类,它绘制了一个圆

Kotlin

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
}

Java

public class MyDrawable extends Drawable {
    private final Paint redPaint;

    public MyDrawable() {
        // Set up color and text size
        redPaint = new Paint();
        redPaint.setARGB(255, 255, 0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        // Get the drawable's bounds
        int width = getBounds().width();
        int height = getBounds().height();
        float radius = Math.min(width, height) / 2;

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

    @Override
    public void setAlpha(int alpha) {
        // This method is required
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        // This method is required
    }

    @Override
    public int getOpacity() {
        // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
        return PixelFormat.OPAQUE;
    }
}

然后,您可以将您的 Drawable 添加到任何您想要的地方,例如添加到 ImageView 中,如下所示

Kotlin

val myDrawing = MyDrawable()
val image: ImageView = findViewById(R.id.imageView)
image.setImageDrawable(myDrawing)
image.contentDescription = resources.getString(R.string.my_image_desc)

Java

MyDrawable mydrawing = new MyDrawable();
ImageView image = findViewById(R.id.imageView);
image.setImageDrawable(mydrawing);
image.setContentDescription(getResources().getString(R.string.my_image_desc));

在 Android 7.0 (API 级别 24) 及更高版本上,您还可以通过以下方式在 XML 中定义自定义 Drawable 的实例:

  • 使用完全限定类名作为 XML 元素名称。对于此方法,自定义 Drawable 类必须是公共顶级类。
    <com.myapp.MyDrawable xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#ffff0000" />
  • 使用 drawable 作为 XML 标签名称,并从 class 属性指定完全限定类名。此方法可同时用于公共顶级类和公共静态内部类。
    <drawable xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.myapp.MyTopLevelClass$MyDrawable"
        android:color="#ffff0000" />