Kéo và thả

Khung kéo và thả của Android cho phép bạn thêm khả năng kéo và thả tương tác vào ứng dụng của mình. Với thao tác kéo và thả, người dùng có thể sao chép hoặc di chuyển văn bản, hình ảnh, đối tượng – bất kỳ nội dung nào có thể được biểu thị bằng một URI – từ View sang thành phần khác trong ứng dụng hoặc, trongchế độ nhiều cửa sổ giữa các ứng dụng.

Chuỗi văn bản và hình ảnh được kéo và thả trong một ứng dụng. Chuỗi văn bản và hình ảnh được kéo và thả giữa các ứng dụng trong chế độ chia đôi màn hình.
Hình 1. Kéo và thả trong ứng dụng.
Hình 2. Kéo và thả giữa các ứng dụng.

Khung này bao gồm một lớp sự kiện kéo, các trình nghe sự kiện kéo (drag listener), các lớp hỗ trợ (helper) và phương thức. Mặc dù được thiết kế chủ yếu để cho phép chuyển dữ liệu, nhưng bạn có thể sử dụng khung này cho các thao tác khác trên giao diện người dùng. Ví dụ: bạn có thể tạo một ứng dụng kết hợp các màu khi người dùng kéo một biểu tượng màu lên trên một biểu tượng khác. Tuy nhiên, phần còn lại của hướng dẫn này mô tả khung kéo và thả trong bối cảnh truyền dữ liệu.

Tổng quan

Thao tác kéo và thả sẽ bắt đầu khi người dùng thực hiện một cử chỉ trên giao diện người dùng mà ứng dụng nhận dạng là một tín hiệu để bắt đầu kéo dữ liệu. Để phản hồi, ứng dụng sẽ thông báo cho hệ thống rằng thao tác kéo và thả đang bắt đầu. Hệ thống gọi lại ứng dụng của bạn để lấy định dạng dữ liệu đang được kéo (bóng khi kéo). Khi người dùng di chuyển bóng khi kéo (drag shadow) trên bố cục của ứng dụng, hệ thống sẽ gửi các sự kiện kéo đến trình nghe sự kiện kéo và các phương thức gọi lại liên kết với các đối tượng View trong bố cục. Nếu người dùng thả bóng khi kéo sang một thành phần hiển thị có thể chấp nhận dữ liệu (mục tiêu thả), hệ thống sẽ gửi dữ liệu đến mục tiêu. Thao tác kéo và thả kết thúc khi người dùng thả bóng khi kéo cho dù bóng khi kéo có nằm trên mục tiêu thả hay không.

Bạn tạo một trình nghe sự kiện kéo bằng cách triển khai View.OnDragListener. Bạn thiết lập trình nghe cho mục tiêu thả bằng phương thức setOnDragListener() của đối tượng View. Mỗi thành phần hiển thị trong bố cục cũng có một phương thức gọi lại onDragEvent().

Ứng dụng của bạn thông báo cho hệ thống bắt đầu thao tác kéo và thả bằng cách gọi phương thức startDragAndDrop(). Phương thức này sẽ cho hệ thống biết để bắt đầu gửi các sự kiện kéo. Phương thức này cũng cung cấp cho hệ thống dữ liệu mà người dùng đang kéo và siêu dữ liệu mô tả dữ liệu đó. Bạn có thể gọi startDragAndDrop() trên bất kỳ View nào trong bố cục hiện tại. Hệ thống chỉ sử dụng đối tượng View để có quyền truy cập vào các tùy chọn cài đặt chung trong bố cục.

Trong quá trình kéo và thả, hệ thống sẽ gửi các sự kiện kéo đến trình nghe sự kiện kéo hoặc phương thức gọi lại của các đối tượng View trong bố cục. Trình nghe sự kiện hoặc phương thức gọi lại sử dụng siêu dữ liệu để quyết định xem có muốn chấp nhận dữ liệu khi dữ liệu đó được thả hay không. Nếu người dùng thả dữ liệu trên mục tiêu thả (View chấp nhận dữ liệu đó), hệ thống sẽ gửi một đối tượng sự kiện kéo chứa dữ liệu đến trình nghe sự kiện kéo hoặc phương thức gọi lại của mục tiêu thả.

Trình nghe sự kiện kéo và phương thức gọi lại

View nhận sự kiện kéo bằng trình lắng nghe sự kiện kéo mà sẽ triển khai View.OnDragListener hoặc bằng phương thức gọi lại onDragEvent() của thành phần hiển thị. Khi gọi phương thức hoặc trình nghe sự kiện, hệ thống sẽ cung cấp một đối số DragEvent.

Trong hầu hết trường hợp, bạn nên sử dụng trình nghe thay vì phương thức gọi lại. Khi thiết kế giao diện người dùng, bạn thường không phân lớp các lớp View, nhưng khi sử dụng phương thức gọi lại, bạn sẽ phải tạo các lớp con để ghi đè phương thức. Khi so sánh, bạn có thể triển khai một lớp trình nghe rồi sử dụng lớp đó với nhiều đối tượng View riêng biệt. Bạn cũng có thể triển khai thẻ này dưới dạng lớp nội tuyến ẩn danh hoặc biểu thức lambda. Để thiết lập trình nghe cho đối tượng View, hãy gọi setOnDragListener().

