15.阻止觸控竊賊
15.1 問題
應用程式檢視中設計了巢狀的觸控互動,這些互動不能很好地作用於觸控層次結構 的標準流程,在此層次結構中,較高層的容器檢視通過子檢視進行竊取來直接處理觸控事件。
15.2 解決方案
(API Level 1)
ViewGroup是框架中所有佈局和容器的基類,它為此提供了描述性命名方法requestDisallowTouchIntercept()。在任何容器檢視上設定此標誌會指示框架,在當前手勢持續期間,我們更希望它們不會攔截進入其子檢視的事件。
15.3 實現機制
為展示此方法的實際使用,我們建立了一個示例,其中兩個互相競爭的可觸控檢視位於同一位置。外部包含檢視是ListView,它通過滾動內容響應指示垂直拖動的觸控事件。在ListView內部是作為頭部新增的ViewPager,它響應水平拖動觸控事件以在頁面之間輕掃。就其本質來說,該例帶來了一個問題,水平輕掃在垂直方向上遠距離變化的ViewPager的嘗試會為了支援ListView滾動而被取消,因為ListView會監控和攔截這些事件。人們無法在垂直或水平運動過程中進行拖動,因此這就產生了可用性問題。
為建立此例,首先需要宣告一個維度資源(參見以下程式碼),程式碼清單給出了完整的Activity。
res/values/dimens.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="header_height">150dp</dimen> </resources>
管理觸控攔截的Activity
public class DisallowActivity extends Activity implements ViewPager.OnPageChangeListener { private static final String[] ITEMS = { "Row One", "Row Two", "Row Three", "Row Four", "Row Five", "Row Six", "Row Seven", "Row Eight", "Row Nine", "Row Ten" }; private ViewPager mViewPager; private ListView mListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Create a header view of horizontal swiping items mViewPager = new ViewPager(this); // As a ListView header, ViewPager must be given a fixed height mViewPager.setLayoutParams(new ListView.LayoutParams( ListView.LayoutParams.MATCH_PARENT, getResources().getDimensionPixelSize(R.dimen.header_height)) ); // Listen for paging state changes to disable parent touches mViewPager.setOnPageChangeListener(this); mViewPager.setAdapter(new HeaderAdapter(this)); // Create a vertical scrolling list mListView = new ListView(this); // Add the pager as the list header mListView.addHeaderView(mViewPager); // Add list items mListView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, ITEMS)); setContentView(mListView); } /* OnPageChangeListener Methods */ @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { // While the ViewPager is scrolling, disable the ScrollView touch // intercept so it cannot take over and try to vertical scroll. // This flag must be set for each gesture you want to override. boolean isScrolling = state != ViewPager.SCROLL_STATE_IDLE; mListView.requestDisallowInterceptTouchEvent(isScrolling); } private static class HeaderAdapter extends PagerAdapter { private Context mContext; public HeaderAdapter(Context context) { mContext = context; } @Override public int getCount() { return 5; } @Override public Object instantiateItem(ViewGroup container, int position) { // Create a new page view TextView tv = new TextView(mContext); tv.setText(String.format("Page %d", position + 1)); tv.setBackgroundColor((position % 2 == 0) ? Color.RED : Color.GREEN); tv.setGravity(Gravity.CENTER); tv.setTextColor(Color.BLACK); // Add as the view for this position, and return as the object for // this position container.addView(tv); return tv; } @Override public void destroyItem(ViewGroup container, int position, Object object) { View page = (View) object; container.removeView(page); } @Override public boolean isViewFromObject(View view, Object object) { return (view == object); } } }
在此Activity中,作為根檢視的ListView包含一個基本介面卡,用於顯示字串條目的靜態列表。同樣在onCreate()方法中,建立ViewPager例項並作為頭部檢視新增到列表中。我們將在本章後面更詳細地討論ViewPager的工作方式,此處只需要知道我們正在建立一個帶有自定義PagerAdapter的簡單ViewPager,它顯示了一些彩色檢視作為其頁面,以供使用者在這些頁面直接輕掃。
建立ViewPager之後,構造並應用一組ListView.LayoutParams來控制ViewPager如何作為頭部顯示。必須執行該操作,因為ViewPager自身沒有內在的內容大小,並且列表不能很好地作用於沒有明確高度的檢視。通過維度資源應用固定的高度,從而可以輕鬆獲得適當縮放的dp值,該值與裝置無關。這比完全通過Java程式碼全面構造dp值要簡單很多。
此例的關鍵之處在於Activity實現的onPageChangeListener(該回調在後面會用於與ViewPager)。當用戶與ViewPager互動並左右輕掃時,就會觸發此回撥。在onPageScrollStateChanged()方法內部,我們傳遞一個指示ViewPager是否空閒、
Activity正在滾動或在滾動後停到某個頁面的值。這是控制父ListView的觸控攔截行為的最佳位置。當ViewPager的滾動狀態不是空閒時,我們不希望ListView竊取Viewager正在使用的觸控事件,因此在requestDisallowTouchIntercept()中設定相應的標誌。
連續觸發該值還要另一個原因。在原始解決方案中提及,該標誌對當前手勢有效。這意味著每次新的ACTION_DOWN事件發生時,我們需要再次設定該標誌。沒有新增觸控偵聽器來查詢特定的事件,我們基於子檢視的滾動行為連續設定該標誌,這就獲得了相同的效果。