Android開發者指南-用戶界面-拖放-Drag and Drop[原創譯文]

用戶界面:


Drag and Drop


英文原文:http://developer.android.com/guide/topics/ui/drag-drop.html

版本:Android 4.0 r1

譯者注:黃色底色爲未決譯文

利用Android的拖/放框架,你可以讓用戶用圖形化的拖放手勢把一個View中的數據移到當前layout內的另一個View中去。 拖放框架包括拖動事件類、拖動偵聽器,以及helper方法和類。

雖然此框架主要是爲轉移數據而設計的,但你可以將它用於其它UI action。比如,你可以創建一個混合顏色的應用,用戶可以把不同顏色的圖標通過拖放疊在一起。 不過,本文描述的是轉移數據這部分內容。

概述

當用戶觸摸出某個拖動數據的手勢,即啓動了拖放操作。作爲響應,你的應用程序應通知系統拖動已開始。系統將回調你的應用程序,以獲取拖動數據所顯示的圖像。 當用戶手指在當前layout上方拖動這個圖像(一個“拖動陰影”)時,系統會把拖動事件發送給拖動事件偵聽器對象、layout內 View 對象關聯的拖動事件回調方法。一旦用戶放開這個拖動陰影,系統就會終止拖動操作。

你可以從 View.OnDragListener 類創建一個拖動事件偵聽器對象(“listener”)。利用View對象的setOnDragListener() 方法,可爲View設置拖動事件偵聽器對象。View對象還有一個 onDragEvent() 回調方法。這兩個方法的詳細信息都在 拖動事件偵聽器和回調方法 一節中描述。

注意: 爲了簡化起見,下文中把接收拖動事件的程序都叫做“拖動事件監聽器”,儘管實際上它可能是個回調方法。

當你開始拖動時,要向系統調用傳入兩類信息:要轉移的數據和描述這些數據的元數據。在拖動過程中,系統會把拖動事件發送給拖動事件偵聽器或layout中所有View的回調方法。 此偵聽器或回調方法可以利用元數據來確定是否在放下後接受數據。 如果用戶在某個View對象上放下數據,並且該View對象的偵聽器或回調方法之前已經通知系統願意接受數據,則系統會把數據發送給偵聽器或拖動事件中的回調方法。

通過調用 startDrag() 方法,你的應用程序可以通知系統開始拖動。這將通知系統開始發送拖動事件。此方法中還負責發送所拖動的數據。

你可以調用任何當前layout內已關聯的View的 startDrag() 。系統用View對象來獲得layout的全局設置參數。

當你的應用程序完成 startDrag() 調用後,剩下的步驟就是使用系統發送給當前layout中View對象的事件了。

拖/放過程

拖放過程有四個基本步驟或狀態:

啓動Started
爲了響應用戶開始拖動的手勢,你的應用程序應調用 startDrag() 來通知系統。 startDrag() 的參數需要指定所拖動的數據、元數據和繪製拖動陰影的回調方法。

作爲響應,系統首先通過回調應用程序來獲取拖動陰影。然後在設備上顯示這個陰影。

下一步,系統會把一個帶有action類型 ACTION_DRAG_STARTED 的拖動事件發送給當前layout中所有View對象的拖動事件偵聽器。 爲了能繼續接收拖動事件,包括可能發生的放下事件,拖動事件偵聽器必須返回true。 這會向系統註冊偵聽器。只有註冊過的偵聽器才能連續接收拖動事件。這時,偵聽器還可以改變所屬View對象的外觀,以便顯示出該偵聽器可以接受放下事件。

如果拖動事件偵聽器返回false,則在系統發送帶有action類型 ACTION_DRAG_ENDED 的拖動事件之前,它就再不會收到當前操作的拖動事件了。 通過返回false,偵聽器通知系統,它對此拖動操作不感興趣,並且不願意接受拖動的數據。

