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()可以使其像常规触摸事件一样处理目标视图的触摸事件,包括必要时的事件拦截。