Tổng quan về tài nguyên có thể kéo

Thử cách sử dụng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách hiển thị đồ hoạ trong Compose.

Khi cần hiển thị hình ảnh tĩnh trong ứng dụng, bạn có thể sử dụng lớp Drawable và các lớp con của lớp này để vẽ hình dạng và hình ảnh. Drawable là một đối tượng trừu tượng chung cho nội dung có thể vẽ được. Các lớp con khác nhau sẽ hỗ trợ các trường hợp hình ảnh cụ thể. Bạn có thể mở rộng các lớp con này để xác định các đối tượng có thể vẽ của riêng mình hoạt động theo những cách khác nhau.

Có 2 cách để xác định và tạo thực thể cho Drawable ngoài việc sử dụng các hàm khởi tạo lớp:

  • Tăng cường tài nguyên hình ảnh (tệp bitmap) được lưu trong dự án của bạn.
  • Tăng cường tài nguyên XML xác định các thuộc tính có thể vẽ.

Lưu ý: Bạn nên sử dụng vectơ vẽ được để xác định hình ảnh bằng một tập hợp các điểm, đường kẻ và đường cong, cùng với thông tin màu liên quan. Điều này cho phép điều chỉnh các vectơ vẽ được theo tỷ lệ cho nhiều kích thước mà không làm giảm chất lượng. Để biết thêm thông tin, hãy xem bài viết Tổng quan về vectơ vẽ được.

Tạo đối tượng có thể vẽ từ hình ảnh tài nguyên

Bạn có thể thêm đồ hoạ vào ứng dụng bằng cách tham chiếu một tệp hình ảnh từ tài nguyên dự án. Các loại tệp được hỗ trợ là PNG (ưu tiên), JPG (có thể chấp nhận) và GIF (không nên chọn). Biểu tượng ứng dụng, biểu trưng và các hình ảnh đồ hoạ khác, chẳng hạn như nội dung dùng trong trò chơi, rất phù hợp với kỹ thuật này.

Để sử dụng tài nguyên hình ảnh, hãy thêm tệp của bạn vào thư mục res/drawable/ của dự án. Sau khi đã ở trong dự án, bạn có thể tham chiếu tài nguyên hình ảnh từ mã hoặc bố cục XML của mình. Dù bằng cách nào, thì hệ thống cũng tham chiếu đến việc sử dụng mã nhận dạng tài nguyên, là tên tệp mà không có đuôi loại tệp. Ví dụ: tham chiếu my_image.pngmy_image.

Lưu ý: Các tài nguyên hình ảnh đặt trong thư mục res/drawable/ có thể được tự động tối ưu hoá với tính năng nén hình ảnh không tổn hao bằng công cụ aapt trong quy trình xây dựng. Ví dụ: tệp PNG màu thực không yêu cầu quá 256 màu có thể được chuyển đổi thành tệp PNG 8 bit thông qua bảng màu. Kết quả là hình ảnh có chất lượng như nhau, nhưng cần ít bộ nhớ hơn. Do đó, các tệp nhị phân hình ảnh được đặt trong thư mục này có thể thay đổi theo thời gian xây dựng. Nếu bạn định đọc một hình ảnh dưới dạng luồng bit để chuyển đổi hình ảnh đó thành một bitmap, hãy đặt hình ảnh của bạn vào thư mục res/raw/, nơi công cụ aapt không sửa đổi những hình ảnh đó.

Đoạn mã sau đây minh hoạ cách tạo ImageView sử dụng hình ảnh được tạo từ tài nguyên có thể vẽ và thêm hình ảnh đó vào bố cục:

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);
}

Trong các trường hợp khác, bạn nên xử lý tài nguyên hình ảnh dưới dạng đối tượng Drawable, như trong ví dụ sau:

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);

Cảnh báo: Mỗi tài nguyên riêng biệt trong dự án của bạn chỉ có thể duy trì một trạng thái, bất kể bạn tạo thực thể cho bao nhiêu đối tượng khác nhau. Ví dụ: nếu bạn tạo thực thể cho 2 đối tượng Drawable từ cùng một tài nguyên hình ảnh và thay đổi một thuộc tính (chẳng hạn như alpha) cho một đối tượng, thì đối tượng đó cũng sẽ ảnh hưởng đến đối tượng còn lại. Khi xử lý nhiều thực thể của một tài nguyên hình ảnh, thay vì chuyển đổi trực tiếp đối tượng Drawable, bạn nên thực hiện ảnh động dạng tween.

