1. 程式人生 > >14. 轉發觸控事件

14. 轉發觸控事件

14.1 問題

應用程式中的一些檢視或觸控目標非常小,導致手指很難準確地觸控到。

14.2 解決方案

(API Level 1)
使用TouchDelegate指定任意的矩形區域來向小檢視轉發觸控事件。TouchDelegate的設計宗旨就是為父ViewGroup關聯特定的區域,該區域偵測到觸控事件後會將該事件轉發給它的某個子檢視。TouchDelegate會發送每個事件到目標檢視,就像觸控目標檢視自己一樣。

實現機制

以下兩段程式碼清單演示瞭如何在自定義的父ViewGroup中使用TouchDelegate。
自定義父檢視實現了TouchDelegate

public class TouchDelegateLayout extends FrameLayout {

    public TouchDelegateLayout(Context context) {
        super(context);
        init(context);
    }

    public TouchDelegateLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TouchDelegateLayout(Context context, AttributeSet attrs, int defStyle){
        super(context, attrs, defStyle);
        init(context);
    }

    private CheckBox mButton;

    private void init(Context context) {
        // 建立一個很小的子檢視,我們要將觸控事件轉發給它
        mButton = new CheckBox(context);
        mButton.setText("Tap Anywhere");

        LayoutParams lp = new FrameLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
                Gravity.CENTER);
        addView(mButton, lp);
    }

    /*
     * TouchDelegate 會將該檢視(父檢視)的某個特定矩形區域,將
     *所有觸控事件轉發給CheckBox(子檢視)。這裡,矩形區域即為父檢視的全部大小
     * 
     * 這個過程必須在檢視確定了大小以後進行,這樣才能知道矩形應該有多大,
     *所以我們選擇在onSizeChanged()中新增代理區域
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if (w != oldw || h != oldh) {
            // 將該檢視的整個區域作為代理區域
            Rect bounds = new Rect(0, 0, w, h);
            TouchDelegate delegate = new TouchDelegate(bounds, mButton);
            setTouchDelegate(delegate);
        }
    }
}

示例Activity

public class DelegateActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TouchDelegateLayout layout = new TouchDelegateLayout(this);

        setContentView(layout);
    }
}

在這個示例中,我們建立了一個父檢視,其中包含了一個居中顯示的複選框。這個檢視還包含一個TouchDelegate,它會將父檢視區域內收到的觸控事件轉發給複選框。因為我們想讓父佈局的整個區域轉發觸控事件,所以會等到在檢視上呼叫onSizeChanged()後再構建和關聯TouchDelegate例項。如果在建構函式中,構建將不會生效,因為在執行建構函式時,檢視還沒有被測量,並且沒有可以讀取的尺寸大小。
Android框架會將沒有處理的觸控事件自動從TouchDelegate分發到它的代理檢視,因此無需額外程式碼即可轉發這些事件。在圖2-9中可以看到,應用程式在距離複選框很遠的地方收到觸控事件後,複選框會做相應的響應,如同它自己直接被觸摸了一樣。

自定義觸控轉發(遠端滾動條)

TouchDelegate非常適合於轉發觸控事件,但它有一個缺點,就是每個被轉發的事件轉發到代理檢視後都會定位到代理檢視的中間位置。這也意味著,如果想要通過TouchDelegate轉發一系列ACTION_MOVE事件的話,結果將不會如你所願,因為這時代理檢視會顯示手指並沒有移動過(每次都定位到同一個點上)。
如果想要以一種更加精確的方式重新路由觸控事件,可以通過手動地呼叫目標檢視的dispatchTouchEvent()方法來實現。參見以下兩段程式碼清單以瞭解相應的實現機制。
res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_touch"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:text="Scroll Anywhere Here" />

    <HorizontalScrollView
        android:id="@+id/scroll_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#CCC">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal" >
            <ImageView
                android:layout_width="250dp"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/ic_launcher" />
            <ImageView
                android:layout_width="250dp"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/ic_launcher" />
            <ImageView
                android:layout_width="250dp"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/ic_launcher" />
            <ImageView
                android:layout_width="250dp"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/ic_launcher" />
        </LinearLayout>
    </HorizontalScrollView>
</LinearLayout>

轉發觸控事件的Activity

public class RemoteScrollActivity extends Activity implements View.OnTouchListener {

    private TextView mTouchText;
    private HorizontalScrollView mScrollView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mTouchText = (TextView) findViewById(R.id.text_touch);
        mScrollView = (HorizontalScrollView) findViewById(R.id.scroll_view);
        //為頂層檢視關聯觸控事件的監聽器
        mTouchText.setOnTouchListener(this);
    }
    
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 如果需要的話,可以修改事件位置
        // 這裡我們將每個事件的垂直方向的位置都是相對於自己的座標
        //將Text View上的每個事件轉發到
        
        // 檢視需要的事件位置都是相對於自己的座標
        event.setLocation(event.getX(), mScrollView.getHeight() / 2);
        
       //將TextView上的每個事件轉發到HorizontalScrollView.
        mScrollView.dispatchTouchEvent(event);
        return true;
    }
}

這個示例將一個Activity一分為二。上半部分是一個TextView,它會提示你觸控並滑動它;而下半部分是一個內部包含若干張圖片的HorizontalScrollView。Activity為TextView設定一個OntouchListener,這樣就可以將他接收的所有觸控事件轉發給HorizontalScrollView。
我們希望觸控事件就像發生在(從HorizontalScrollView的角度)HorizontalScrollView自己的檢視內部一樣。所以在轉發事件之前,我們會呼叫setLocation()來修改x/y座標。在本例中,x座標就是原來的座標,y座標則調整到了HorizontalScrollView的中間。這樣,當用戶手指向前或向後滾動時,就好像在HorizontalScrollView的中間滾動一樣。然後,呼叫dispatchTouchEvent()將修改後的事件交予HorizontalScrollView處理。

注意:
避免直接呼叫onTouchEvent()方法轉發觸控事件。呼叫dispatchTouchEvent()可以使其像常規觸控事件一樣處理目標檢視的觸控事件,包括必要時的事件攔截。