Cắt xén đối tượng Canvas

Lớp học lập trình này nằm trong khoá học Kotlin nâng cao cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự, nhưng đó không phải là yêu cầu bắt buộc. Tất cả các lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Trong lớp học lập trình này, phân đoạn là một cách để xác định các vùng của hình ảnh, canvas hoặc bitmap được vẽ hoặc không được vẽ một cách có chọn lọc lên màn hình. Một mục đích của việc cắt là giảm hiện tượng vẽ nhiều lần. Vẽ nhiều lần là khi một pixel trên màn hình được vẽ nhiều lần để hiển thị hình ảnh cuối cùng. Khi giảm số lần vẽ quá mức, bạn sẽ giảm thiểu số lần một pixel hoặc vùng hiển thị được vẽ để tối đa hoá hiệu suất vẽ. Bạn cũng có thể sử dụng tính năng cắt để tạo hiệu ứng thú vị trong thiết kế giao diện người dùng và ảnh động.

Ví dụ: khi bạn vẽ một chồng thẻ chồng lên nhau như minh hoạ bên dưới, thay vì vẽ đầy đủ từng thẻ từ dưới lên, thì việc chỉ vẽ các phần hiển thị thường sẽ hiệu quả hơn. "Thường" là vì các thao tác cắt cũng có chi phí và nói chung, hệ thống Android thực hiện rất nhiều hoạt động tối ưu hoá bản vẽ.

Để chỉ vẽ các phần hiển thị của thẻ, bạn chỉ định một vùng cắt cho mỗi thẻ. Ví dụ: trong sơ đồ bên dưới, khi một hình chữ nhật cắt được áp dụng cho một hình ảnh, chỉ phần bên trong hình chữ nhật đó mới được hiển thị.

Vùng cắt thường là một hình chữ nhật, nhưng có thể là bất kỳ hình dạng hoặc tổ hợp hình dạng nào, thậm chí là văn bản. Bạn cũng có thể chỉ định xem bạn muốn bao gồm hay loại trừ khu vực bên trong vùng cắt. Ví dụ: bạn có thể tạo một vùng cắt hình tròn và chỉ hiển thị nội dung bên ngoài hình tròn.

Trong lớp học lập trình này, bạn sẽ thử nghiệm nhiều cách cắt khác nhau.

Kiến thức bạn cần có

Bạn cần thông thạo:

  • Cách tạo một ứng dụng có Activity và chạy ứng dụng đó bằng Android Studio.
  • Cách tạo và vẽ trên một Canvas.
  • Cách tạo View tuỳ chỉnh, cũng như ghi đè onDraw()onSizeChanged().

Kiến thức bạn sẽ học được

  • Cách cắt các đối tượng để vẽ trên Canvas.
  • Cách lưu và khôi phục trạng thái vẽ của một canvas.
  • Cách áp dụng các phép biến đổi cho một canvas và cho văn bản.

Bạn sẽ thực hiện

  • Tạo một ứng dụng vẽ các hình dạng bị cắt trên màn hình, minh hoạ các cách cắt khác nhau và kết quả của việc cắt đối với khả năng hiển thị của các hình dạng đó.
  • Bạn cũng sẽ vẽ một số văn bản đã dịch và bị lệch.

Ứng dụng ClippingExample minh hoạ cách bạn có thể sử dụng và kết hợp các hình dạng để chỉ định những phần nào của canvas sẽ xuất hiện trong một khung hiển thị. Ứng dụng cuối cùng của bạn sẽ có dạng như ảnh chụp màn hình dưới đây.

Bạn sẽ tạo ứng dụng này từ đầu, vì vậy, bạn sẽ phải thiết lập một dự án, xác định các phương diện và chuỗi, đồng thời khai báo một số biến.

Bước 1: Tạo dự án ClippingExample

  1. Tạo một dự án Kotlin có tên là ClippingExample bằng mẫu Empty Activity (Hoạt động trống). Sử dụng com.example.android cho tiền tố tên gói.
  2. Mở MainActivity.kt.
  3. Trong phương thức onCreate(), hãy thay thế khung hiển thị nội dung mặc định và đặt khung hiển thị nội dung thành một thực thể mới của ClippedView. Đây sẽ là khung hiển thị tuỳ chỉnh cho các ví dụ về việc cắt mà bạn sẽ tạo tiếp theo.
