1. 程式人生 > >自定義View系列教程08--滑動衝突的產生及其處理

自定義View系列教程08--滑動衝突的產生及其處理

PS:如果覺得文章太長,那就直接看視訊

在之前的幾篇文章中,我們已經分析了View對於Touch的處理以及ViewGroup對於Touch事件的分發。
但在開發中時常遇到一個棘手的問題:Touch事件的滑動衝突。比如ListView巢狀ScrollView,ViewPager巢狀ScrollView,ListView巢狀ScrollView時常常發生。

這些滑動衝突的產生,一般而言都具有以下特點:

  1. 子View和父View都有滑動的需求
  2. 滑動事件不能準確地傳遞給合適的View

其實,Google官方不鼓勵這種滑動巢狀的設計;但是在實際專案中卻會碰到客戶提出類似的要求。既然不太合理的東西已經存在了,開發人員再去抱怨設計或者客戶都沒有實際作用了,只有想辦法完成對應的功能。

那麼,有哪些方法可以解決滑動衝突呢?

  1. 子View禁止父View攔截Touch事件
    在分析ViewGroup的dispatchTouchEvent()原始碼時,我們知道:Touch事件是由父View分發的。如果一個Touch事件是子View需要的,但是被其父View攔截了,子View就無法處理該Touch事件了。在此情形下,子View可以呼叫requestDisallowInterceptTouchEvent( )禁止父View對Touch的攔截

  2. 在父View中準確地進行事件分發和攔截
    我們可以重寫父View中與Touch事件分發相關的方法,比如onInterceptTouchEvent( )。這些方法中摒棄系統預設的流程,結合自身的業務邏輯重寫該部分程式碼,從而使父View放行子View需要的Touch。

在此,通過一個示例展示解決滑動衝突的常用方法。

示例效果,請看下圖:

這裡寫圖片描述

它的佈局檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context
="com.stay4it.testtouch3.MainActivity">
<HorizontalScrollView android:id="@+id/horizontalScrollView" android:layout_height="450dp" android:layout_width="match_parent" android:scrollbars="horizontal" android:fillViewport="true"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" /> </HorizontalScrollView> </RelativeLayout>

在該示例中,我們在HorizontalScrollView中嵌入ViewPager瀏覽圖片。由於HorizontalScrollView和ViewPager都可以響應水平滑動,當我們用手指切換圖片時發現ViewPager無法正常的滾動。這是因為Touch事件被HorizontalScrollView處理,根本沒有傳遞給ViewPager。

嗯哼,滑動衝突的原因已經清楚了,我們現在來解決這個問題。

第一種解決方法

在ViewPager中禁止HorizontalScrollView攔截Touch事件

package com.stay4it.testtouch3;

import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
/**
 * 原創作者
 * 谷哥的小弟
 *
 * 部落格地址
 * http://blog.csdn.net/lfdfhl
 */
public class MainActivity extends AppCompatActivity {
    private ViewPager mViewPager;
    private ViewPagerAdapter mViewPagerAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init(){
        mViewPager = (ViewPager) findViewById(R.id.viewPager);
        mViewPagerAdapter=new ViewPagerAdapter(this);
        mViewPager.setAdapter(mViewPagerAdapter);
        mViewPager.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                mViewPager.getParent().requestDisallowInterceptTouchEvent(true);
                return false;
            }
        });
    }
}

在此,請關注第32行程式碼

mViewPager.getParent().requestDisallowInterceptTouchEvent(true);

此處首先利用 mViewPager.getParent( )得到了ViewPager的父View即HorizontalScrollView;然後再執行requestDisallowInterceptTouchEvent( )方法從而禁止掉父View對於Touch事件的傳遞。

第二種解決方法

在自定義HorizontalScrollView中合理地處理Touch事件的攔截和分發

package com.stay4it.testtouch4;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
/**
 * 原創作者
 * 谷哥的小弟
 *
 * 部落格地址
 * http://blog.csdn.net/lfdfhl
 */
public class HorizontalScrollViewSubclass extends HorizontalScrollView{
    private float LastX;
    private float LastY;
    private float distanceX;
    private float distanceY;
    public HorizontalScrollViewSubclass(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                distanceX = 0f;
                distanceY = 0f;
                LastX = ev.getX();
                LastY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:

                final float currentX = ev.getX();
                final float currentY = ev.getY();

                distanceX += Math.abs(currentX - LastX);
                distanceY += Math.abs(currentY - LastY);

                LastX = currentX;
                LastY = currentY;

                if(distanceX > distanceY){
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }
}

從這段程式碼中,我們的主要操作是在自定義HorizontalScrollView中重寫了onInterceptTouchEvent( )。
在該方法中如果判定Touch是水平的滑動:

if(distanceX > distanceY)

那麼直接返回false不攔截該Touch。這樣Touch事件就可以順利傳遞到其子View即ViewPager中,從而正常地切換圖片。

既然已經採用了自定義的HorizontalScrollView就不要忘記修改佈局檔案喔~

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context="com.stay4it.testtouch4.MainActivity">

    <com.stay4it.testtouch4.HorizontalScrollViewSubclass
        android:id="@+id/horizontalScrollView"
        android:layout_height="450dp"
        android:layout_width="match_parent"
        android:scrollbars="horizontal"
        android:fillViewport="true"
      >

        <android.support.v4.view.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.stay4it.testtouch4.HorizontalScrollViewSubclass>

</RelativeLayout>

嗯哼,在此通過一個完整的示例展示了事件滑動衝突的常用解決方式。
在實際的專案開發中所遇到的滑動衝突可能比這要複雜,但是處理衝突的基本原理和方式是相同的。

至此,自定義View系列教程就全部結束了

多謝大家的指正和鼓勵

Thank you very much

PS:如果覺得文章太長,那就直接看視訊