Thay vào đó, bạn có thể thay đổi phương thức triển khai mặc định của onDragEvent() mà không cần ghi đè phương thức. Nếu bạn thiết lập OnReceiveContentListener trên một thành phần hiển thị (xem setOnReceiveContentListener()), phương thức onDragEvent() theo mặc định sẽ thực hiện những việc sau:

  • Trả về giá trị đúng (true) khi gọi tới startDragAndDrop()
  • Gọi performReceiveContent() nếu dữ liệu kéo và thả được thả trên thành phần hiển thị

    Dữ liệu được truyền vào phương thức dưới dạng đối tượng ContentInfo. Phương thức gọi OnReceiveContentListener.

  • Trả về giá trị đúng (true) nếu dữ liệu kéo và thả được thả vào thành phần hiển thị và OnReceiveContentListener sử dụng bất kỳ nội dung nào

Bạn định nghĩa OnReceiveContentListener để xử lý dữ liệu dành riêng cho ứng dụng của mình. Để giảm khả năng tương thích ngược xuống API cấp 24, hãy sử dụng phiên bản Jetpack OnReceiveContentListener.

Bạn có thể có cả trình nghe sự kiện kéo và phương thức gọi lại cho đối tượng View, trong trường hợp này, hệ thống sẽ gọi trình nghe trước. Hệ thống không gọi phương thức gọi lại trừ khi trình nghe trả về false.

Việc kết hợp phương thức onDragEvent()View.OnDragListener sẽ tương tự như cách kết hợp onTouchEvent()View.OnTouchListener dùng cho các sự kiện chạm.

Quy trình kéo và thả

Về cơ bản, có 4 bước hoặc trạng thái trong quá trình kéo và thả: Bắt đầu (Started), Tiếp tục (Continuing), Thả (Dropped) và Kết thúc (Ended).

Bắt đầu

Để phản hồi thao tác kéo của người dùng, ứng dụng của bạn sẽ gọi startDragAndDrop() để yêu cầu hệ thống bắt đầu thao tác kéo và thả. Các đối số của phương thức cung cấp các yếu tố sau:

  • Dữ liệu cần kéo
  • Lệnh gọi lại để vẽ bóng khi kéo
  • Siêu dữ liệu mô tả dữ liệu đã kéo

Trước tiên, hệ thống sẽ phản hồi bằng cách gọi lại ứng dụng của bạn để lấy bóng khi kéo. Sau đó, hệ thống sẽ hiển thị bóng khi kéo trên thiết bị.

Tiếp theo, hệ thống sẽ gửi một sự kiện kéo có loại thao tác ACTION_DRAG_STARTED cho trình nghe sự kiện kéo của tất cả View trong bố cục hiện tại. Để tiếp tục nhận các sự kiện kéo, bao gồm cả một sự kiện thả có thể xảy ra, trình nghe sự kiện kéo phải trả về true. Việc này sẽ đăng ký trình nghe với hệ thống. Chỉ những trình nghe đã đăng ký mới tiếp tục nhận được sự kiện kéo. Tại thời điểm này, trình nghe cũng có thể thay đổi giao diện của đối tượng View mục tiêu thả để cho biết rằng thành phần hiển thị có thể chấp nhận sự kiện thả.

Nếu trình nghe sự kiện kéo trả về false, thì trình nghe sự kiện sẽ không nhận được sự kiện kéo cho thao tác hiện tại cho đến khi hệ thống gửi sự kiện kéo có loại thao tác ACTION_DRAG_ENDED. Khi trả về false, trình nghe sẽ cho hệ thống biết rằng nó không quan tâm đến thao tác kéo và thả, cũng như không muốn chấp nhận dữ liệu đã kéo.

của thành phần hiển thị.
Tiếp tục

Người dùng tiếp tục kéo. Lúc bóng khi kéo giao với hộp giới hạn (bounding box) của mục tiêu thả, hệ thống sẽ gửi một hoặc nhiều sự kiện kéo đến trình nghe sự kiện kéo của mục tiêu. Trình nghe có thể chọn thay đổi giao diện của mục tiêu thả View để phản hồi sự kiện. Ví dụ: nếu sự kiện cho biết bóng khi thả đã vào phạm vi hộp giới hạn của mục tiêu thả xuống (loại thao tác ACTION_DRAG_ENTERED), thì trình nghe có thể phản ứng bằng cách làm nổi bật View.

Thả

Người dùng thả bóng khi kéo trong hộp giới hạn của mục tiêu thả. Hệ thống sẽ gửi trình nghe của mục tiêu thả một sự kiện kéo có loại thao tác ACTION_DROP. Đối tượng sự kiện kéo chứa dữ liệu đã được truyền đến hệ thống qua lệnh gọi startDragAndDrop() đã bắt đầu thao tác. Trình nghe dự kiến sẽ trả về boolean true cho hệ thống nếu trình nghe xử lý thành công dữ liệu được thả.

Lưu ý rằng bước này chỉ xảy ra nếu người dùng thả bóng khi kéo trong hộp giới hạn của View mà trình nghe đã đăng ký để nhận sự kiện kéo (mục tiêu thả). Nếu người dùng thả bóng khi kéo trong bất kỳ tình huống nào khác, thì sẽ không có sự kiện kéo ACTION_DROP nào được gửi.

Kết thúc