保持Continuing
表示用戶保持拖動狀態。當拖動陰影與View對象的屏幕邊界相交時,系統會發送一個或多個拖動事件給View對象的拖動事件偵聽器(如果已經註冊同意接收事件)。 作爲對事件的響應,偵聽器可以選擇改變View對象的外觀。比如,如果事件表明拖動陰影已經進入了View的邊界(action類型 ACTION_DRAG_ENTERED ),偵聽器可以讓View高亮顯示。
放下Dropped
表示用戶在某個可接受數據的View的屏幕邊界內釋放了拖動陰影。系統會向View對象的偵聽器發送一個帶有action類型 ACTION_DROP 的拖動事件。拖動事件中包含了開始拖動時由 startDrag() 傳給系統的數據。如果接受放下事件執行成功,偵聽器應該向系統返回布爾值true

記住,只有View的偵聽器已註冊接收拖動事件了,用戶在此View邊界內放下拖動陰影纔會發生這一步。如果用戶是在其它情況下釋放拖動陰影,則不會發送 ACTION_DROP 拖動事件。

結束Ended
用戶釋放了拖動陰影,系統也已發出(必要時)帶有action類型 ACTION_DROP 拖動事件之後,系統會發出一個帶action類型 ACTION_DRAG_ENDED 拖動事件,用以表明拖動操作已經結束。無論用戶在何處釋放拖動陰影,這一步都會發生。 此事件會發送給所有註冊接收拖動事件的偵聽器,即使偵聽器已收到過了 ACTION_DROP 事件也一樣。

拖放操作的設計 一節中還會詳細說明這四個步驟。

拖動事件偵聽器及回調方法

一個View可以通過實現一個 View.OnDragListener 或者實現其 onDragEvent(DragEvent) 回調方法來接收拖動事件。當系統調用此方法或偵聽器時,會傳入一個 DragEvent 對象。

在絕大多數場合,你都會更願意使用偵聽器。當進行用戶界面設計時,你通常不會建立View類的子類,可是要使用回調方法你就只能這麼做,因爲你要重寫這個回調方法。 相比之下,你可以實現一個偵聽器類,並把它用於多個不同的View對象。你也可以把偵聽器實現爲匿名的內嵌類。 要設置View對象的偵聽器,請調用setOnDragListener() 。

你可以同時對View對象使用偵聽器和回調方法。這時,系統會首先調用偵聽器。只有偵聽器返回false時,系統纔會再去調用回調方法。

onDragEvent(DragEvent) 方法和 View.OnDragListener 的混合使用,與混用 onTouchEvent() 和觸摸事件的 View.OnTouchListener 的效果類似。

拖動事件

系統以 DragEvent 對象的格式送出一個拖動事件。此對象包含了一個action類型,用於告訴偵聽器拖放過程中發生的事件。根據action類型的不同,此對象中還包含了其它一些數據。

要獲取action類型,偵聽器調用 getAction() 即可。共有六種可能的類型,都在 DragEvent 類中用常量定義,已在表 1中列出。

DragEvent 對象還包含了應用程序在 startDrag() 調用中提交給系統的數據。某些數據僅針對特定action類型纔會有。 每種action對應的可用數據都列在了table 2中。表中還詳細說明了 拖放操作的設計 一節中可用的事件。

表 1. DragEvent action type

getAction() 值 含義
ACTION_DRAG_STARTED 一旦應用程序調用了 startDrag() ,某個View對象的拖動事件偵聽器接着就會收到該事件action類型,並且獲得一個拖動陰影。
ACTION_DRAG_ENTERED 一旦拖動陰影進入了某View的屏幕邊界內,此View對象的拖動事件偵聽器接着就會收到該事件action類型。這是拖動陰影進入邊界後,偵聽器收到的第一個事件action類型。 如果偵聽器期望能繼續收到本次操作的拖動事件,則必須向系統返回true
ACTION_DRAG_LOCATION 當View對象的拖動事件偵聽器已收到過一個 ACTION_DRAG_ENTERED 事件,且拖動陰影仍舊在本View的邊界範圍內時,偵聽器會收到該事件action類型。
ACTION_DRAG_EXITED 當View對象的拖動事件偵聽器收到過一個 ACTION_DRAG_ENTERED 事件和至少一個ACTION_DRAG_LOCATION ,並且用戶已經把拖動陰影移到了View的邊界之外時,偵聽器將會收到該事件action類型。
ACTION_DROP 當用戶在View對象上面釋放拖動陰影時,View對象的拖動事件偵聽器將會收到該事件action類型。僅當偵聽器在 ACTION_DRAG_STARTED 返回true時,該事件action類型纔會發送給View對象的拖動事件偵聽器。 如果用戶釋放拖動陰影時下方的View沒有註冊偵聽器,或者用戶未在當前layout上釋放拖動陰影,則該action類型不會發送。

