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,修改測量高度方法,如下
Java代碼
- @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上一定時間才能算長按。有了思路,下面看關鍵的幾處代碼:
Java代碼
- 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替換了位置並且用戶體驗比較好,代碼片段如下:
Java代碼
- //用來處理是否為長按的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);
解決問題四
Java代碼
- 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進行滾動
結束
源碼地址 https://github.com/halibobo/TouchListenerConflict