Sau khi người dùng thả bóng khi kéo và sau khi hệ thống gửi đi (nếu cần) một sự kiện kéo có loại thao tác ACTION_DROP, hệ thống sẽ gửi một sự kiện kéo với loại thao tác ACTION_DRAG_ENDED để cho biết thao tác kéo và thả đã kết thúc. Việc này được thực hiện bất kể người dùng đã thả bóng khi kéo ở đâu. Sự kiện này được gửi tới mọi trình nghe đã đăng ký để nhận sự kiện kéo, ngay cả khi trình nghe cũng nhận được sự kiện ACTION_DROP.

Mỗi bước trong số 4 bước này đều được mô tả chi tiết hơn trong phần Thao tác kéo và thả.

Sự kiện kéo

Hệ thống sẽ gửi một sự kiện kéo dưới dạng đối tượng DragEvent, chứa loại thao tác mô tả những gì đang xảy ra trong quá trình kéo và thả. Tuỳ thuộc vào loại thao tác mà đối tượng cũng có thể chứa dữ liệu khác.

Trình nghe sự kiện kéo sẽ nhận đối tượng DragEvent. Để biết loại thao tác, trình nghe gọi DragEvent#getAction(). Có 6 giá trị có thể được xác định qua các hằng số trong lớp DragEvent.

Bảng 1. Các loại thao tác sự kiện kéo

Loại thao tác Ý nghĩa
ACTION_DRAG_STARTED Ứng dụng gọi startDragAndDrop() và có một bóng khi kéo. Nếu muốn tiếp tục nhận các sự kiện kéo cho thao tác này, trình nghe phải trả về giá trị boolean true cho hệ thống.
ACTION_DRAG_ENTERED Bóng khi thả vừa vào phạm vi hộp giới hạn của View của trình nghe sự kiện kéo. Đây là loại thao tác sự kiện đầu tiên mà trình nghe nhận được lúc phần bóng khi kéo vào phạm vi hộp giới hạn.
ACTION_DRAG_LOCATION Sau sự kiện ACTION_DRAG_ENTERED, bóng (shadow) vẫn nằm trong hộp giới hạn của View của trình nghe sự kiện kéo.
ACTION_DRAG_EXITED Sau ACTION_DRAG_ENTERED và ít nhất một sự kiện ACTION_DRAG_LOCATION, bóng khi kéo đã di chuyển ra ngoài hộp giới hạn của View của trình nghe sự kiện kéo.
ACTION_DROP Bóng khi kéo được thả qua View của trình nghe sự kiện kéo. Loại thao tác này chỉ được gửi đến trình nghe của đối tượng View nếu trình nghe đã trả về boolean true để phản hồi lại sự kiện kéo ACTION_DRAG_STARTED. Loại thao tác này sẽ không được gửi nếu người dùng thả bóng khi kéo trên View có trình nghe chưa đăng ký hoặc nếu người dùng thả bóng khi kéo trên bất kỳ phần khác không thuộc bố cục hiện tại.

Trình nghe dự kiến sẽ trả về giá trị boolean true nếu xử lý thành công sự kiện thả. Nếu không, trình nghe sẽ trả về giá trị false.

ACTION_DRAG_ENDED Hệ thống đang kết thúc thao tác kéo và thả. Loại thao tác này không nhất thiết phải xảy ra sau sự kiện ACTION_DROP. Nếu hệ thống gửi một ACTION_DROP, thì việc nhận được loại thao tác ACTION_DRAG_ENDED không cho biết rằng sự kiện thả đã được thực hiện thành công. Trình nghe phải gọi getResult() (xem bảng 2) để nhận giá trị được trả về khi phản hồi ACTION_DROP. Nếu ACTION_DROP không được gửi, thì getResult() sẽ trả về false.

Đối tượng DragEvent cũng chứa dữ liệu và siêu dữ liệu mà ứng dụng của bạn cung cấp cho hệ thống trong lệnh gọi startDragAndDrop(). Một số dữ liệu chỉ hợp lệ cho một số loại thao tác nhất định được tóm tắt trong bảng 2. Để biết thêm thông tin về các sự kiện và dữ liệu liên quan, hãy xem bài Thao tác kéo và thả.

Hình 2. Dữ liệu DragEvent hợp lệ theo loại thao tác

Giá trị getAction()
Giá trị getClipDescription()
Giá trị getLocalState()
Giá trị getX()
Giá trị getY()
Giá trị getClipData()
Giá trị getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

Phương thức DragEvent getAction(), describeContents(), writeToParcel()toString() luôn trả về dữ liệu hợp lệ.

Nếu không chứa dữ liệu hợp lệ cho một loại thao tác cụ thể, thì phương thức đó sẽ trả về null hoặc 0, tuỳ thuộc vào loại kết quả của phương thức đó.

Bóng khi kéo

Trong quá trình kéo và thả, hệ thống sẽ hiển thị một hình ảnh mà người dùng kéo. Đối với di chuyển dữ liệu, hình ảnh này đại diện cho dữ liệu đang được kéo. Đối với các thao tác khác, hình ảnh đại diện cho một số chương trình thành phần của thao tác kéo.

Hình ảnh này được gọi là bóng khi kéo (drag shadow). Bạn tạo bóng khi kéo bằng các phương thức bạn khai báo cho đối tượng View.DragShadowBuilder. Bạn truyền trình tạo tới hệ thống khi bắt đầu thao tác kéo và thả bằng startDragAndDrop(). Để phản hồi startDragAndDrop(), hệ thống gọi các phương thức gọi lại mà bạn đã định nghĩa trong View.DragShadowBuilder để lấy bóng khi kéo.