如果偵聽器已經成功處理了放下操作,它應該返回布爾值true。否則返回false。

ACTION_DRAG_ENDED 當系統停止拖動操作時,View對象的拖動事件偵聽器將會收到該事件action類型。該action類型之前不一定是發生了 ACTION_DROP 事件。如果系統已發送了一個 ACTION_DROP ,收到action類型ACTION_DRAG_ENDED 也並不表示放下操作已經處理成功。偵聽器必須調用 getResult() 來獲取ACTION_DROP 的執行結果。如果沒有發送過 ACTION_DROP 事件,則 getResult() 將返回false

表 2. action 類型對應的可用 DragEvent 數據

getAction() 值 getClipDescription() getLocalState() getX() getY() getClipData() getResult()
ACTION_DRAG_STARTED X X X      
ACTION_DRAG_ENTERED X X X X    
ACTION_DRAG_LOCATION X X X X    
ACTION_DRAG_EXITED X X        
ACTION_DROP X X X X X  
ACTION_DRAG_ENDED X X       X

getAction()、 describeContents()、 writeToParcel() 和 toString() 方法總是返回可用數據。

如果不包含某個action類型可用的數據,此方法會返回null或0,視結果類型而定。

拖動陰影

在拖放操作的過程中,系統會顯示一個用戶所拖動部分的圖像。對於數據轉移而言,這個圖像代表了要拖動的數據。 對於其它操作,這個圖像代表了被拖動的東西

此圖像叫做拖動陰影(drag shadow)。你可以用 View.DragShadowBuilder 來創建它,並在用startDrag() 開始拖動時把它傳給系統。 調用 startDrag() 後,系統會執行你在View.DragShadowBuilder 中定義的回調方法來獲取一個拖動陰影。

View.DragShadowBuilder 類包含兩個構造方法:

View.DragShadowBuilder(View)
這個構造方法可接受你的應用程序中的任一 View 對象。它會在 View.DragShadowBuilder 對象中保存View對象,因此構造拖動陰影時你可以在執行回調方法中訪問到該View。 這樣就不一定要記住用戶選中並開始拖動操作的View了(如果有的話)。

如果使用了本構造方法,你就不必擴展 View.DragShadowBuilder 及重寫其方法。默認情況下,你會得到一個與參數中的View外觀相同的拖動陰影,並且以用戶觸摸點爲中心來顯示。

View.DragShadowBuilder()
如果你使用了本構造方法,則 View.DragShadowBuilder 對象中不存在View對象(對應的字段值爲null)。 如果使用了本構造方法,且未擴展 View.DragShadowBuilder 並重寫其方法,那麼你將得到一個不可見的拖動陰影。且系統不會報錯。

View.DragShadowBuilder 類有兩個方法:

onProvideShadowMetrics()
當你調用了 startDrag() 之後,系統馬上會調用本方法。可用於向系統發送拖動陰影的大小和觸摸點座標。本方法有兩個參數:
dimensions
一個 Point 對象。其中的 x 和 y 對應了拖動陰影的寬和高。
touch_point
一個 Point 對象。觸摸點是指用戶手指在拖動過程中所觸及的點在拖動陰影中的相對位置。X座標用 x 表示,Y座標用 y 表示。
onDrawShadow()
在調用 onProvideShadowMetrics() 之後,系統會立即調用 onDrawShadow() ,用於獲取拖動陰影。本方法有一個參數,即一個 Canvas 對象,系統根據你在 onProvideShadowMetrics() 中給出的參數來構建它,並將在這個 Canvas 上繪製拖動陰影。

爲了提高性能,你應該讓拖動陰影儘可能小一些。對於單個目標,也許你該使用圖標。如果選中了多個目標,也許你該把多個圖標堆疊起來顯示,而不是把整個圖像顯示在屏幕上。

