可绘制对象概述

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

当您需要在应用中显示静态图像时,可以使用 Drawable 类及其子类来绘制形状和图像。一个 Drawable 是对 可绘制内容 的通用抽象。各种子类有助于处理特定的图像场景,您可以扩展它们来定义具有独特行为的自己的可绘制对象。

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

  • 膨胀保存在项目中的图像资源(位图文件)。
  • 膨胀定义可绘制对象属性的 XML 资源。

注意: 您可能更愿意使用矢量可绘制对象,它使用一组点、线和曲线以及相关的颜色信息来定义图像。这使矢量可绘制对象可以针对不同大小进行缩放,而不会损失质量。有关详细信息,请参阅 矢量可绘制对象概述

从资源图像创建可绘制对象

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

要使用图像资源,请将您的文件添加到项目的 res/drawable/ 目录中。一旦在您的项目中,您就可以从代码或 XML 布局中引用图像资源。无论哪种方式,它都使用资源 ID 进行引用,资源 ID 是不带文件类型扩展名的文件名。例如,将 my_image.png 引用为 my_image

注意: 放在 res/drawable/ 目录中的图像资源可能会在构建过程中由 aapt 工具使用无损图像压缩自动优化。例如,一个不需要超过 256 种颜色的真彩色 PNG 可能会被转换为具有调色板的 8 位 PNG。这会导致图像质量相同,但所需内存更少。因此,放置在此目录中的图像二进制文件可能会在构建时发生变化。如果您打算将图像读取为比特流以将其转换为位图,请将图像放在 res/raw/ 文件夹中,因为 aapt 工具不会修改它们。

以下代码片段演示了如何构建一个使用从可绘制资源创建的图像的 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 代码段显示了如何在 XML 布局中向 ImageView 添加可绘制资源

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

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

注意: 当使用图像资源作为可绘制对象源时,请确保图像的大小适合各种像素密度。如果图像不正确,它们将被放大以适合,这会导致可绘制对象出现伪影。有关详细信息,请阅读 支持不同的像素密度

从 XML 资源创建可绘制对象

如果您想创建的 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 膨胀的可绘制对象类都使用特定的 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 属性的更多信息,请参阅上面列出的类。

形状可绘制对象

当您想要动态绘制二维图形时,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 类。例如,您可以以编程方式将其添加到应用中的活动中,如以下示例所示

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"
        />

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

您也可以使用 XML 资源定义基本的可绘制形状。有关详细信息,请参阅 形状可绘制对象 可绘制资源类型 中。

NinePatch 可绘制对象

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

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

您还可以通过在右侧和底部绘制一条线来定义图像的可选可绘制区域(实际上是填充线)。如果 View 对象将 NinePatch 图像设置为其背景,然后指定视图的文本,则它会拉伸自身,以便所有文本仅占据由右侧和底部线(如果存在)指定的区域。如果没有填充线,Android 将使用左侧和顶部线来定义此可绘制区域。

为了清楚地说明线条之间的区别,左侧和顶部线定义了允许复制图像的哪些像素以拉伸图像。底部和右侧线定义了视图内容允许占据的图像内的相对区域。

图 1 显示了用于定义按钮的 NinePatch 图像示例

Image of stretchable area
and padding box

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

此 NinePatch 图像使用左侧和顶部线定义一个可拉伸区域,并使用底部和右侧线定义可绘制区域。在上面的图像中,点状灰色线标识了为了拉伸图像而复制的图像区域。下面图像中的粉色矩形标识了视图内容允许占据的区域。如果内容不适合此区域,则图像将拉伸以使它们适合。

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

以下示例布局 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 类(或其任何子类)来实现。

要实现的最重要的方法是 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;
    }
}

然后,您可以将可绘制对象添加到您想要的位置,例如添加到 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 中定义自定义可绘制对象的实例

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

向可绘制对象添加色调

在 Android 5.0(API 级别 21)及更高版本中,您可以对定义为 alpha 遮罩的位图和九宫格图像进行色调处理。您可以使用颜色资源或解析为颜色资源的主题属性(例如,?android:attr/colorPrimary)对其进行色调处理。通常,您只创建这些资产一次,并自动对其进行颜色处理以匹配您的主题。

您可以使用 setTint() 方法将色调应用于 BitmapDrawableNinePatchDrawableVectorDrawable 对象。您还可以在布局中使用 android:tintandroid:tintMode 属性来设置色调颜色和模式。

从图像中提取突出颜色

Android 支持库包含 Palette 类,它允许您从图像中提取突出颜色。您可以将可绘制对象加载为 Bitmap 并将其传递给 Palette 以访问其颜色。有关更多信息,请阅读 使用 Palette API 选择颜色