Lớp View.DragShadowBuilder có hai hàm khởi tạo (constructor):

View.DragShadowBuilder(View)

Hàm khởi tạo này chấp nhận mọi đối tượng View của ứng dụng. Hàm khởi tạo lưu trữ đối tượng View trong đối tượng View.DragShadowBuilder, vì vậy, các lệnh gọi lại có thể truy cập vào đối tượng đó để tạo bóng khi kéo. Thành phần hiển thị không nhất thiết phải là View (nếu có) mà người dùng đã chọn để bắt đầu thao tác kéo.

Nếu sử dụng hàm khởi tạo này, bạn không cần phải mở rộng View.DragShadowBuilder hoặc ghi đè phương thức của hàm khởi tạo. Theo mặc định, bạn sẽ thấy một bóng khi kéo có giao diện giống như View mà bạn đã truyền vào làm đối số, nằm ở chính giữa vị trí mà người dùng đang chạm vào màn hình.

View.DragShadowBuilder()

Nếu bạn sử dụng hàm khởi tạo này, thì sẽ không có đối tượng View nào trong đối tượng View.DragShadowBuilder (trường này được đặt giá trị là null). Bạn phải mở rộng View.DragShadowBuilder và ghi đè các phương thức của hàm khởi tạo đó, nếu không bạn sẽ thấy một bóng khi kéo vô hình. Hệ thống không báo ngoại lệ.

Lớp View.DragShadowBuilder có hai phương thức để tạo bóng khi kéo:

onProvideShadowMetrics()

Hệ thống sẽ gọi phương thức này ngay sau khi bạn gọi startDragAndDrop(). Sử dụng phương thức để gửi kích thước và điểm chạm của bóng khi kéo đến hệ thống. Phương thức có hai tham số:

outShadowSize
Đối tượng Point. Chiều rộng bóng khi kéo được truyền vào x và chiều cao được truyền vào y.
outShadowTouchPoint
Đối tượng Point. Điểm chạm là vị trí trong bóng khi kéo và dưới ngón tay người dùng trong khi kéo. Vị trí X của bóng khi kéo được truyền vào x và vị trí Y được truyền vào y.
onDrawShadow()

Ngay sau khi gọi onProvideShadowMetrics(), hệ thống sẽ gọi onDrawShadow() để tạo bóng khi kéo. Phương thức này có một đối số duy nhất, đối tượng Canvas mà hệ thống tạo từ tham số bạn cung cấp trong onProvideShadowMetrics(). Phương thức này sẽ kéo bóng khi kéo trên Canvas được cung cấp.

Để cải thiện hiệu suất, bạn nên giữ cho kích thước của bóng khi kéo nhỏ. Để chọn một mục duy nhất, bạn có thể dùng biểu tượng. Để chọn nhiều mục, bạn có thể sử dụng các biểu tượng trong ngăn xếp thay vì toàn hình ảnh bung rộng trên màn hình.

Thao tác kéo và thả

Phần này trình bày từng bước cách bắt đầu kéo, phản hồi các sự kiện trong quá trình kéo, phản hồi một sự kiện thả và cách kết thúc thao tác kéo và thả.

Bắt đầu kéo