Đoạn mã XML dưới đây cho biết cách thêm tài nguyên có thể vẽ vào ImageView trong bố cục XML:

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

Để biết thêm thông tin về cách sử dụng tài nguyên của dự án, hãy xem bài viết Tài nguyên và thành phần.

Lưu ý: Khi sử dụng tài nguyên hình ảnh làm nguồn của các đối tượng có thể vẽ, hãy đảm bảo hình ảnh có kích thước phù hợp với nhiều mật độ pixel. Nếu không chính xác, hình ảnh sẽ được điều chỉnh theo tỷ lệ cho phù hợp. Điều này có thể gây ra việc tạo cấu phần mềm trong đối tượng có thể vẽ của bạn. Để biết thêm thông tin, hãy đọc bài viết Hỗ trợ nhiều mật độ pixel.

Tạo đối tượng có thể vẽ từ tài nguyên XML

Nếu bạn muốn tạo một đối tượng Drawable, ban đầu đối tượng này không phụ thuộc vào các biến do mã của bạn hoặc hoạt động tương tác của người dùng xác định, thì bạn nên xác định Drawable trong XML. Ngay cả khi bạn muốn Drawable thay đổi các thuộc tính trong quá trình người dùng tương tác với ứng dụng, bạn cũng nên cân nhắc việc xác định đối tượng trong XML, vì bạn có thể sửa đổi các thuộc tính sau khi đối tượng đã được tạo thực thể.

Sau khi bạn xác định Drawable trong XML, hãy lưu tệp vào thư mục res/drawable/ của dự án. Ví dụ sau đây cho thấy mã XML xác định tài nguyên TransitionDrawable kế thừa từ 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>

Sau đó, hãy truy xuất và tạo thực thể cho đối tượng bằng cách gọi Resources#getDrawable() và truyền mã nhận dạng tài nguyên của tệp XML. Bất kỳ lớp con Drawable nào hỗ trợ phương thức inflate() đều có thể được xác định trong XML và được ứng dụng tạo thực thể.

Mỗi lớp có thể vẽ hỗ trợ tính năng tăng cường XML sẽ sử dụng các thuộc tính XML cụ thể giúp xác định các thuộc tính đối tượng. Mã sau đây sẽ tạo thực thể cho TransitionDrawable và đặt thành nội dung của đối tượng 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.

Để biết thêm thông tin về các thuộc tính XML được hỗ trợ, hãy tham khảo các lớp nêu trên.

Hình dạng có thể vẽ

Đối tượng ShapeDrawable có thể là một lựa chọn phù hợp khi bạn muốn tự động vẽ đồ hoạ hai chiều. Bạn có thể lập trình để vẽ các hình dạng gốc trên đối tượng ShapeDrawable và áp dụng các kiểu mà ứng dụng của bạn cần.

ShapeDrawable là lớp con của Drawable. Vì lý do này, bạn có thể sử dụng ShapeDrawable tại bất cứ nơi nào có Drawable. Ví dụ: bạn có thể sử dụng đối tượng ShapeDrawable để đặt nền của khung hiển thị bằng cách truyền đối tượng đó vào phương thức setBackgroundDrawable() của khung hiển thị đó. Bạn cũng có thể vẽ hình dạng dưới dạng khung hiển thị tuỳ chỉnh riêng rồi thêm hình dạng đó vào một bố cục trong ứng dụng.

ShapeDrawable có phương thức draw() riêng, nên bạn có thể tạo một lớp con của View vẽ đối tượng ShapeDrawable trong sự kiện onDraw(), như minh hoạ trong đoạn mã ví dụ sau:

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);
  }
}

Bạn có thể sử dụng lớp CustomDrawableView trong mã mẫu ở trên giống như cách sử dụng bất kỳ khung hiển thị tuỳ chỉnh nào khác. Ví dụ: bạn có thể thêm thông tin này vào một hoạt động trong ứng dụng theo phương thức lập trình, như trong ví dụ sau:

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);
}