setContentView(ClippedView(this))
  1. Ở cùng cấp với MainActivity.kt, hãy tạo một tệp và lớp Kotlin mới cho một khung hiển thị tuỳ chỉnh có tên là ClippedView, mở rộng View. Hãy đặt chữ ký như minh hoạ bên dưới. Phần còn lại của công việc sẽ nằm trong ClippedView này. Chú giải @JvmOverloads hướng dẫn trình biên dịch Kotlin tạo các hàm nạp chồng cho hàm này để thay thế các giá trị tham số mặc định.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

Bước 2: Thêm các phương diện và tài nguyên chuỗi

  1. Xác định các phương diện mà bạn sẽ dùng cho các khung hiển thị bị cắt trong một tệp tài nguyên mới trong res/values/dimens.xml. Các phương diện mặc định này được mã hoá cứng và có kích thước phù hợp với màn hình khá nhỏ.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

Để ứng dụng trông đẹp mắt trên màn hình lớn hơn (và dễ dàng xem chi tiết hơn), bạn có thể tạo một tệp dimens có các giá trị lớn hơn chỉ áp dụng cho màn hình lớn hơn.

  1. Trong Android Studio, hãy nhấp chuột phải vào thư mục values rồi chọn New > Values resource file (Mới > Tệp tài nguyên giá trị).
  2. Trong hộp thoại New Resource File (Tệp tài nguyên mới), hãy gọi tệp dimens. Trong Available qualifiers (Bộ hạn định có sẵn), hãy chọn Smallest Screen Width (Chiều rộng màn hình nhỏ nhất) rồi nhấp vào nút >> để thêm bộ hạn định này vào Chosen qualifiers (Bộ hạn định được chọn). Nhập 480 vào hộp Chiều rộng màn hình nhỏ nhất rồi nhấp vào OK.

  1. Tệp này sẽ xuất hiện trong thư mục values (giá trị) như minh hoạ bên dưới.

  1. Nếu bạn không thấy tệp này, hãy chuyển sang chế độ xem Project Files (Tệp dự án) của ứng dụng. Đường dẫn đầy đủ của tệp mới như minh hoạ dưới đây: ClippingExample/app/src/main/res/values-sw480dp/dimens.xml.

  1. Thay thế nội dung mặc định của tệp values-sw480dp/dimens.xml bằng các kích thước bên dưới.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. Trong strings.xml, hãy thêm các chuỗi sau. Các đối tượng này sẽ được dùng để hiển thị văn bản trên canvas.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

Bước 3: Tạo và khởi chạy một đối tượng Paint và Path

  1. Chuyển về chế độ xem Android của dự án.
  2. Trong ClippedView, hãy xác định một biến Paint để vẽ. Bật tính năng khử răng cưa và sử dụng độ rộng nét vẽ cũng như cỡ chữ được xác định trong các phương diện, như minh hoạ dưới đây.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. Trong ClippedView, hãy tạo và khởi chạy một Path để lưu trữ cục bộ đường dẫn của nội dung đã vẽ. Nhập android.graphics.Path.
private val path = Path()

Bước 4: Thiết lập các hình dạng

Trong ứng dụng này, bạn đang hiển thị một số hàng và hai cột hình dạng được cắt theo nhiều cách.

Tất cả đều có điểm chung là:

  • Một hình chữ nhật (hình vuông) lớn đóng vai trò là vùng chứa
  • Một đường chéo trên hình chữ nhật lớn
  • Vòng tròn
  • Một chuỗi văn bản ngắn

Trong bước này, bạn thiết lập các kích thước cho những hình dạng đó từ các tài nguyên, để bạn chỉ phải lấy kích thước một lần khi sử dụng chúng sau này.

  1. Trong ClippedView, bên dưới path, hãy thêm các biến cho kích thước của hình chữ nhật cắt xung quanh toàn bộ nhóm hình dạng.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. Thêm các biến cho phần lồng ghép của một hình chữ nhật và độ lệch của một hình chữ nhật nhỏ.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. Thêm một biến cho bán kính của một hình tròn. Đây là bán kính của hình tròn được vẽ bên trong hình chữ nhật.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. Thêm độ lệch và kích thước văn bản cho văn bản được vẽ bên trong hình chữ nhật.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

Bước 4: Thiết lập vị trí hàng và cột

