源碼地址
基本思路
我們先考慮簡(jiǎn)單的情況父虑,兩個(gè)控件之間的圖片拖拽母赵,首先我們需要準(zhǔn)備ImageViewA和ImageViewB兩個(gè)ImageView,然后在里面設(shè)置圖片逸爵。接著我們需要考慮拖拽事件的觸發(fā)條件,這里假設(shè)為手指從ImageView的某個(gè)邊緣滑出一段距離即觸發(fā)拖拽事件
假設(shè)我們此時(shí)在ImageViewA的邊緣向下滑動(dòng)了一段距離凹嘲,在觸發(fā)拖拽事件的時(shí)候我們需要將ImageViewA的圖片隱藏,然后將A的圖片傳遞給一個(gè)半透明的ImageViewC并將C顯示出來(lái)师倔,由于這個(gè)ImageViewC同一時(shí)間只會(huì)有一個(gè),所以我們可以在自定義的layout中創(chuàng)建一個(gè)ImageView類型的成員變量進(jìn)行復(fù)用周蹭,在觸發(fā)拖拽事件的時(shí)候ImageViewC會(huì)跟隨手指滑動(dòng)趋艘,在手指抬起來(lái)的時(shí)候判斷ImageViewC的中心是否在另一個(gè)ImageViewB上疲恢,若在則交換A和B的圖片
整體思路還是比較清晰的,主要是要?jiǎng)?chuàng)建一個(gè)自定義的layout對(duì)子View進(jìn)行管理并自定義事件分發(fā)規(guī)則
代碼實(shí)現(xiàn)
自定義Layout
此處選擇繼承自FrameLayout致稀,因?yàn)榭梢酝ㄟ^(guò)margin屬性自由控制View所在的位置并且不會(huì)影響到其他的View冈闭,后期可能會(huì)向Layout中添加一些EditText、TextView或者各種自定義View抖单,而這些View可能是要疊在ImageView上面的萎攒,因此選擇繼承自FrameLayout無(wú)疑是最合適的,而且它已經(jīng)幫我們處理了很多細(xì)節(jié)矛绘,我們可以專注于功能的實(shí)現(xiàn)
獲取子View所在的區(qū)域信息
因?yàn)閷?duì)事件進(jìn)行分發(fā)需要子View的位置信息耍休,所以我們需要一個(gè)成員變量來(lái)保存,此處選擇RectF來(lái)保存一個(gè)View所在的區(qū)域货矮,然后將所有View的位置信息存放到一個(gè)HashMap中羊精,這樣就可以處理兩個(gè)以上的控件間圖片拖拽了
public class ConfigurableFrameLayout extends FrameLayout implements OnTriggerDragListener {
...
// 保存所有子View的區(qū)域
private HashMap<View, RectF> mChildViewRects;
...
@Override
protected void onLayout(boolean changed,int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right,bottom);
/* 因?yàn)閘ayout后每個(gè)子View的位置才確定,所以在此處初始化子View的位置信息*/
initChildViewRect();
}
/**
* 獲取各子View所在區(qū)域
*/
private void initChildViewRect() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
// 避免多次創(chuàng)建對(duì)象
RectF rect = mChildViewRects.get(child;
if (rect == null) {
rect = new RectF();
}
// 設(shè)置子View所在的矩形區(qū)域
rect.set(lp.leftMargin,lp.topMargin,
lp.leftMargin + child.getWidt(),
lp.topMargin +child.getHeight());
mChildViewRects.put(child, rect);
}
}
}
對(duì)事件進(jìn)行分發(fā)
子View有可能比較小囚玫,如果要兩根手指都在子View里面才能對(duì)圖片進(jìn)行操作會(huì)不太方便喧锦,而我們要實(shí)現(xiàn)只要一根手指在子View內(nèi),另一根手指無(wú)論在哪都可以對(duì)子View的圖片進(jìn)行操作抓督,并且同一時(shí)間只能操作一個(gè)子View燃少,此時(shí)就需要自定義事件分發(fā)規(guī)則
自定義事件分發(fā)規(guī)則可以選擇重寫(xiě)dispatchTouchEvent()方法,但是需要考慮的細(xì)節(jié)比較多铃在,所以我們選擇攔截所有事件阵具,然后在onTouchEvent()方法中對(duì)事件進(jìn)行分發(fā)
//重寫(xiě)自定義Layout的onInterceptTouchEvent()和onTouchEvent()
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
super.onInterceptTouchEvent(ev);
// 攔截所有事件
return true;
}
private View mCurrentChildView; // 當(dāng)前正在處理觸摸事件的子View
@Override
public boolean onTouchEvent(MotionEventevent) {
// 事件是否被子View消費(fèi)
boolean handled = false;
// 當(dāng)前事件流若已被分發(fā)給某個(gè)子View處理,則將后續(xù)事件都分發(fā)給該子View
if (mCurrentChildView != null) {
handled = dispatchTouchEventToChild(event, mCurrentChildView);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 獲取位于觸點(diǎn)的子View
mCurrentChildView = viewInXY(event.getX(), event.getY());
// 判斷子View是否可以觸發(fā)拖拽事件定铜,可以則為其設(shè)置觸發(fā)時(shí)的監(jiān)聽(tīng)事件
if (mCurrentChildView instanceof TriggerDraggable) {
((TriggerDraggable) mCurrentChildView).setOnTriggerDragListener(this);
}
// 只在ACTION_DOWN時(shí)對(duì)事件進(jìn)行分發(fā)阳液,事件只能交由一個(gè)子View處理
if (mCurrentChildView != null) {
handled =dispatchTouchEventToChild(event, mCurrentChildView);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 設(shè)置當(dāng)前處理事件流的子View為null
mCurrentChildView = null;
break;
}
if (!handled) handled = super.onTouchEvent(event);
return handled;
}
/**
* 獲取位于指定坐標(biāo)的子View
* @param x x坐標(biāo)
* @param y y坐標(biāo)
* @return 包含該坐標(biāo)的子View,若找不到則返回null
*/
@Nullable
private View viewInXY(float x, float y) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
RectF rectF = mChildViewRects.get(child);
if (rectF != null && rectF.contains(x, y)) {
return child;
}
}
return null;
}
/**
* 將觸摸事件坐標(biāo)變換后傳遞給子View
* @return true如果事件被子View消費(fèi)揣炕,否則返回false
*/
private boolean dispatchTouchEventToChild(MotionEvent event, View child) {
final float offsetX = getScrollX() - child.getLeft();
final float offsetY = getScrollY() - child.getTop();
event.offsetLocation(offsetX, offsetY);
boolean handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
return handled;
}
判斷是否觸發(fā)拖拽事件
現(xiàn)在子View已經(jīng)可以接收到觸摸事件了帘皿,然后我們需要判斷是否觸發(fā)了拖拽事件,我們要實(shí)現(xiàn)的效果是可以指定View的上下左右的某個(gè)或某幾個(gè)邊界畸陡,當(dāng)超過(guò)指定的邊界指定的距離后即觸發(fā)拖拽事件矮烹,觸發(fā)后隱藏觸發(fā)條件的ImageViewA(此處假設(shè)在ImageViewA上觸發(fā))并將半透明的ImageViewC顯示出來(lái)
這邊需要考慮將判斷邏輯寫(xiě)在ImageView中還是Layout中,當(dāng)然罩锐,寫(xiě)在哪里都可以實(shí)現(xiàn)功能奉狈,但是設(shè)計(jì)上可能不夠合理,由于拖拽事件是在操作子View圖片的過(guò)程中觸發(fā)的涩惑,所以個(gè)人認(rèn)為在子View中對(duì)是否觸發(fā)進(jìn)行判斷比較合理仁期。
此處使用了觀察者模式,將判斷邏輯寫(xiě)在子View中,然后在觸發(fā)拖拽事件的時(shí)候通過(guò)監(jiān)聽(tīng)器通知Layout做相應(yīng)的操作
為子View設(shè)置監(jiān)聽(tīng)器的代碼可以參考上文對(duì)事件進(jìn)行分發(fā)中的OnTouchEvent()中ACTION_DOWN下面的代碼
//用于解耦ConfigurableFrameLayout與DraggableImageView的接口
/**
* 實(shí)現(xiàn)該接口表示可觸發(fā)拖拽事件
*/
public interface TriggerDraggable {
// 判斷是否觸發(fā)拖拽事件
boolean triggerDrag(MotionEvent event);
// 設(shè)置拖拽事件監(jiān)聽(tīng)器
void setOnTriggerDragListener(OnTriggerDragListener listener);
}
/**
* 拖拽事件監(jiān)聽(tīng)器
*/
public interface OnTriggerDragListener {
// 在拖拽的時(shí)候調(diào)用
void onDrag(MotionEvent event);
// 拖拽事件結(jié)束時(shí)調(diào)用
void onDragFinish(MotionEvent event);
}
在子View中判斷是否觸發(fā)拖拽事件跛蛋,這邊我沒(méi)有直接使用ImageView,而是繼承了我之前寫(xiě)的一個(gè)自定義ImageView熬的,主要是比普通的ImageView多了手勢(shì)操作圖片旋轉(zhuǎn)、平移赊级、縮放三個(gè)功能押框。(ImageViewC只需要展示圖片,所以使用的還是普通的ImageView)
public class DraggableImageView extends TransformativeImageView implements TriggerDraggable {
...
/**
* 可觸發(fā)拖拽事件的邊界,若數(shù)組某個(gè)index的變量為true理逊,則表示該index對(duì)應(yīng)的邊界可以觸發(fā)拖拽事件橡伞;
* 默認(rèn)所有邊界均不可觸發(fā)拖拽事件
* index: {0, 1, 2, 3} -> boundary: {left, top, right, bottom}
*
* 例:mBoundary = {true, false, false, true} 表示左邊界與下邊界可觸發(fā)拖拽事件
*/
private boolean[] mBoundary = new boolean[4];
private float mTriggerDistance = DEFAULT_TRIGGER_DISTANCE; // 觸發(fā)拖拉事件的距離
/**
* 判斷是否觸發(fā)拖拽事件
* @param event 觸摸事件
* @return 符合觸發(fā)條件則返回true,否則返回false
*/
@Override
public boolean triggerDrag(MotionEvent event) {
boolean canDrag = false;
// 當(dāng)前觸點(diǎn)坐標(biāo)
final float x = event.getX();
final float y = event.getY();
// 判斷某個(gè)邊界是否可觸發(fā)拖拽事件并且達(dá)到了觸發(fā)條件
if (mBoundary[0] && -x > mTriggerDistance
|| mBoundary[1] && -y > mTriggerDistance
|| mBoundary[2] && x - getWidth() > mTriggerDistance
|| mBoundary[3] && y - getHeight() > mTriggerDistance) {
canDrag = true;
}
return canDrag;
}
private boolean isPointerCountChanged = false; // 本次觸摸事件流中觸點(diǎn)數(shù)量是否減少
private boolean mCanDrag = false; // 是否可以將圖片拖拽出控件
private OnTriggerDragListener mOnTriggerDragListener; // 拖拽事件監(jiān)聽(tīng)器
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
/* 只有處于不可拖拽狀態(tài)時(shí)才判斷是否觸發(fā)拖拽事件晋被,
* 且本次觸摸事件流觸點(diǎn)數(shù)量未減少的情況兑徘,才判斷是否觸發(fā)拖拽事件
*/
if (!mCanDrag && !isPointerCountChanged && triggerDrag(event)) {
mCanDrag = true;
}
// 調(diào)用拖拽監(jiān)聽(tīng)方法
if (mCanDrag && mOnTriggerDragListener != null) {
mOnTriggerDragListener.onDrag(event);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 拖拽事件結(jié)束
if (mCanDrag && mOnTriggerDragListener != null) {
mOnTriggerDragListener.onDragFinish(event);
}
// 清除是否可拖拽的標(biāo)志位
mCanDrag = false;
// ACTION_UP意味著本次事件流結(jié)束,所以將記錄觸點(diǎn)數(shù)量是否減少的標(biāo)志位清除
isPointerCountChanged = false;
break;
case MotionEvent.ACTION_POINTER_UP:
// 觸點(diǎn)數(shù)量減少
isPointerCountChanged = true;
break;
} return true;
}
...
}
觸發(fā)拖拽事件后執(zhí)行的操作
觸發(fā)拖拽事件后我們需要一個(gè)半透明的ImageViewC(代碼中為mInterpolationImageView)來(lái)存放觸發(fā)拖拽的子View中的圖片
//在自定義Layout中創(chuàng)建并初始化ImageViewC
private void initInterpolationView() {
mInterpolationImageView = new ImageView(getContext());
// TODO dp轉(zhuǎn)px, 大小羡洛,透明度為可配置
LayoutParams lp = new LayoutParams(300, 300);
mInterpolationImageView.setLayoutParams(lp);
mInterpolationImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
mInterpolationImageView.setAlpha(0.5f);
mInterpolationImageView.setVisibility(GONE);
addView(mInterpolationImageView);
}
觸發(fā)拖拽后挂脑,觸發(fā)事件的控件的圖片會(huì)隱藏,并且半透明的ImageViewC會(huì)跟隨手指移動(dòng)
public class ConfigurableFrameLayout extends FrameLayout implements OnTriggerDragListener {
...
// 判斷拖拽的ImageView是否已經(jīng)設(shè)置了當(dāng)前處理事件的子View的圖片
private boolean mInterpolationHasImg = false;
/**
* 拖拽ImageView,使之跟隨觸點(diǎn)位置移動(dòng)
* @param event 當(dāng)前觸摸事件
*/
private void dragInterpolationImageView(MotionEvent event) {
ImageView curImgView = null;
if (mCurrentChildView instanceof ImageView) {
curImgView = (ImageView) mCurrentChildView;
}
if (curImgView != null) {
// 為中間控件設(shè)置圖片
if (!mInterpolationHasImg
&& curImgView.getDrawable() instanceof BitmapDrawable) {
Drawable drawable = curImgView.getDrawable();
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
}
if (bitmap != null) {
mInterpolationImageView.setImageBitmap(bitmap);
}
mInterpolationHasImg = true;
}
// 隱藏控件的圖片
curImgView.setImageAlpha(0);
// 設(shè)置中間控件為可見(jiàn)
mInterpolationImageView.setVisibility(VISIBLE);
// 跟隨手指移動(dòng)中間控件
LayoutParams lp = (LayoutParams) mInterpolationImageView.getLayoutParams();
lp.setMargins((int)(event.getX() - mInterpolationImageView.getWidth() / 2),
(int)(event.getY() - mInterpolationImageView.getHeight() / 2),
0, 0);
// 將中間控件移到最上層
mInterpolationImageView.bringToFront();
}
}
...
@Override
public void onDrag(MotionEvent event) {
// 由于傳遞過(guò)來(lái)的事件是相對(duì)于子View的坐標(biāo)欲侮,所以需要變換為相對(duì)Layout的坐標(biāo)
final float offsetX = mCurrentChildView.getLeft() - getScrollX();
final float offsetY = mCurrentChildView.getTop() - getScrollY();
event.offsetLocation(offsetX, offsetY);
dragInterpolationImageView(event);
event.offsetLocation(-offsetX, -offsetY);
}
...
}
拖拽結(jié)束時(shí)判斷是否交換圖片
當(dāng)所有手指抬起崭闲,即拖拽事件結(jié)束后,判斷ImageViewC 的中心是否在另一個(gè)子View上威蕉,若在則交換兩者圖片
public class ConfigurableFrameLayout extends FrameLayout implements OnTriggerDragListener {
...
/**
* 判斷是否需要交換圖片刁俭,并清理一些標(biāo)志位憔狞,設(shè)置各子控件的最終狀態(tài)
* @param event 當(dāng)前觸摸事件
*/
private void dragFinish(MotionEvent event) {
ImageView curImgView = null;
if (mCurrentChildView instanceof ImageView) {
curImgView = (ImageView) mCurrentChildView;
}
if (curImgView != null) {
// 判斷當(dāng)前觸點(diǎn)是否在其他子View內(nèi),若是則交換兩者圖片
View v = viewInXY(event.getX(), event.getY());
if (v instanceof ImageView) {
exchangeImg(curImgView, (ImageView) v);
}
// 將之前拖拽過(guò)程隱藏當(dāng)前處理事件的子View的圖片顯示出來(lái)
curImgView.setImageAlpha(255);
// 隱藏中間控件
mInterpolationImageView.setVisibility(GONE);
// 設(shè)置中間控件不含當(dāng)前處理事件的子View的圖片
mInterpolationHasImg = false;
}
}
/**
* 交換兩個(gè)ImageView的圖片
* @param fromImgView 源控件
* @param toImgView 目標(biāo)控件
*/
private void exchangeImg(ImageView fromImgView, ImageView toImgView) {
// 若源控件不包含圖片則不交換
if (toImgView == null || fromImgView == null || fromImgView.getDrawable() == null) {
return;
}
Bitmap fromBmp = null;
Bitmap toBmp = null;
// 獲取源控件圖片
if (fromImgView.getDrawable() instanceof BitmapDrawable) {
fromBmp = ((BitmapDrawable) fromImgView.getDrawable()).getBitmap();
}
// 獲取目標(biāo)控件圖片
if (toImgView.getDrawable() instanceof BitmapDrawable) {
toBmp = ((BitmapDrawable) toImgView.getDrawable()).getBitmap();
}
// 交換兩者圖片
if (toBmp != null) fromImgView.setImageBitmap(toBmp);
if (fromBmp != null) toImgView.setImageBitmap(fromBmp);
}
...
@Override
public void onDragFinish(MotionEvent event) {
// 由于傳遞過(guò)來(lái)的事件是相對(duì)于子View的坐標(biāo)通今,所以需要變換為相對(duì)Layout的坐標(biāo)
final float offsetX = mCurrentChildView.getLeft() - getScrollX();
final float offsetY = mCurrentChildView.getTop() - getScrollY();
event.offsetLocation(offsetX, offsetY);
dragFinish(event);
event.offsetLocation(-offsetX, -offsetY);
}
}
使用
自定義Layout和ImageView寫(xiě)好后就大功告成了策治,只需要在xml文件中進(jìn)行配置,然后為ImageView設(shè)置圖片即可看到效果
// 下面兩個(gè)屬性為TrasmativeImageView中的自定義屬性
app:max_scale="4" 最大縮放比例為4倍
app:revert="false" 不開(kāi)啟回彈效果
// 下面的為DraggableImageView的自定義屬性
app:boundary_left="true" 左邊界可觸發(fā)拖拽
app:boundary_top="true" 上邊界可觸發(fā)拖拽
app:trigger_distance="3dp" 觸發(fā)拖拽的距離
<?xml version="1.0" encoding="utf-8"?>
<cn.lkllkllkl.configurableframelayout.ConfigurableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="cn.lkllkllkl.configurableframelayoutsample.MainActivity">
<cn.lkllkllkl.configurableframelayout.DraggableImageView
android:background="@color/gray"
android:id="@+id/draggable_image_view_1"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginLeft="90dp"
android:layout_marginTop="30dp"
app:max_scale="4"
app:revert="false"
app:boundary_bottom="true"
app:trigger_distance="100dp"
/>
<cn.lkllkllkl.configurableframelayout.DraggableImageView
android:background="@color/gray"
android:id="@+id/draggable_image_view_2"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="270dp"
android:layout_marginLeft="50dp"
app:revert="false"
app:max_scale="4"
app:boundary_top="true"
app:boundary_right="true"
app:trigger_distance="3dp"/>
<cn.lkllkllkl.configurableframelayout.DraggableImageView
android:background="@color/gray"
android:id="@+id/draggable_image_view_3"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="290dp"
android:layout_marginLeft="220dp"
app:max_scale="4"
app:revert="false"
app:boundary_left="true"
app:boundary_top="true"
app:trigger_distance="3dp"/>
</cn.lkllkllkl.configurableframelayout.ConfigurableFrameLayout>
/**
* Activity 代碼
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DraggableImageView draggableImageView1 =
(DraggableImageView) findViewById(R.id.draggable_image_view_1);
DraggableImageView draggableImageView2 =
(DraggableImageView) findViewById(R.id.draggable_image_view_2);
DraggableImageView draggableImageView3 =
(DraggableImageView) findViewById(R.id.draggable_image_view_3);
// 此處使用Glide將圖片載入View中
GlideApp.with(this)
.load(R.drawable.black)
.into(draggableImageView1);
GlideApp.with(this)
.load(R.drawable.cat)
.into(draggableImageView2);
GlideApp.with(this)
.load(R.drawable.cat)
.into(draggableImageView3);
}
}