Nếu bạn muốn dùng khung hiển thị tuỳ chỉnh trong bố cục XML, thì lớp CustomDrawableView phải ghi đè hàm khởi tạo View(Context, AttributeSet). Hàm này được gọi khi lớp được tăng cường từ XML. Ví dụ sau cho biết cách khai báo CustomDrawableView trong bố cục XML:

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

Giống như nhiều loại đối tượng có thể vẽ khác trong gói android.graphics.drawable, lớp ShapeDrawable cho phép bạn xác định nhiều thuộc tính của đối tượng bằng các phương thức công khai. Một số thuộc tính mẫu mà bạn có thể muốn điều chỉnh, bao gồm độ trong suốt alpha, bộ lọc màu, tông màu, độ mờ và màu sắc.

Bạn cũng có thể xác định các hình dạng có thể vẽ nguyên gốc bằng tài nguyên XML. Để biết thêm thông tin, hãy xem phần Hình dạng đối tượng có thể vẽ trong phần Các loại tài nguyên có thể vẽ.

Đối tượng có thể vẽ NinePatch

Đồ hoạ NinePatchDrawable là hình ảnh bitmap có thể co giãn mà bạn có thể dùng làm nền của khung hiển thị. Android sẽ tự động đổi kích thước đồ hoạ cho phù hợp với nội dung của thành phần hiển thị. Một ví dụ về cách sử dụng hình ảnh NinePatch làm nền cho các nút Android tiêu chuẩn – các nút phải kéo giãn để phù hợp với các chuỗi có độ dài khác nhau. Đồ hoạ NNAPI là một hình ảnh PNG tiêu chuẩn có thêm đường viền 1 pixel. Bạn phải lưu tệp này bằng phần mở rộng 9.png trong thư mục res/drawable/ của dự án.

Dùng đường viền để xác định các vùng tĩnh và vùng co giãn của hình ảnh. Bạn chỉ định phần có thể co giãn bằng cách vẽ một (hoặc nhiều) đường màu đen rộng 1 pixel ở phần bên trái và trên cùng của đường viền (các pixel đường viền khác phải hoàn toàn trong suốt hoặc có màu trắng). Bạn có thể có số lượng phần có thể co giãn tuỳ ý. Kích thước tương đối của các phần có thể co giãn vẫn giữ nguyên, vì vậy phần lớn nhất luôn luôn lớn nhất.

Bạn cũng có thể xác định một phần có thể vẽ không bắt buộc của hình ảnh (hiệu quả hơn là các đường khoảng đệm) bằng cách vẽ một đường ở bên phải và một đường ở dưới cùng. Nếu một đối tượng View đặt đồ hoạ NinePatch làm nền, sau đó chỉ định văn bản của khung hiển thị, thì đối tượng này sẽ tự kéo giãn để tất cả văn bản chỉ chiếm khu vực được chỉ định bởi các dòng bên phải và dưới cùng (nếu có). Nếu không bao gồm các đường khoảng đệm, Android sẽ sử dụng các dòng bên trái và trên cùng để xác định vùng có thể vẽ này.

Để làm rõ sự khác biệt giữa các đường này, dòng bên trái và trên cùng sẽ xác định pixel nào của hình ảnh được phép sao chép để kéo dài hình ảnh. Các dòng dưới cùng và bên phải xác định vùng tương đối trong hình ảnh mà nội dung của thành phần hiển thị được phép chiếm.

Hình 1 cho thấy ví dụ về đồ hoạ NinePatch dùng để xác định một nút:

Hình ảnh vùng co giãn
và hộp khoảng đệm

Hình 1: Ví dụ về đồ hoạ NinePatch xác định một nút

Hình ảnh đồ hoạ NinePatch này xác định một vùng có thể co giãn với các dòng bên trái và trên cùng, cũng như vùng có thể vẽ với các đường dưới cùng và bên phải. Trong hình ảnh trên cùng, các đường chấm màu xám xác định các vùng của hình ảnh được sao chép để kéo dài hình ảnh. Hình chữ nhật màu hồng trong hình ảnh dưới cùng xác định khu vực mà nội dung của thành phần hiển thị được phép. Nếu nội dung không vừa với vùng này, thì hình ảnh sẽ được kéo giãn để vừa với vùng này.