Các hình dạng cho ứng dụng này xuất hiện trong 2 cột và 4 hàng, được xác định bằng các giá trị của phương diện mà bạn thiết lập ở trên. Phép toán cho việc này không thuộc lớp học lập trình này, nhưng hãy xem xét khi bạn sao chép mã được cung cấp trong bước này.

  1. Thiết lập toạ độ cho hai cột.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. Thêm toạ độ cho từng hàng, bao gồm cả hàng cuối cùng cho văn bản đã chuyển đổi.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. Chạy ứng dụng. Ứng dụng sẽ mở ra với một màn hình trắng trống ở bên dưới tên của ứng dụng.

Trong onDraw(), bạn gọi các phương thức để vẽ 7 hình chữ nhật bị cắt khác nhau như trong ảnh chụp màn hình ứng dụng bên dưới. Tất cả các hình chữ nhật đều được vẽ theo cùng một cách; điểm khác biệt duy nhất là các vùng cắt được xác định và vị trí của chúng trên màn hình.

Thuật toán dùng để vẽ các hình chữ nhật hoạt động như minh hoạ trong sơ đồ và phần giải thích bên dưới. Tóm lại, bạn vẽ một loạt hình chữ nhật bằng cách di chuyển gốc của Canvas. Về mặt khái niệm, quy trình này bao gồm các bước sau:

(1) Trước tiên, bạn dịch Canvas đến vị trí bạn muốn vẽ hình chữ nhật. Tức là thay vì tính toán vị trí cần vẽ hình chữ nhật tiếp theo và tất cả các hình dạng khác, bạn sẽ di chuyển gốc Canvas, tức là hệ toạ độ của gốc.

(2) Sau đó, bạn vẽ hình chữ nhật tại nguồn gốc mới của canvas. Tức là bạn vẽ các hình dạng ở cùng một vị trí trong hệ toạ độ được dịch. Cách này đơn giản hơn nhiều và hiệu quả hơn một chút.

(3) Cuối cùng, bạn khôi phục Canvas về Origin ban đầu.

Sau đây là thuật toán mà bạn sẽ triển khai:

  1. Trong onDraw(), hãy gọi một hàm để tô Canvas bằng màu nền xám và vẽ các hình dạng ban đầu.
  2. Gọi một hàm cho từng hình chữ nhật bị cắt và văn bản cần vẽ.

Đối với mỗi hình chữ nhật hoặc văn bản:

  1. Lưu trạng thái hiện tại của Canvas để bạn có thể đặt lại về trạng thái ban đầu đó.
  2. Dịch Origin của canvas sang vị trí bạn muốn vẽ.
  3. Áp dụng hình dạng và đường dẫn cắt.
  4. Vẽ hình chữ nhật hoặc văn bản.
  5. Khôi phục trạng thái của Canvas.

Bước: Ghi đè onDraw()

  1. Ghi đè onDraw() như minh hoạ trong mã bên dưới. Bạn sẽ gọi một hàm cho mỗi hình dạng mà bạn đang vẽ. Bạn sẽ triển khai hàm này sau.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. Tạo các phần giữ chỗ cho từng hàm vẽ để mã tiếp tục biên dịch. Bạn có thể sao chép mã bên dưới.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

Ứng dụng vẽ cùng một hình chữ nhật và hình dạng 7 lần, lần đầu tiên không có đường viền cắt, sau đó 6 lần với nhiều đường viền cắt được áp dụng. Phương thức drawClippedRectangle() tách mã để vẽ một hình chữ nhật, như minh hoạ dưới đây.

Bước 1: Tạo phương thức drawClippedRectangle()

  1. Tạo một phương thức drawClippedRectangle() nhận đối số canvas thuộc loại Canvas.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. Trong phương thức drawClippedRectangle(), hãy đặt ranh giới của hình chữ nhật cắt cho toàn bộ hình dạng. Áp dụng một hình chữ nhật cắt để chỉ vẽ hình vuông.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

Phương thức Canvas.clipRect(...) sẽ giảm vùng màn hình mà các thao tác vẽ trong tương lai có thể ghi vào. Phương thức này đặt ranh giới cắt thành giao điểm không gian của hình chữ nhật cắt hiện tại và hình chữ nhật được truyền vào clipRect(). Có nhiều biến thể của phương thức clipRect() chấp nhận các dạng khác nhau cho các vùng và cho phép các thao tác khác nhau trên hình chữ nhật cắt.

  1. canvas bằng màu trắng. Có! Toàn bộ canvas, vì bạn không vẽ hình chữ nhật mà đang cắt! Do hình chữ nhật cắt, chỉ vùng do hình chữ nhật cắt xác định được tô màu, tạo ra một hình chữ nhật màu trắng. Phần còn lại của bề mặt vẫn có màu xám.