拖放操作的設計

這一節分步展示瞭如何開始拖動、在拖動過程中響應事件、響應放下事件、結束拖放操作等內容。

開始拖動

用戶用拖動手勢來開始拖動,通常是在View對象上進行一個長按操作。在事件響應中,你應該進行:

  1. 必須創建用於移動數據的 ClipData 和 ClipData.Item 。在ClipData對象中,需要給出存放元數據的ClipDescription 對象。對於不用於轉移數據的拖放操作,你可能要用null來取代實際的對象。

    比如,以下代碼片段展示瞭如何響應ImageView上的長按操作,創建一個ClipData對象,其中包含了ImageView的tag或label。 然後,再下一段代碼展示瞭如何重寫 View.DragShadowBuilder 中的方法:

    // 創建一個字符串,用於ImageView label
    private static final String IMAGEVIEW_TAG = "icon bitmap"
    
    // 創建一個新的ImageView
    ImageView imageView = new ImageView(this);
    
    // 用某個圖標(在其它地方定義)的位圖設置ImageView位圖圖像
    imageView.setImageBitmap(mIconBitmap);
    
    // 設置tag
    imageView.setTag(IMAGEVIEW_TAG);
    
        ...
    
    // 把一個匿名偵聽器對象設爲ImageView的長按操作偵聽器
    // 偵聽器實現了OnLongClickListener接口
    imageView.setOnLongClickListener(new View.OnLongClickListener() {
    
        // 定義接口的方法,長按View時會被調用到
        public boolean onLongClick(View v) {
    
        // 新建一個ClipData
        // 這用兩步即可完成,
    // ClipData.newPlainText()方法可以很方便地一步完成一個純文本ClipData的創建工作
    
        // 用ImageView對象的tag創建一個新的ClipData.Item
        ClipData.Item item = new ClipData.Item(v.getTag());
    
        // 新建一個ClipData,用已有的tag作爲label,純文本MIME類型。
        // 這會在ClipData中新建一個ClipDescription對象,
        // 並把它的MIME類型一欄設爲"text/plain"。
        ClipData dragData = new ClipData(v.getTag(),ClipData.MIMETYPE_TEXT_PLAIN,item);
    
        // 實例化drag shadow builder.
        View.DrawShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
        // 開始拖動
    
                v.startDrag(dragData,  // 要拖動的數據
                            myShadow,  // drag shadow builder
                            null,      // 不需要用到本地數據
                            0          // 標誌位(目前未啓用,設爲0)
                );
    
        }
    }


  2. 以下代碼段定義了myDragShadowBuilder 爲TextView創建一個拖動陰影,顯示爲一個灰色的小方框:
    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
        // 拖動陰影圖像,定義爲一個drawable
        private static Drawable shadow;
    
            // 定義myDragShadowBuilder的構造方法
            public MyDragShadowBuilder(View v) {
    
                // 保存傳給myDragShadowBuilder的View參數
                super(v);
    
                // 創建一個可拖動的圖像,用於填滿系統給出的Canvas
                shadow = new ColorDrawable(Color.LTGRAY);
            }
    
            // 定義一個回調方法,用於把拖動陰影的大小和觸摸點位置返回給系統
            @Override
            public void onProvideShadowMetrics (Point size, Point touch)
                // 定義本地變量
                private int width, height;
    
                // 把陰影的寬度設爲原始View的一半
                width = getView().getWidth() / 2;
    
                // 把陰影的高度設爲原始View的一半
                height = getView().getHeight() / 2;
    
                // 拖動陰影是一個ColorDrawable對象。
                // 下面把它設爲與系統給出的Canvas一樣大小。這樣,拖動陰影將會填滿整個Canvas。
                shadow.setBounds(0, 0, width, height);
    
                // 設置長寬值,通過size參數返回給系統。
                size.set(width, height);
    
                // 把觸摸點的位置設爲拖動陰影的中心
                touch.set(width / 2, height / 2);
            }
    
            // 定義回調方法,用於在Canvas上繪製拖動陰影,Canvas由系統根據onProvideShadowMetrics()傳入的尺寸參數創建。
            @Override
            public void onDrawShadow(Canvas canvas) {
    
                // 在系統傳入的Canvas上繪製ColorDrawable
                shadow.draw(canvas);
            }
        }


    注意:請記住你不必擴展 View.DragShadowBuilder 。構造器View.DragShadowBuilder(View) 創建一個默認與傳入的參數View大小相同的拖動陰影,其中觸摸點位於拖動陰影的中心。