Người dùng bắt đầu kéo bằng một cử chỉ kéo, thường là chạm và giữ trên đối tượng View. Để phản hồi lại, ứng dụng của bạn cần làm những việc sau:

  1. Tạo một đối tượng ClipData và đối tượng ClipData.Item cho dữ liệu đang được di chuyển. Là một phần của ClipData, cung cấp siêu dữ liệu được lưu trữ trong đối tượng ClipDescription trong ClipData. Đối với thao tác kéo và thả không để di chuyển dữ liệu, bạn có thể muốn sử dụng null thay vì một đối tượng thực tế.

    Ví dụ: đoạn mã dưới đây cho biết cách phản hồi thao tác chạm và giữ trên ImageView bằng cách tạo đối tượng ClipData chứa thẻ (hoặc nhãn) của ImageView:

    Kotlin

    // Create a string for the ImageView label.
    val IMAGEVIEW_TAG = "icon bitmap"
    
    ...
    
    val imageView = ImageView(this).apply {
        // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere).
        setImageBitmap(iconBitmap)
        tag = IMAGEVIEW_TAG
        setOnLongClickListener { v ->
            // Create a new ClipData.
            // This is done in two steps to provide clarity. The convenience method
            // ClipData.newPlainText() can create a plain text ClipData in one step.
    
            // Create a new ClipData.Item from the ImageView object's tag.
            val item = ClipData.Item(v.tag as? CharSequence)
    
            // Create a new ClipData using the tag as a label, the plain text MIME type, and
            // the already-created item. This creates a new ClipDescription object within the
            // ClipData and sets its MIME type to "text/plain".
            val dragData = ClipData(
                v.tag as? CharSequence,
                arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                item)
    
            // Instantiate the drag shadow builder.
            val myShadow = MyDragShadowBuilder(this)
    
            // Start the drag.
            v.startDragAndDrop(dragData,  // The data to be dragged
                               myShadow,  // The drag shadow builder
                               null,      // No need to use local data
                               0          // Flags (not currently used, set to 0)
            )
    
           // Indicate that the long-click was handled.
           true
        }
    }
    

    Java

    // Create a string for the ImageView label.
    private static final String IMAGEVIEW_TAG = "icon bitmap";
    
    ...
    
    // Create a new ImageView.
    ImageView imageView = new ImageView(this);
    
    // Set the bitmap for the ImageView from an icon bit map (defined elsewhere).
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Sets a long click listener for the ImageView using an anonymous listener object that
    // implements the OnLongClickListener interface.
    imageView.setOnLongClickListener( v -> {
    
        // Create a new ClipData.
        // This is done in two steps to provide clarity. The convenience method
        // ClipData.newPlainText() can create a plain text ClipData in one step.
    
        // Create a new ClipData.Item from the ImageView object's tag.
        ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
    
        // Create a new ClipData using the tag as a label, the plain text MIME type, and
        // the already-created item. This creates a new ClipDescription object within the
        // ClipData and sets its MIME type to "text/plain".
        ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
        // Instantiate the drag shadow builder.
        View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
        // Start the drag.
        v.startDragAndDrop(dragData,  // The data to be dragged
                           myShadow,  // The drag shadow builder
                           null,      // No need to use local data
                           0          // Flags (not currently used, set to 0)
        );
    
        // Indicate that the long-click was handled.
        return true;
    });
    
  2. Đoạn mã sau đây định nghĩa myDragShadowBuilder bằng cách ghi đè các phương thức trong View.DragShadowBuilder. Mã này tạo một bóng khi kéo hình chữ nhật nhỏ màu xám cho TextView:

    Kotlin

    private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
    
        private val shadow = ColorDrawable(Color.LTGRAY)
    
        // Defines a callback that sends the drag shadow dimensions and touch point
        // back to the system.
        override fun onProvideShadowMetrics(size: Point, touch: Point) {
    
            // Set the width of the shadow to half the width of the original View.
            val width: Int = view.width / 2
    
            // Set the height of the shadow to half the height of the original View.
            val height: Int = view.height / 2
    
            // The drag shadow is a ColorDrawable. This sets its dimensions to be the
            // same as the Canvas that the system provides. As a result, the drag shadow
            // fills the Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Set the size parameter's width and height values. These get back to
            // the system through the size parameter.
            size.set(width, height)
    
            // Set the touch point's position to be in the middle of the drag shadow.
            touch.set(width / 2, height / 2)
        }
    
        // Defines a callback that draws the drag shadow in a Canvas that the system
        // constructs from the dimensions passed to onProvideShadowMetrics().
        override fun onDrawShadow(canvas: Canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
        }
    }
    

    Java

    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
        // The drag shadow image, defined as a drawable object.
        private static Drawable shadow;
    
        // Constructor
        public MyDragShadowBuilder(View v) {
    
            // Stores the View parameter.
            super(v);
    
            // Creates a draggable image that fills the Canvas provided by the system.
            shadow = new ColorDrawable(Color.LTGRAY);
        }
    
        // Defines a callback that sends the drag shadow dimensions and touch point
        // back to the system.
        @Override
        public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Defines local variables
            int width, height;
    
            // Set the width of the shadow to half the width of the original View.
            width = getView().getWidth() / 2;
    
            // Set the height of the shadow to half the height of the original View.
            height = getView().getHeight() / 2;
    
            // The drag shadow is a ColorDrawable. This sets its dimensions to be the
            // same as the Canvas that the system provides. As a result, the drag shadow
            // fills the Canvas.
            shadow.setBounds(0, 0, width, height);
    
            // Set the size parameter's width and height values. These get back to the
            // system through the size parameter.
            size.set(width, height);
    
            // Set the touch point's position to be in the middle of the drag shadow.
            touch.set(width / 2, height / 2);
        }
    
        // Defines a callback that draws the drag shadow in a Canvas that the system
        // constructs from the dimensions passed to onProvideShadowMetrics().
        @Override
        public void onDrawShadow(Canvas canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
        }
    }
    

Phản hồi khi bắt đầu kéo

Trong quá trình kéo, hệ thống sẽ gửi các sự kiện kéo đến trình nghe sự kiện kéo của các đối tượng View trong bố cục hiện tại. Trình nghe phản hồi bằng cách gọi DragEvent#getAction() để nhận loại thao tác. Khi bắt đầu kéo, phương thức này sẽ trả về ACTION_DRAG_STARTED.

Để phản hồi lại một sự kiện có loại thao tác ACTION_DRAG_STARTED, trình nghe sự kiện kéo sẽ thực hiện:

  1. Gọi DragEvent#getClipDescription() và sử dụng các phương thức loại MIME trong ClipDescription được trả về để xem liệu trình nghe có thể chấp nhận dữ liệu đang được kéo hay không.

    Nếu thao tác kéo và thả không để di chuyển dữ liệu, thì việc này có thể không cần thiết.

  2. Nếu trình nghe sự kiện kéo có thể chấp nhận thao tác thả, thì trình nghe đó sẽ trả về true để thông báo cho hệ thống tiếp tục gửi các sự kiện kéo cho trình nghe. Nếu trình nghe không thể chấp nhận thao tác thả, trình nghe sẽ trả về false và hệ thống sẽ ngừng gửi sự kiện kéo đến trình nghe cho đến khi hệ thống gửi ACTION_DRAG_ENDED để kết thúc thao tác kéo và thả.

Lưu ý rằng đối với sự kiện ACTION_DRAG_STARTED, những phương thức DragEvent sau là không hợp lệ :getClipData(), getX(), getY()getResult().