canvas.drawColor(Color.WHITE)
  1. Thay đổi màu thành đỏ và vẽ một đường chéo bên trong hình chữ nhật cắt.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. Đặt màu thành xanh lục và vẽ một hình tròn bên trong hình chữ nhật cắt.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. Đặt màu thành xanh dương và vẽ văn bản căn chỉnh với cạnh phải của hình chữ nhật cắt. Sử dụng canvas.drawText() để vẽ văn bản.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

Bước 2: Triển khai phương thức drawBackAndUnclippedRectangle()

  1. Để xem phương thức drawClippedRectangle() hoạt động, hãy vẽ hình chữ nhật không bị cắt đầu tiên bằng cách triển khai phương thức drawBackAndUnclippedRectangle() như minh hoạ dưới đây. Lưu canvas, dịch sang vị trí hàng và cột đầu tiên, vẽ bằng cách gọi drawClippedRectangle(), sau đó khôi phục canvas về trạng thái trước đó.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Chạy ứng dụng. Bạn sẽ thấy hình chữ nhật màu trắng đầu tiên cùng với vòng tròn, đường màu đỏ và văn bản trên nền xám.

Trong các phương thức ví dụ về việc cắt sau đây, bạn sẽ áp dụng nhiều tổ hợp vùng cắt để đạt được hiệu ứng đồ hoạ và tìm hiểu cách kết hợp các vùng cắt để tạo bất kỳ hình dạng nào bạn cần.

Mỗi phương thức này đều tuân theo cùng một mẫu.

  1. Lưu trạng thái hiện tại của canvas: canvas.save()

Ngữ cảnh hoạt động duy trì một ngăn xếp các trạng thái vẽ. Trạng thái vẽ bao gồm ma trận biến đổi hiện tại và vùng cắt hiện tại. Bạn có thể lưu trạng thái hiện tại, thực hiện các thao tác thay đổi trạng thái bản vẽ (chẳng hạn như dịch hoặc xoay canvas), rồi khôi phục trạng thái bản vẽ đã lưu. (Lưu ý: Thao tác này tương tự như lệnh "stash" trong git!).

Khi bản vẽ của bạn có các phép biến đổi, việc xâu chuỗi và huỷ các phép biến đổi bằng cách đảo ngược chúng rất dễ xảy ra lỗi. Ví dụ: nếu bạn dịch, kéo giãn rồi xoay, thì quá trình này sẽ nhanh chóng trở nên phức tạp. Thay vào đó, hãy lưu trạng thái của canvas, áp dụng các phép biến đổi, vẽ rồi khôi phục trạng thái trước đó.

Ví dụ: bạn có thể xác định một vùng cắt và lưu trạng thái đó. Sau đó, hãy dịch canvas, thêm một vùng cắt và xoay. Sau khi vẽ, bạn có thể khôi phục trạng thái cắt ban đầu và tiếp tục thực hiện một phép biến đổi tịnh tiến và biến đổi xiên khác, như minh hoạ trong sơ đồ.

  1. Dịch nguồn gốc của canvas sang toạ độ hàng/cột: canvas.translate()

Việc di chuyển gốc của canvas và vẽ cùng một thứ trong hệ toạ độ mới sẽ đơn giản hơn nhiều so với việc di chuyển tất cả các phần tử để vẽ. (Lưu ý: Bạn có thể sử dụng cùng một kỹ thuật để xoay các phần tử.)

  1. Áp dụng các phép biến đổi cho path (nếu có).
  2. Áp dụng tính năng cắt: canvas.clipPath(path)
  3. Vẽ các hình dạng: drawClippedRectangle() or drawText()
  4. Khôi phục trạng thái trước đó của canvas: canvas.restore()

Bước 1: Triển khai drawDifferenceClippingExample(canvas)

Thêm mã để vẽ hình chữ nhật thứ hai. Hình chữ nhật này dùng sự khác biệt giữa hai hình chữ nhật cắt để tạo hiệu ứng khung ảnh.

Hãy sử dụng mã bên dưới để thực hiện những việc sau:

  1. Lưu canvas.
  2. Di chuyển điểm gốc của canvas vào không gian mở ở hàng đầu tiên, cột thứ hai, bên phải hình chữ nhật đầu tiên.
  3. Áp dụng 2 hình chữ nhật cắt. Toán tử DIFFERENCE sẽ trừ hình chữ nhật thứ hai cho hình chữ nhật thứ nhất.
  1. Gọi phương thức drawClippedRectangle() để vẽ canvas đã sửa đổi.
  2. Khôi phục trạng thái canvas.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Chạy ứng dụng và ứng dụng sẽ có dạng như sau.