響應拖動開始事件

在拖動過程中,系統會向當前layout中View對象的拖動事件偵聽器發送拖動事件。偵聽器應該調用getAction() 來獲取action類型。在開始拖動時,該方法返回 ACTION_DRAG_STARTED 。

爲了響應帶action類型 ACTION_DRAG_STARTED 的事件,偵聽器應該完成以下工作:

  1. 調用 getClipDescription() 來獲取 ClipDescription 。使用 ClipDescription 的方法來獲取MIME類型,以確定偵聽器是否接受拖動的數據。

    如果拖放操作不是用於轉移數據的,這步可能就不需要了。

  2. 如果偵聽器可以接受拖動,則應該返回true。這將告訴系統以後還要向該偵聽器發送拖動事件。 如果不能接受拖動,則它應該返回false,系統將會在發出 ACTION_DRAG_ENDED 之前停止向它發送拖動事件。

請注意,對於 ACTION_DRAG_STARTED 事件而言, DragEvent 的以下方法是不可用的: getClipData()getX()、 getY()、 getResult()

拖動過程中的事件處理

在拖動過程中,響應 ACTION_DRAG_STARTED 拖動事件並返回true的偵聽器將會持續接受拖動事件。 在拖動過程中,偵聽器收到的拖動事件類型取決於拖動陰影的位置和偵聽器所屬View的可見性。

在拖動過程中,偵聽器主要依據拖動事件來確定是否要修改View的外觀。

在拖動過程中, getAction() 返回以下三種類型之一:

偵聽器不需要對這些action類型返回值。如果偵聽器向系統返回一個值,將會被忽略。下面列出了一些響應這些action類型需要遵守的規則:

  • 在對 ACTION_DRAG_ENTERED 或 ACTION_DRAG_LOCATION 的響應過程中,偵聽器可以修改View的外觀,以便顯示出將要接受放下操作。
  • 帶有action類型 ACTION_DRAG_LOCATION 的事件包含了 getX() 和 getY() 所需的數據,表示觸摸點的座標位置。偵聽器可能要用這個信息來改變View上觸摸點附近的外觀。 偵聽器還可以用此信息來確定用戶將要放下拖動陰影的確切位置。
  • 在對 ACTION_DRAG_EXITED 響應過程中,偵聽器應該把響應 ACTION_DRAG_ENTERED 或ACTION_DRAG_LOCATION 時對外觀的改動恢復原狀。這向用戶表明此View不再是作爲放下操作的目的地了。

響應放下事件

當用戶在應用程序的某個View上釋放了拖動陰影,並且這個View之前已聲明它可以接受被拖動的內容,系統就會向此View發送一個帶有View類型 ACTION_DROP 的拖動事件。偵聽器應該完成以下工作:

  1. 調用 getClipData() 來獲取先前提交給 startDrag() 的 ClipData 對象,並保存下來。如果拖放操作不是用於數據轉移,則此步不是必須的。
  2. 返回布爾值true,表明放下操作處理完成,或返回false表示不成功。 這個返回值會成爲ACTION_DRAG_ENDED 事件中 getResult() 的返回值,

    請注意,如果系統沒有發出 ACTION_DROP 事件, ACTION_DRAG_ENDED 中 getResult() 的返回值將會是false

針對 ACTION_DROP 事件, getX() 和 getY() 將返回放下時拖動點的X和Y座標,採用收到放下操作的View的座標系。

系統允許用戶在偵聽器不接收拖動事件的View上釋放拖動陰影,也允許用戶在應用程序界面的空白區域上釋放拖動陰影,甚至在應用程序之外的區域也可以。 在這些情況下,系統都不會發送帶有action類型 ACTION_DROP 的事件,不過會發出一個 ACTION_DRAG_ENDED 事件。