Công cụ Draw 9-patch cung cấp một cách cực kỳ tiện lợi để tạo hình ảnh NinePatch, bằng cách sử dụng trình chỉnh sửa đồ hoạ WYSIWYG. Thao tác này thậm chí còn đưa ra cảnh báo nếu vùng bạn đã xác định cho vùng có thể co giãn có nguy cơ tạo ra các cấu phần phần mềm vẽ do quá trình sao chép pixel.

XML bố cục mẫu sau đây minh hoạ cách thêm đồ hoạ NinePatch vào một số nút. Hình ảnh NinePatch được lưu vào 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"/>

Lưu ý rằng các thuộc tính layout_widthlayout_height được đặt thành wrap_content để làm cho nút nằm vừa vặn quanh văn bản.

Hình 2 cho thấy hai nút được kết xuất từ hình ảnh XML và NinePatch hiển thị ở trên. Hãy lưu ý đến cách chiều rộng và chiều cao của nút thay đổi theo văn bản và hình nền sẽ kéo giãn để phù hợp với văn bản đó.

Hình ảnh các nút nhỏ
và có kích thước bình thường

Hình 2: Các nút được kết xuất bằng tài nguyên XML và đồ hoạ NinePatch

Đối tượng có thể vẽ tuỳ chỉnh

Nếu muốn, bạn có thể tạo một số bản vẽ tuỳ chỉnh bằng cách mở rộng lớp Drawable (hoặc bất kỳ lớp con nào của lớp này).

Phương thức quan trọng nhất cần triển khai là draw(Canvas) vì phương thức này cung cấp đối tượng Canvas mà bạn phải sử dụng để cung cấp hướng dẫn vẽ.

Mã sau đây cho thấy một lớp con đơn giản của Drawable vẽ một vòng tròn:

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;
    }
}

Sau đó, bạn có thể thêm đối tượng có thể vẽ ở bất cứ nơi nào bạn muốn, chẳng hạn như vào ImageView như minh hoạ dưới đây:

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));

Trên Android 7.0 (API cấp 24) trở lên, bạn cũng có thể xác định các thực thể của đối tượng có thể vẽ tuỳ chỉnh bằng XML theo những cách sau:

  • Sử dụng tên lớp đủ điều kiện làm tên phần tử XML. Đối với phương pháp này, lớp đối tượng có thể vẽ tuỳ chỉnh phải là lớp công khai cấp cao nhất:
    <com.myapp.MyDrawable xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#ffff0000" />
    
  • Sử dụng drawable làm tên thẻ XML và chỉ định tên lớp đủ điều kiện từ thuộc tính lớp. Phương pháp này có thể dùng cho cả lớp công khai cấp cao nhất và các lớp tĩnh bên trong công khai:
    <drawable xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.myapp.MyTopLevelClass$MyDrawable"
        android:color="#ffff0000" />
    

Thêm sắc thái màu vào đối tượng có thể vẽ

Với Android 5.0 (API cấp 21) trở lên, bạn có thể phủ màu bitmap và 9-patch được xác định là mặt nạ alpha. Bạn có thể phủ màu bằng các tài nguyên màu hoặc thuộc tính giao diện phân giải các tài nguyên màu (ví dụ: ?android:attr/colorPrimary). Thông thường, bạn chỉ tạo những thành phần này một lần và tự động tô màu cho phù hợp với giao diện của mình.

Bạn có thể áp dụng sắc thái màu cho các đối tượng BitmapDrawable, NinePatchDrawable hoặc VectorDrawable bằng phương thức setTint(). Bạn cũng có thể đặt chế độ và màu phủ trong bố cục bằng các thuộc tính android:tintandroid:tintMode.

Trích xuất màu nổi bật từ một hình ảnh

Thư viện hỗ trợ Android có lớp Palette, cho phép bạn trích xuất các màu nổi bật từ một hình ảnh. Bạn có thể tải các đối tượng có thể vẽ dưới dạng Bitmap rồi truyền đối tượng đó vào Palette để truy cập vào các màu đó. Để biết thêm thông tin, hãy đọc bài viết Chọn màu bằng Palette API (API Bảng khung hiển thị).