Xử lý sự kiện trong quá trình kéo

Trong thao tác kéo, các trình nghe sự kiện kéo mà trả về true để phản hồi sự kiện kéo ACTION_DRAG_STARTED sẽ tiếp tục nhận các sự kiện kéo. Các sự kiện kéo mà trình nghe nhận được trong quá trình kéo phụ thuộc vào vị trí của bóng khi kéo và mức độ hiển thị của View của trình nghe. Trình nghe chủ yếu sử dụng các sự kiện kéo để quyết định xem có nên thay đổi giao diện của View hay không.

Trong thao tác kéo, DragEvent#getAction() sẽ trả về một trong ba giá trị:

  • ACTION_DRAG_ENTERED: trình nghe nhận loại thao tác sự kiện này khi điểm chạm (điểm trên màn hình bên dưới ngón tay hoặc chuột của người dùng) di chuyển vào phạm vi hộp giới hạn của View của trình nghe.
  • ACTION_DRAG_LOCATION: Sau khi nhận được sự kiệnACTION_DRAG_ENTERED và trước khi nhận sự kiện ACTION_DRAG_EXITED, trình nghe sẽ nhận được một sự kiện ACTION_DRAG_LOCATION mỗi khi điểm chạm di chuyển. Phương thức getX()getY() trả về toạ độ X và Y của điểm chạm.
  • ACTION_DRAG_EXITED: Loại thao tác sự kiện này được gửi tới trình nghe từng nhận được ACTION_DRAG_ENTERED. Sự kiện được gửi khi điểm chạm bóng khi kéo di chuyển từ bên trong hộp giới hạn của View của trình nghe ra bên ngoài hộp giới hạn.

Trình nghe sự kiện kéo không cần phải phản hồi với bất kỳ loại thao tác nào trong số này. Nếu trình nghe trả về một giá trị cho hệ thống thì giá trị đó sẽ bị bỏ qua.

Dưới đây là một số nguyên tắc để phản hồi từng loại thao tác:

  • Để phản hồi ACTION_DRAG_ENTERED hoặc ACTION_DRAG_LOCATION, trình nghe có thể thay đổi giao diện của View để cho biết thành phần hiển thị có thể là một mục tiêu thả.
  • Sự kiện có loại thao tác ACTION_DRAG_LOCATION chứa dữ liệu hợp lệ cho getX()getY(), tương ứng với vị trí của điểm chạm. Trình nghe có thể sử dụng thông tin này để thay đổi giao diện của View tại điểm chạm hoặc để xác định vị trí chính xác mà người dùng có thể thả bóng khi kéo (tức là thả dữ liệu).
  • Để phản hồi ACTION_DRAG_EXITED, trình nghe phải đặt lại mọi thay đổi giao diện đã áp dụng cho ACTION_DRAG_ENTERED hoặc ACTION_DRAG_LOCATION. Việc này cho người dùng biết rằng View không còn là một mục tiêu thả sắp tới.

Phản hồi sự kiện thả

Khi người dùng thả bóng khi kéo qua ViewView đã báo rằng nó có thể chấp nhận nội dung đang được kéo thì hệ thống sẽ gửi một sự kiện kéo đến View với loại hành động ACTION_DROP.

Trình nghe sự kiện kéo thực hiện các bước như sau:

  1. Gọi getClipData() để lấy đối tượng ClipData ban đầu được cung cấp khi gọi startDragAndDrop() và xử lý dữ liệu.

    Nếu thao tác kéo và thả không để di chuyển dữ liệu, thì bạn không cần thực hiện bước này.

  2. Trả về giá trị boolean true để cho biết rằng sự kiện thả đã được xử lý thành công hoặc false nếu không. Giá trị trả về trở thành giá trị mà getResult() trả về cho sự kiện ACTION_DRAG_ENDED cuối cùng.

    Lưu ý rằng nếu hệ thống không gửi sự kiện ACTION_DROP thì giá trị do getResult() trả về cho sự kiện ACTION_DRAG_ENDEDfalse.

Đối với sự kiện ACTION_DROP, getX()getY(), sử dụng hệ toạ độ của View mà nhận được sự kiện thả để trả về vị trí X và Y của điểm chạm khi thả.

Hệ thống cho phép người dùng thả bóng khi kéo trên View có trình nghe sự kiện kéo không nhận được sự kiện kéo. Hệ thống cũng cho phép người dùng thả bóng khi kéo trên các vùng trống của giao diện người dùng trong ứng dụng hoặc trên các khu vực bên ngoài ứng dụng của bạn. Trong tất cả các trường hợp này, hệ thống sẽ không gửi một sự kiện có loại thao tác là ACTION_DROP. Tuy nhiên, hệ thống sẽ gửi một sự kiện ACTION_DRAG_ENDED.

Phản hồi khi kết thúc quá trình kéo

Ngay sau khi người dùng thả bóng khi kéo, hệ thống sẽ gửi một sự kiện kéo có loại thao tác là ACTION_DRAG_ENDED cho tất cả trình nghe sự kiện kéo trong ứng dụng của bạn. Việc này cho biết rằng thao tác kéo và thả đã kết thúc.