Bước 2: Triển khai drawCircularClippingExample(canvas)

Tiếp theo, hãy thêm mã để vẽ một hình chữ nhật sử dụng vùng cắt hình tròn được tạo từ một đường dẫn hình tròn, về cơ bản là xoá (không vẽ) hình tròn và do đó, thay vào đó sẽ hiện nền màu xám.

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Bước 3: Triển khai drawIntersectionClippingExample(canvas)

Tiếp theo, hãy thêm mã để vẽ giao điểm của hai hình chữ nhật cắt trong hàng và cột thứ hai.

Xin lưu ý rằng tuỳ thuộc vào độ phân giải màn hình, giao diện của khu vực này sẽ khác nhau. Thử nghiệm với phương diện smallRectOffset để thay đổi kích thước của vùng hiển thị. smallRectOffset nhỏ hơn sẽ tạo ra một vùng lớn hơn trên màn hình.

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Bước 4: Triển khai drawCombinedClippingExample(canvas)

Tiếp theo, hãy kết hợp các hình dạng, một hình tròn và một hình chữ nhật, rồi vẽ bất kỳ đường dẫn nào để xác định một vùng cắt.

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Bước 5: Triển khai drawRoundedRectangleClippingExample(canvas)

Tiếp theo, hãy thêm một hình chữ nhật bo tròn, đây là một hình dạng cắt thường dùng.

  1. Ở cấp cao nhất, hãy tạo và khởi chạy một biến hình chữ nhật. RectF là một lớp lưu giữ toạ độ hình chữ nhật ở dạng số thực dấu phẩy động.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. Triển khai hàm drawRoundedRectangleClippingExample(). Hàm addRoundRect() lấy một hình chữ nhật, các giá trị cho giá trị x và y của bán kính góc, cũng như hướng để tạo đường viền của hình chữ nhật bo tròn. Path.Direction chỉ định cách các hình dạng khép kín (ví dụ: hình chữ nhật, hình bầu dục) được định hướng khi chúng được thêm vào một đường dẫn. CCW là viết tắt của ngược chiều kim đồng hồ.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Bước 6: Triển khai drawOutsideClippingExample(canvas)

Cắt phần bên ngoài xung quanh hình chữ nhật bằng cách tăng gấp đôi phần lồng ghép của hình chữ nhật cắt.

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Bước 7: Triển khai drawTranslatedTextExample(canvas)

Văn bản vẽ không khác gì các hình dạng khác và bạn có thể áp dụng các phép biến đổi cho văn bản. Ví dụ: bạn có thể dịch văn bản bằng cách dịch canvas và vẽ văn bản.

  1. Triển khai hàm bên dưới.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. Chạy ứng dụng để xem văn bản đã dịch.

Bước 8: Triển khai drawSkewedTextExample(canvas)

Bạn cũng có thể làm xiên văn bản. Tức là làm biến dạng theo nhiều cách.

  1. Tạo hàm bên dưới trong ClippedView.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. Chạy ứng dụng để xem văn bản bị nghiêng được vẽ trước văn bản đã dịch.

Phương thức quickReject() Canvas cho phép bạn kiểm tra xem một hình chữ nhật hoặc đường dẫn cụ thể có nằm hoàn toàn bên ngoài các vùng hiện đang hiển thị hay không, sau khi áp dụng tất cả các phép biến đổi.

Phương thức quickReject() cực kỳ hữu ích khi bạn đang tạo các bản vẽ phức tạp hơn và cần thực hiện việc này nhanh nhất có thể. Với quickReject(), bạn có thể quyết định một cách hiệu quả những đối tượng mà bạn không cần vẽ và không cần viết logic giao cắt của riêng mình.

  • Phương thức quickReject() trả về true nếu hình chữ nhật hoặc đường dẫn hoàn toàn không xuất hiện trên màn hình. Đối với trường hợp trùng lặp một phần, bạn vẫn phải tự kiểm tra.
  • EdgeTypeAA (Khử răng cưa: Xử lý các cạnh bằng cách làm tròn, vì các cạnh có thể được khử răng cưa) hoặc BW (Đen trắng: Xử lý các cạnh bằng cách chỉ làm tròn đến ranh giới pixel gần nhất) để chỉ làm tròn đến pixel gần nhất.

