對android開發有一定瞭解的同學一定或多或少知道android的觸摸事件分發,整個事件的分發消耗流程都可以通過看源碼理解,下麵通過講解demo幫助加深事件分發的理解和在實戰中的應用。首先直接上demo截圖: ...
結合支付寶和微信首頁鞏固android事件分發機制
文章出處大黑的博客--事件分發
源碼地址 https://github.com/halibobo/TouchListenerConflict 歡迎star
android的事件分發和處理方式
對android開發有一定瞭解的同學一定或多或少知道android的觸摸事件分發,整個事件的分發消耗流程都可以通過看源碼理解,下麵通過講解demo幫助加深事件分發的理解和在實戰中的應用。首先直接上demo截圖:
demo佈局
整個首頁佈局是這樣的,最外層是ViewPager,裡面包含四個子功能,每個子功能的視圖都是一個Fragment。“功能1”里的列表項是一個GridView,此gridview外層是帶有LinearLayout的ScrollView。
佈局如下
難點分析和講解
一、GridView高度問題
二、長按GridView某一項可以替換位置,最後一項“更多”不參與滑動與替換位置
三、GridView長按並滑動某一項時,滑動過程中與ScrollView和ViewPager衝突問題
四、滑動GridView中某一項時,當滑動到頂部時ScrollView要能向下滾動;手指滑動到底部時要能項上滾動
解決問題
解決問題一
為瞭解決這一系列問題,我需要重寫DragGridView繼承GridView,第一步需要讓DragGridView的每一項item占滿整個視圖,而gridview預設是限定高度的,解決辦法是重寫onMeasure,修改測量高度方法,如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = View.MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);//1.精確模式(MeasureSpec.EXACTLY) 2.最大模式(MeasureSpec.AT_MOST) 3.未指定模式(MeasureSpec.UNSPECIFIED)
super.onMeasure(widthMeasureSpec, expandSpec);
}
解決問題二
現在我們需要長按GridView某一項可以替換位置,思路是首先需要捕捉到長按事件,那麼怎麼算長按事件呢?手指快速的從item上劃是不算的,手指按下然後很快離開這是點擊事件也不能算長按。只有手指在item上按下並且停留在item上一定時間才能算長按。有了思路,下麵看關鍵的幾處代碼:
private Handler mHandler = new Handler();
//用來處理是否為長按的Runnable
private Runnable mLongClickRunnable = new Runnable() {
@Override
public void run() {
//todo
}
};
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
//使用Handler延遲dragResponseMS執行mLongClickRunnable
mHandler.postDelayed(mLongClickRunnable, dragResponseMS);
break;
case MotionEvent.ACTION_MOVE:
//如果我們在按下的item上面移動,只要不超過item的邊界我們就不移除mRunnable
if(!isTouchInItem(mStartDragItemView, moveX, moveY)){
mHandler.removeCallbacks(mLongClickRunnable);
mHandler.removeCallbacks(mScrollRunnable);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mHandler.removeCallbacks(mLongClickRunnable);
break;
}
return super.dispatchTouchEvent(ev);
}
解析: 定義一個Handler,重寫ViewGroup(這裡即DragGridView)分發事件方法dispatchTouchEvent(),在接受到ACTION DOWN事件時handler向消息隊列會延遲發送一條消息,延遲時間就是長按時間的閥值,當接受到消息時說明長按事件已成立,如果手指在消息延遲期間滑走或移出則取消消息。
有了長按事件如何滑動某一條item呢?如何替換兩個item位置呢?
這裡的解決思路是,當其中的某項假設是item1接收到長按事件後,先隱藏item1,在item1的位置上新建一個和item1長相位置都一樣view假設叫它litem,手指滑動時對litem跟隨滑動,接著重點來了,當手指滑動到item2區域後發出一個替換事件。這樣我們就成功將兩個item替換了位置並且用戶體驗比較好,代碼片段如下:
//用來處理是否為長按的Runnable
private Runnable mLongClickRunnable = new Runnable() {
@Override
public void run() {
if (onDragStartListener != null) {
onDragStartListener.onDragStart();
}
isDrag = true; //設置可以拖拽
mVibrator.vibrate(50); //震動一下
mStartDragItemView.setVisibility(View.INVISIBLE);//隱藏該item
//根據我們按下的點顯示item鏡像
createDragImage(mDragBitmap, mDownX, mDownY);
}
};
/**
* 設置替換回調介面
* @param onChangeListener
*/
public void setOnChangeListener(OnChangeListener onChangeListener){
this.onChangeListener = onChangeListener;
}
/**
* 創建拖動的鏡像
* @param bitmap
* @param downX
* 按下的點相對父控制項的X坐標
* @param downY
* 按下的點相對父控制項的X坐標
*/
private void createDragImage(Bitmap bitmap, int downX , int downY){
mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; //圖片之外的其他地方透明
mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowLayoutParams.alpha = 0.55f; //透明度
mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE ;
mDragImageView = new ImageView(getContext());
mDragImageView.setImageBitmap(bitmap);
mWindowManager.addView(mDragImageView, mWindowLayoutParams);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(isDrag && mDragImageView != null){
switch(ev.getAction()){
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
moveY = (int) ev.getY();
moveRawY = (int) ev.getRawY();
//拖動item
onDragItem(moveX, moveY);
break;
case MotionEvent.ACTION_UP:
onStopDrag();
isDrag = false;
break;
}
return true;
}
return super.onTouchEvent(ev);
}
解析:在接收到長按事件後會調用 createDragImage(mDragBitmap, mDownX, mDownY)方法,這是為了在item的位置上創建一個長相一樣的view,當手指滑動時在onTouch()方法中監聽move事件處理view的位置
解決問題三
為題三的解決比較簡單,當gridview接收到長按事件後,處理ViewPager和ScrollView的方法一樣requestDisallowInterceptTouchEvent(false);
這個方法是讓ViewGroup本身不攔截觸摸事件。當gridView滑動結束在requestDisallowInterceptTouchEvent(true);
解決問題四
直接上源碼
public class ControlScrollView extends ScrollView {
private boolean isInControl = true;
private int moveSpeed = 5;
private final int msgWhat = 1;
private final int time = 20;
private ScrollState scrollState;
public ControlScrollView(Context context) {
super(context);
init();
}
public ControlScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:/**/
if (!isInControl) {
if (ev.getY() < 0) {
if (!myHandler.hasMessages(msgWhat)) {
Message msg = new Message();
msg.arg1 = -1;
msg.what = msgWhat;
myHandler.sendMessageDelayed(msg, time);
}
return super.dispatchTouchEvent(ev);
} else if (ev.getY() > getHeight()) {
if (!myHandler.hasMessages(msgWhat)) {
Message msg = new Message();
msg.arg1 = 1;
msg.what = msgWhat;
myHandler.sendMessageDelayed(msg, time);
}
return super.dispatchTouchEvent(ev);
} else {
myHandler.removeMessages(msgWhat);
}
} else {
myHandler.removeMessages(msgWhat);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (scrollState != null) {
scrollState.stopTouch();
}
myHandler.removeMessages(msgWhat);
requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
myHandler.removeMessages(msgWhat);
}
private void init() {
moveSpeed = ScreenUtils.dip2px(getContext(), moveSpeed);
}
public boolean isInControl() {
return isInControl;
}
public ScrollState getScrollState() {
return scrollState;
}
public void setScrollState(ScrollState scrollState) {
this.scrollState = scrollState;
}
public void setInControl(boolean inControl) {
isInControl = inControl;
}
private Handler myHandler = new Handler() {
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
smoothScrollBy(0, moveSpeed * (msg.arg1 > 0 ? 1 : -1));
Message msg1 = new Message();
msg1.what = msg.what;
msg1.arg1 = msg.arg1;
myHandler.sendMessageDelayed(msg1, time);
}
};
public interface ScrollState {
void stopTouch();
}
}
解析:大致邏輯就是判斷手指位置,超出邊界發送消息對scrollview進行滾動