響應拖動結束事件

一旦用戶釋放了拖動陰影,系統會立即嚮應用程序內所有的拖動事件偵聽器發送一個拖動事件,其中附帶action類型 ACTION_DRAG_ENDED 。這表明拖動操作已經結束。

所有偵聽器都應完成以下工作:

  1. 如果偵聽器在運行過程中修改了View對象的外觀,它應該將View恢復爲默認畫面。這向用戶顯示出拖放操作已完成。
  2. 偵聽器可選擇調用 getResult() 來獲取更多關於拖放操作的信息。如果偵聽器在響應action類型爲ACTION_DROP 的事件時返回true,則 getResult() 將返回布爾值true。 除此之外,包括系統未發出ACTION_DROP 事件時, getResult() 都會返回布爾值false
  3. 偵聽器應該向系統返回true

響應拖動事件的示例

所有的拖動事件首先都是由你的拖動事件方法或偵聽器接收的。以下代碼片段簡單演示了一個在偵聽器中響應拖動事件的例子:

// 新建一個拖動事件偵聽器
mDragListen = new myDragEventListener();

View imageView = new ImageView(this);

// 爲View設置拖動事件偵聽器
imageView.setOnDragListener(mDragListen);

...

protected class myDragEventListener implements View.OnDragEventListener {

    // 這是系統向偵聽器發送拖動事件時將會調用的方法
    public boolean onDrag(View v, DragEvent event) {

        // 定義一個變量,用於保存收到事件的action類型
        final int action = event.getAction();

        // 處理所有需要的事件
        switch(action) {

            case DragEvent.ACTION_DRAG_STARTED:

                // 確定本View是否接受拖動數據
                if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                    // 作爲例子,把View的色彩濾鏡設置藍色,表示它可以接受數據。
                  v.setColorFilter(Color.BLUE);

                    // 標明View強制用新的顏色重繪
                    v.invalidate();

                    // 返回true表示View可以接受拖動數據
                    return(true);

                    } else {

                    // 返回false。在本次拖放操作中,本View不再會收到拖放事件,除非發出了ACTION_DRAG_ENDED。
                    return(false);

                    }
                break;

            case DragEvent.ACTION_DRAG_ENTERED: {

                // 把View的色彩濾鏡設置爲綠色。返回true,但返回值將被忽略。

                v.setColorFilter(Color.GREEN);

                // 表明本View強制用新的顏色重繪
                v.invalidate();

                return(true);

                break;

                case DragEvent.ACTION_DRAG_LOCATION:

                // 忽略事件
                    return(true);

                break;

                case DragEvent.ACTION_DRAG_EXITED:

                    // 重新設置爲藍色。返回true,但返回值將被忽略。
                    v.setColorFilter(Color.BLUE);

                    // 讓view失效,以便強制用新的顏色重繪。
                    v.invalidate();

                    return(true);

                break;

                case DragEvent.ACTION_DROP:

                    // 獲取包含拖動數據的item
                    ClipData.Item item = event.getClipData().getItemAt(0);

                    // 從數據項中獲取文本數據
                    dragData = item.getText();

                    // 顯示一個包含拖動數據的信息
                    Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG);

                    // 去除所有色彩濾鏡
                    v.clearColorFilter();

                    // 讓view失效,以便強制用新的顏色重繪。
                    v.invalidate();

                    // 返回true。 DragEvent.getResult()也將返回true.
                    return(true);

                break;

                case DragEvent.ACTION_DRAG_ENDED:

                    // 去除所有色彩濾鏡
                    v.clearColorFilter();

                    // 讓view失效,以便強制用新的顏色重繪。
                    v.invalidate();

                    // 執行getResult(),顯示操作的結果。
                    if (event.getResult()) {
                        Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG);

                    } else {
                        Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG);

                    };

                    // 返回true; 返回值將被忽略
                    return(true);

                break;

                // 收到一個未知的action type
                default:
                    Log.e("DragDrop Example","Unknown action type received by OnDragListener.");

                break;
        };
    };
};


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章