Có một số phiên bản của quickReject() và bạn cũng có thể tìm thấy chúng trong tài liệu.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

Trong bài tập này, bạn sẽ vẽ trong một hàng mới, bên dưới văn bản và bên trong clipRect, như trước đây.

  • Trước tiên, bạn gọi quickReject() bằng một hình chữ nhật inClipRectangle, trùng lặp với clipRect. Vì vậy, quickReject() trả về giá trị false, clipRect được điền bằng BLACK và hình chữ nhật inClipRectangle được vẽ.

  • Sau đó, hãy thay đổi mã và gọi quickReject() bằng notInClipRectangle. quickReject() hiện trả về giá trị true, clipRect được điền bằng WHITEnotInClipRectangle không được vẽ.

Khi bạn có các bản vẽ phức tạp, điều này có thể nhanh chóng cho bạn biết những hình dạng nào hoàn toàn nằm ngoài vùng cắt và những hình dạng nào bạn có thể phải thực hiện các phép tính và bản vẽ bổ sung, vì chúng nằm một phần hoặc hoàn toàn bên trong vùng cắt.

Bước: Thử nghiệm với quickReject()

  1. Ở cấp cao nhất, hãy tạo một biến cho toạ độ y của một hàng khác.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. Thêm hàm drawQuickRejectExample() sau vào ClippedView. Đọc mã này vì mã này chứa mọi thông tin bạn cần biết để sử dụng quickReject().
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. Trong onDraw(), hãy bỏ đánh dấu ghi chú trên lệnh gọi drawQuickRejectExample().
  2. Chạy ứng dụng, bạn sẽ thấy một hình chữ nhật màu đen (đây là vùng cắt được tô) và các phần của inClipRectangle, vì 2 hình chữ nhật này chồng lên nhau nên quickReject() sẽ trả về falseinClipRectangle được vẽ.

  1. Trong drawQuickRejectExample(), hãy thay đổi mã để chạy quickReject() theo notInClipRectangle.. Giờ đây, quickReject() sẽ trả về true và vùng cắt được điền màu trắng.

Tải mã xuống cho lớp học lập trình đã hoàn thành.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén và mở tệp đó trong Android Studio.

Tải tệp Zip xuống

  • Context của một hoạt động duy trì trạng thái bảo toàn các phép biến đổi và vùng cắt cho Canvas.
  • Dùng canvas.save()canvas.restore() để vẽ và quay về trạng thái ban đầu của canvas.
  • Để vẽ nhiều hình dạng trên một canvas, bạn có thể tính toán vị trí của các hình dạng đó hoặc di chuyển (dịch) nguồn gốc của bề mặt vẽ. Cách thứ hai có thể giúp bạn dễ dàng tạo các phương thức tiện ích cho các trình tự vẽ lặp lại.
  • Vùng cắt có thể là bất kỳ hình dạng, tổ hợp hình dạng hoặc đường dẫn nào.
  • Bạn có thể thêm, trừ và giao các vùng cắt để có được chính xác vùng bạn cần.
  • Bạn có thể áp dụng các phép biến đổi cho văn bản bằng cách biến đổi canvas.
  • Phương thức quickReject() Canvas cho phép bạn kiểm tra xem một hình chữ nhật hoặc đường dẫn cụ thể có nằm hoàn toàn bên ngoài các vùng hiện đang hiển thị hay không.

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Ngoài ra, hãy xem loạt bài viết về Cấu trúc đồ hoạ để biết nội dung giải thích chi tiết về cách khung Android vẽ lên màn hình.

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Trả lời các câu hỏi sau

Câu hỏi 1

Bạn gọi phương thức nào để loại trừ hiệu quả các hình dạng khỏi việc được vẽ?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

Câu hỏi 2

Canvas.save()Canvas.restore() lưu và khôi phục thông tin nào?

▢ Màu sắc, chiều rộng đường kẻ, v.v.

▢ Chỉ các phép biến đổi hiện tại

▢ Các phép biến đổi và vùng cắt hiện tại

▢ Chỉ vùng cắt hiện tại

Câu hỏi 3

Paint.Align chỉ định:

▢ Cách căn chỉnh các hình vẽ sau

▢ Văn bản được lấy từ phía nào của nguồn

▢ Vị trí được căn chỉnh trong vùng cắt

▢ Căn chỉnh văn bản theo hướng nào so với nguồn

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của các lớp học lập trình trong khoá học Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.