16.建立拖放檢視
16.1 問題
應用程式的UI需要允許使用者將一些檢視在螢幕上進行拖動,而且可以將它們放置到其他檢視的上面。
16.2 解決方案
(API Level 11)
使用框架中可用的拖放API。View類包含了對管理螢幕上的所有拖動事件的改進,而onDragListener介面則可以關聯到任何拖動事件發生時需要得到通知的View。要想開始拖動事件,只需要在希望使用者開始拖動的檢視上簡單地呼叫startDrag()方法即可。這個方法需要一個DragShadowBuilder例項,它用來構建檢視中拖動部分的外觀。另外還有兩個引數將會傳遞給放置時的目標和監聽器。
在所有傳遞的引數中首先是一個ClipData物件,用來傳遞文字或Uri例項。它在傳遞檔案路徑或查詢ContentProvider的場景下非常有用。第二個引數是一個物件,表示拖動事件的“本地狀態”。這個引數可以為任何物件,它是一個輕量級的例項,用來對拖動進行一些應用程式相關的描述。ClipData只會用於拖動檢視放下事件的監聽器,而本地狀態對於所有的監聽器都是可訪問的(任何時刻呼叫DragEvent的getLocalState()方法即可)。
在拖放過程中發生的每個特定事件都會呼叫onDragListener.onDrag()方法,同時傳回一個描述每個事件特徵的DragEvent。每個DragEvent都具有 以下動作中的一個:
- ACTION_DRAG_STARTED :當呼叫startDrag()以開始一個新的拖動事件時會向所有檢視傳送該動作。
位置資訊可以通過getX()和getY() 。 - ACTION_DRAG_ENTERED :
- ACTION_DRAG_EXITED :
- ACTION_DRAG_LOCATION :
- ACTION_DRAG_DROP :
- ACTION_DRAG_ENED :
這個方法和自定義觸控事件的工作方式類似,即監聽器返回的值決定了後續的事件傳遞。如果某個特殊的OnDragListener並沒有對ACTION_DRAG_STARTED動作返回true,那麼除了ACTION_DRAG_ENDED以外,它將不會收到拖動過程中任何後續的事件。
16.3 實現機制
讓我們看一個拖放功能的實現,首先是採用以下程式碼清單。這裡我們建立了一個自定義的ImageView,它實現了onDragListener介面。
自定義檢視實現了OnDragListener
public class DropTargetView extends ImageView implements OnDragListener { private boolean mDropped; public DropTargetView(Context context) { super(context); init(); } public DropTargetView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public DropTargetView(Context context, AttributeSet attrs, int defaultStyle) { super(context, attrs, defaultStyle); init(); } private void init() { //我們必須設定一個有效的監聽器來接收DragEvent setOnDragListener(this); } @Override public boolean onDrag(android.view.View v, DragEvent event) { PropertyValuesHolder pvhX, pvhY; switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: //React to a new drag by shrinking the view pvhX = PropertyValuesHolder.ofFloat("scaleX", 0.5f); pvhY = PropertyValuesHolder.ofFloat("scaleY", 0.5f); ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY).start(); //Clear the current drop image on a new event setImageDrawable(null); mDropped = false; break; case DragEvent.ACTION_DRAG_ENDED: // React to a drag ending by resetting the view size // if we weren't the drop target. if (!mDropped) { pvhX = PropertyValuesHolder.ofFloat("scaleX", 1f); pvhY = PropertyValuesHolder.ofFloat("scaleY", 1f); ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY).start(); mDropped = false; } break; case DragEvent.ACTION_DRAG_ENTERED: //React to a drag entering this view by growing slightly pvhX = PropertyValuesHolder.ofFloat("scaleX", 0.75f); pvhY = PropertyValuesHolder.ofFloat("scaleY", 0.75f); ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY).start(); break; case DragEvent.ACTION_DRAG_EXITED: //React to a drag leaving this view by returning to previous size pvhX = PropertyValuesHolder.ofFloat("scaleX", 0.5f); pvhY = PropertyValuesHolder.ofFloat("scaleY", 0.5f); ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY).start(); break; case DragEvent.ACTION_DROP: // React to a drop event with a short animation keyframe animation // and setting this view's image to the drawable passed along with // the drag event // This animation shrinks the view briefly down to nothing // and then back. Keyframe frame0 = Keyframe.ofFloat(0f, 0.75f); Keyframe frame1 = Keyframe.ofFloat(0.5f, 0f); Keyframe frame2 = Keyframe.ofFloat(1f, 0.75f); pvhX = PropertyValuesHolder.ofKeyframe("scaleX", frame0, frame1, frame2); pvhY = PropertyValuesHolder.ofKeyframe("scaleY", frame0, frame1, frame2); ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY).start(); //Set our image from the Object passed with the DragEvent setImageDrawable((Drawable) event.getLocalState()); //We set the dropped flag to the ENDED animation will not also run mDropped = true; break; default: //Ignore events we aren't interested in return false; } //Declare interest in all events we have noted return true; } }
這個Image View用來監控新產生的拖動事件並自己執行相應的動畫。每次新的拖動行為出現時,ACTION_DRAG_STARTED事件就會被髮送到這裡,這個ImageView就會自己縮小50%。這對使用者是一個非常好的引導,可以告訴使用者他們剛剛選擇的檢視可以拖動到哪裡。這裡我們還確保這個監聽器會對此事件返回true,這樣就可以接收拖動過程中的其他事件了。
如果使用者將他們的檢視拖動到這個ImageView上,這就會ACTION_DRAG_ENTERED,這時ImageView會稍微放大一下,表示該ImageView可以接收的檢視放置行為。當檢視被拖離時會觸發ACTION_DRAG_EXITED事件,這時ImageView會恢復到剛進入“拖放模式”時的大小。如果使用者在該ImageView的上方鬆手,會觸發ACTION_DROP事件,同時會進行一段特殊的動畫表示放置動作已經收到。這時我們會讀取事件中的本地狀態變數,如果是一個Drawable,就把它設定為ImageView的圖片內容。
ACTION_DRAG_ENDED會通知該ImageView恢復到之前的大小,因為此時已經不再處於“拖動模式”了。但是,如果這個ImageView就是放置的目標,我們希望保持它的大小,因此這種情況下會忽略掉這個事件。
以下兩段程式碼顯示了一個示例Activity,該Activity允許使用者長按一個圖片,然後可以拖動該圖片到我們自定義的放置目標上。
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 頂部一行是可拖放的條目 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/image1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:src="@drawable/ic_send" /> <ImageView android:id="@+id/image2" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:src="@drawable/ic_share" /> <ImageView android:id="@+id/image3" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:src="@drawable/ic_favorite" /> </LinearLayout> <!-- 底部一行是可放置的目標 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:orientation="horizontal" > <com.examples.dragtouch.DropTargetView android:id="@+id/drag_target1" android:layout_width="0dp" android:layout_height="100dp" android:layout_weight="1" android:background="#A00" /> <com.examples.dragtouch.DropTargetView android:id="@+id/drag_target2" android:layout_width="0dp" android:layout_height="100dp" android:layout_weight="1" android:background="#0A0" /> <com.examples.dragtouch.DropTargetView android:id="@+id/drag_target3" android:layout_width="0dp" android:layout_height="100dp" android:layout_weight="1" android:background="#00A" /> </LinearLayout> </RelativeLayout>
轉發觸控事件的Activity
public class DragTouchActivity extends Activity implements OnLongClickListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //為每個ImageView關聯長按監聽器 findViewById(R.id.image1).setOnLongClickListener(this); findViewById(R.id.image2).setOnLongClickListener(this); findViewById(R.id.image3).setOnLongClickListener(this); } @Override public boolean onLongClick(View v) { DragShadowBuilder shadowBuilder = new DragShadowBuilder(v); //開始拖動,將View的圖片作為本地狀態傳遞出去 v.startDrag(null, shadowBuilder, ((ImageView) v).getDrawable(), 0); return true; } }
這個示例會在螢幕的頂部顯示一行三張圖片,同時在螢幕的底部顯示三個我們自定義的目標檢視。為每張圖片都設定了一個長按事件監聽器,長按動作會通過startDrag()觸發一個新的拖動事件。拖動事件初始化時傳入的DragShadowBuilder,但本節中檢視在拖動時會建立一個透明的副本並顯示在觸控點的正下方。
我們還通過getDrawable()得到了使用者選擇的檢視的圖片內容並把它作為拖動的本地狀態傳遞出去,自定義的放置目標會使用它來設定圖片。這樣就會產生檢視已經放置到目標上的效果。參見下圖,檢視載入時的效果、拖動操作過程時的效果以及圖片被放置到某個放置目標後的效果。

Drag示例拖動前的效果

檢視放置後的效果
自定義DragShadowBuilder
DragShadowBuilder的預設實現非常方便。但有可能並不是應用程式需要的。讓我們看一下以下程式碼,它實現了自定義的DragShadowBuilder。
自定義DragShadowBuilder
public class DrawableDragShadowBuilder extends DragShadowBuilder { private Drawable mDrawable; public DrawableDragShadowBuilder(View view, Drawable drawable) { super(view); // 設定Drawable並使用一個綠色的過濾器 mDrawable = drawable; mDrawable.setColorFilter(new PorterDuffColorFilter(Color.GREEN, PorterDuff.Mode.MULTIPLY)); } @Override public void onProvideShadowMetrics(Point shadowSize, Point touchPoint) { // 填充大小 shadowSize.x = mDrawable.getIntrinsicWidth(); shadowSize.y = mDrawable.getIntrinsicHeight(); // 設定陰影相對於觸控點的位置 // 這裡陰影位於手指下方的中心 touchPoint.x = mDrawable.getIntrinsicWidth() / 2; touchPoint.y = mDrawable.getIntrinsicHeight() / 2; mDrawable.setBounds(new Rect(0, 0, shadowSize.x, shadowSize.y)); } @Override public void onDrawShadow(Canvas canvas) { //在提供的 Canvas上繪製陰影檢視 mDrawable.draw(canvas); } }
此自定義實現會使用一個單獨的Drawable引數,陰影的顯示會使用該圖片而不是使用源檢視的可見副本。另外,我們還對該圖片使用了綠色的ColorFilter來增加一些效果。DragShadowBuilder是一個非常容易擴充套件的類,只需要有效地覆寫兩個主要的方法。
第一個方法是onProviderShadowMetrics(),它會在DragShadowBuilder初始化時呼叫一次並使用兩個Point物件填充DragShadowBuilder的內容。首先會填充陰影使用的圖片的大小,即會將期望的寬度設定為x值,將期望的高度設定為y值。本例中會將該大小設定為圖片本來的寬度和高度。另外需要填充的陰影期望觸控的位置。這裡會定義陰影圖片相對於使用者手指的位置,例如將x和y都設定為0時,手指會位於圖片的左上角。在我們的示例中,我們設定到了圖片的中心點,因此手指會位於圖片中心上方。
第二個方法是onDrawShadow(),它會被重複呼叫以渲染陰影圖片。這個方法中傳入的Canvas是由框架根據onProviderShadowMetric()中包含的資訊建立的。這裡你可以像其他自定義檢視一樣進行各種自定義繪製。我們的示例只是簡單地告訴Drawable在Canvas上繪製它自己。