Mỗi trình nghe sự kiện kéo thực hiện các bước như sau:

  1. Nếu trình nghe thay đổi giao diện của đối tượng View trong khi thực hiện thao tác, trình nghe sẽ đặt View về lại giao diện mặc định. Đây là thông báo cho người dùng biết rằng thao tác đã kết thúc.
  2. Trình nghe có thể gọi getResult() nếu muốn để tìm hiểu thêm về thao tác. Nếu trình nghe trả về true để phản hồi một sự kiện có loại thao tác ACTION_DROP, thì getResult() sẽ trả về giá trị boolean true. Trong tất cả các trường hợp khác, getResult() sẽ trả về giá trị boolean false, bao gồm cả trường hợp hệ thống không gửi sự kiện ACTION_DROP.
  3. Để cho biết thao tác kéo và thả thành công, trình nghe phải trả về giá trị boolean true cho hệ thống.

Ví dụ về phản hồi sự kiện kéo

Tất cả sự kiện kéo sẽ được phương thức sự kiện kéo hoặc trình nghe nhận. Đoạn mã sau đây là một ví dụ đơn giản về việc phản hồi lại các sự kiện kéo:

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handles each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example of what your application might do, applies a blue color tint
                // to the View to indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Returns true to indicate that the View can accept the dragged data.
                true
            } else {
                // Returns false to indicate that, during the current drag and drop operation,
                // this View will not receive events again until ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Applies a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate()

            // Returns true; the value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Resets the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate()

            // Returns true; the value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Gets the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Gets the text data from the item.
            val dragData = item.text

            // Displays a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turns off any color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidates the view to force a redraw.
            v.invalidate()

            // Returns true. DragEvent.getResult() will return true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turns off any color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidates the view to force a redraw.
            v.invalidate()

            // Does a getResult(), and displays what happened.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Returns true; the value is ignored.
            true
        }
        else -> {
            // An unknown action type was received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handles each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determines if this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example of what your application might do, applies a blue color tint
                // to the View to indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Returns true to indicate that the View can accept the dragged data.
                return true;

            }

            // Returns false to indicate that, during the current drag and drop operation,
            // this View will not receive events again until ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Applies a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate();

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Resets the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate();

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Gets the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Gets the text data from the item.
            CharSequence dragData = item.getText();

            // Displays a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turns off any color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidates the view to force a redraw.
            v.invalidate();

            // Returns true. DragEvent.getResult() will return true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turns off any color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidates the view to force a redraw.
            v.invalidate();

            // Does a getResult(), and displays what happened.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Returns true; the value is ignored.
            return true;

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

Kéo và thả ở chế độ nhiều cửa sổ

Các thiết bị chạy Android 7.0 (API cấp 24) trở lên hỗ trợ chế độ nhiều cửa sổ, cho phép người dùng di chuyển dữ liệu từ ứng dụng này sang ứng dụng khác bằng thao tác kéo và thả (xem nội dung Hỗ trợ nhiều cửa sổ).

Ứng dụng nguồn cung cấp dữ liệu. Thao tác kéo và thả bắt đầu trong ứng dụng nguồn. Ứng dụng đích sẽ nhận được dữ liệu. Thao tác kéo và thả kết thúc trong ứng dụng đích.

Khi bắt đầu thao tác kéo và thả, ứng dụng nguồn phải thiết lập cờ DRAG_FLAG_GLOBAL để cho biết người dùng có thể kéo dữ liệu sang một ứng dụng khác.

Vì dữ liệu di chuyển qua ranh giới ứng dụng nên các ứng dụng chia sẻ quyền truy cập dữ liệu bằng cách sử dụng URI nội dung:

  • Ứng dụng nguồn phải thiết lập một hoặc cả hai cờ DRAG_FLAG_GLOBAL_URI_READDRAG_FLAG_GLOBAL_URI_WRITE, tuỳ thuộc vào quyền đọc/ghi dữ liệu mà ứng dụng nguồn muốn cấp cho ứng dụng đích.
  • Ứng dụng đích phải gọi requestDragAndDropPermissions() ngay trước khi xử lý dữ liệu mà người dùng kéo vào ứng dụng. Nếu ứng dụng đích không còn cần quyền truy cập vào dữ liệu kéo và thả, ứng dụng có thể gọi release() trên đối tượng được trả về từ requestDragAndDropPermissions(). Nếu không, các quyền truy cập sẽ được huỷ khi hoạt động (activity) chứa bị huỷ bỏ.

Đoạn mã sau đây minh hoạ cách huỷ quyền chỉ có thể đọc để kéo và thả dữ liệu ngay sau khi thao tác kéo và thả diễn ra. Hãy xem mẫu DragAndDrop trên GitHub để tham khảo ví dụ hoàn chỉnh hơn.

Hoạt động kéo và thả ở nguồn

Kotlin

// Drag a file stored in internal storage. The file is in an "images/" directory.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to allow for dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored under an "images/" directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to allow for dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

Hoạt động kéo và thả ở đích

Kotlin

// Container for where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterwards because it's
            // no longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being
            // dragged into the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterwards because
            // it's no longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

Kéo và thả đơn giản hơn nhờ DropHelper

Lớp DropHelper đơn giản hoá việc triển khai khả năng kéo và thả. Là một phần của thư viện Jetpack DragAndDrop, DropHelper hỗ trợ tương thích ngược đến cấp API 24.

Hãy dùng DropHelper để chỉ định các mục tiêu thả, tuỳ chỉnh việc làm nổi bật mục mục tiêu thả và xác định cách xử lý dữ liệu được thả.

Mục tiêu thả

DropHelper#configureView() là một phương thức tĩnh, nạp chồng cho phép bạn chỉ định các mục tiêu thả (drop target). Gồm có các tham số sau:

Ví dụ: để tạo mục tiêu thả chấp nhận hình ảnh, hãy sử dụng một trong các lệnh gọi phương thức sau:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

Lệnh gọi thứ hai bỏ qua các tuỳ chọn cấu hình mục tiêu thả, trong trường hợp này, màu dùng để làm nổi bật mục tiêu thả được đặt thành màu chủ đề phụ (hoặc màu nhấn), bán kính góc của mục tiêu thả được làm nổi bật sẽ được đặt thành 16dp và danh sách EditTexts sẽ trống (xem nội dung Cấu hình mục tiêu thả dưới đây).

Cấu hình mục tiêu thả

Lớp DropHelper.Options cho phép bạn định cấu hình các mục tiêu thả. Bạn cung cấp một thực thể (instance) của lớp cho phương thức DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) (xem mục Mục tiêu thả ở trên).

Đánh dấu mục tiêu thả

DropHelper định cấu hình các mục tiêu thả để hiển thị vùng nổi bật khi người dùng kéo nội dung lên trên mục tiêu thả. DropHelper cung cấp kiểu mặc định, nhưng DropHelper.Options cho phép bạn đặt màu của vùng nổi bật và chỉ định bán kính góc của hình chữ nhật được làm nổi bật.

Sử dụng lớp DropHelper.Options.Builder để tạo một thực thể DropHelper.Options và đặt các tuỳ chọn cấu hình, ví dụ:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

Các thành phần EditText trong mục tiêu thả

DropHelper cũng kiểm soát tiêu điểm trong mục tiêu thả khi mục tiêu chứa các trường văn bản có thể chỉnh sửa.

Mục tiêu thả có thể là một thành phần hiển thị hoặc hệ phân cấp thành phần hiển thị. Nếu hệ phân cấp thành phần hiển thị mục tiêu thả chứa một hoặc nhiều thành phần EditText, bạn phải cung cấp danh sách các thành phần cho DropHelper.Options.Builder#addInnerEditTexts(EditText...) để đảm bảo rằng mục tiêu thả được làm nổi bật và việc xử lý dữ liệu văn bản hoạt động chính xác.

DropHelper ngăn chặn các thành phần EditText trong hệ phân cấp thành phần hiển thị mục tiêu thả lấy cắp tiêu điểm từ thành phần hiển thị chứa trong các tương tác kéo.

Ngoài ra, nếu thao tác kéo và thả ClipData bao gồm dữ liệu văn bản và URI thì DropHelper sẽ chọn một trong các thành phần EditText trong mục tiêu thả để xử lý dữ liệu văn bản. Việc lựa chọn được dựa trên thứ tự ưu tiên sau:

  1. EditText nơi ClipData được thả
  2. EditText chứa con trỏ văn bản (con nháy)
  3. EditText đầu tiên được cung cấp khi gọi DropHelper.Options.Builder#addInnerEditTexts(EditText...)

Để đặt EditText làm trình xử lý dữ liệu văn bản mặc định, hãy truyền EditText làm đối số đầu tiên của lệnh gọi tới DropHelper.Options.Builder#addInnerEditTexts(EditText...). Ví dụ: nếu mục tiêu thả của bạn xử lý hình ảnh nhưng chứa các trường văn bản có thể chỉnh sửa T1, T2T3, hãy đặt T2 làm mặc định như sau:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

Xử lý dữ liệu mục tiêu thả

Phương thức DropHelper#configureView() chấp nhận OnReceiveContentListener mà bạn tạo để xử lý thao tác kéo và thả ClipData. Dữ liệu kéo và thả được cung cấp cho trình nghe trong đối tượng ContentInfoCompat. Dữ liệu văn bản đã có trong đối tượng; nội dung đa phương tiện, chẳng hạn như hình ảnh, được biểu thị bằng URI.

OnReceiveContentListener cũng xử lý dữ liệu dữ liệu được cung cấp cho mục tiêu thả do tương tác của người dùng bên cạnh dữ liệu kéo và thả (chẳng hạn như sao chép và dán) khi DropHelper#configureView() được sử dụng để định cấu hình các loại thành phần hiển thị sau đây:

  • Tất cả thành phần hiển thị nếu người dùng đang chạy Android 12 trở lên
  • AppCompatEditText xuống Android 7.0

Loại MIME, quyền và xác thực nội dung

Quá trình kiểm tra loại MIME của DropHelper dựa trên ClipDescription kéo và thả được tạo bởi ứng dụng cung cấp dữ liệu kéo và thả. Bạn nên xác thực ClipDescription để đảm bảo rằng bạn đã thiết lập đúng loại MIME.

DropHelper yêu cầu tất cả các quyền truy cập cho các URI nội dung có trong ClipData kéo và thả (xem DragAndDropPermissions). Các quyền này cho phép bạn phân giải các URI nội dung khi xử lý dữ liệu kéo và thả.

DropHelper không xác thực dữ liệu do nhà cung cấp nội dung trả về khi phân giải các URI trong dữ liệu được thả. Bạn nên kiểm tra xem dữ liệu có rỗng (null) hay không và xác minh tính chính xác của mọi dữ liệu đã phân giải.

Tài nguyên khác