首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》控制器布局

关灯直达底部

P167“组装图形界面”一节演示了一个包含两个按钮的视图。虽然按钮看起来很不错,在单击时甚至高亮显示,但是这些按钮没有什么用。单击它们并不会执行任何操作。P165“控制器”一节已经介绍了Android框架如何把外部动作(如屏幕触摸、按键等)翻译成事件,并把这些事件插入到队列中,然后传递给应用。例6-4说明了如何把事件处理器添加到demo中的一个按钮中,使得当单击按钮时可以执行某些操作。

例6-4:绑定按钮


@Override public void onCreate(Bundle state) {    super.onCreate(state);    setContentView(R.layout.main);    final EditText tb1 = (EditText) findViewById(R.id.text1);    final EditText tb2 = (EditText) findViewById(R.id.text2);    ((Button) findViewById(R.id.button2)).setOnClickListener(        new Button.OnClickListener {            // mRand is a class data member            @Override public void onClick(View arg0) {                tb1.setText(String.valueOf(mRand.nextInt(200)));                tb2.setText(String.valueOf(mRand.nextInt(200)));            }        }    );}  

这个版本的应用在运行时还是很像图6-2所示。但是,和之前不同,在这个版本中,每当用户单击Green按钮时,在EditText文本框中的数字就会发生变化,如图6-4所示。

图6-4:可以工作的按钮

虽然改变数字看起来没什么意思,但是这个小小的例子说明了应用响应UI事件的标准机制。要注意的是,暂不讨论外观,该例子没有破坏MVC的分离规则!在实现OnClickListener时,为了响应setText调用,EditText对象更新文本的内部展现,然后调用自己的invalidate方法。它并没有马上渲染屏幕。在编程中,很少有规则是绝对的。模型-视图-控制器之间的界限实际上相当接近。

在这个例子中,Button类的实例通过回调执行操作,正如P98“重写(override)和回调”一节所描述的。Button是视图View的子类,它定义了接口OnClickListener和方法setOnClickListener,通过它们来注册Listener。OnClickListener接口只定义一个方法onClick。当一个按钮从UI框架中接收到事件后,除了其他要执行的操作,它会检查事件,看是否满足“单击”的条件。(在给出的第一个例子中,当单击时,按钮会高亮显示,甚至是在添加监听器之前。)如果事件确实满足“单击”的要求,并且单击listener已经安装,就会调用该listener的onClick方法。

单击监定器可以自由地实现任何需要的自定义行为。在这个例子中,自定义行为会创建两个0~200之间的随机数,并分别把这两个随机数放到文本框中。要扩展Button的行为,所要做的不是实现Button的子类并覆盖其事件处理方法,而是注册一个实现了该行为的单击监听器。这当然就简单了很多!

单击处理程序特别值得关注,因为在Android系统的核心(Android框架事件队列)中并不存在单击事件!相反,View事件处理合成了其他的事件“单击”概念。如果设备包含触摸屏,则屏幕触摸就被认为是单击。如果设备在其D-pad中包含中心键,或Enter键,则按下和释放这些键都会注册单击事件。View客户端不需要考虑什么是单击,或者它在某个设备上是如何生成的。它们只处理较高抽象层次的概念,细节留给了Android框架来处理。

一个视图只能有一个onClickListener。在一个视图上第二次调用setOnClickListener会首先删除老的监听器,然后再新安装一个监听器。另一方面,一个监听器可以监听多个视图。例如,例6-5所示的代码是另一个应用的一部分,它看起来和例6-2完全一样。但是,在这个版本中,按下任何一个按钮都会更新文本框。

这个功能对于包含一些生成相同行为的动作的应用是非常方便的。但是,不要尝试创建可以在所有的部件中使用的强大的单个监听器!如果你的代码包含多个较小的监听器,每个实现一个简单的行为,那么代码维护会简单得多。

例6-5:监听多个按钮


@Override public void onCreate(Bundle state) {    super.onCreate(state);    setContentView(R.layout.main);    final EditText tb1 = (EditText) findViewById(R.id.text1);    final EditText tb2 = (EditText) findViewById(R.id.text2);    Button.OnClickListener listener = new Button.OnClickListener {        @Override public void onClick(View arg0) {            tb1.setText(String.valueOf(rand.nextInt(200)));            tb2.setText(String.valueOf(rand.nextInt(200)));        } };    ((Button) findViewById(R.id.button1)).setOnClickListener(listener);    ((Button) findViewById(R.id.button2)).setOnClickListener(listener);}  

监听模型

Android UI框架普遍使用处理程序安装模式。虽然前面给出的例子都是Button视图,但很多其他的Android部件可以定义监听器。View类定义了一些到处可用的事件和监听器,我们将在后面进一步详细探讨这些。但是,其他类定义了其他专门的事件类型,提供只对这些类有意义的部件处理这些事件。标准的方式是允许客户端自定义部件行为,而不需要继承它。

这种模式也是程序处理外部的且是异步的操作的很好的方式。是否响应远程服务器的状态变化或基于位置的服务更新,你的应用都可以定义自己的事件和监听器以只回应客户端的请求。

以上给出的例子是基础的简化版本。虽然这些模式说明了如何连接View和Controller,但是它们都没有真正的模型(例6-4中使用的String实际上是在EditText模型的实现中的)。

后面会更详细地介绍如何构建一个真正的、可用的模型。例6-6给出的两个类组成的模型支持对演示应用的扩展。这些扩展提供了存储对象列表的工具,每个扩展都包含x和y坐标、颜色和尺寸。它们还提供注册监听器的方式,以及该监听器必须实现的接口。这些例子都是基于通用的监听器模型的,因此它们是相当简单的。

例6-6:Dots模型


package com.oreilly.android.intro.model;/** A dot: the coordinates, color and size. */public final class Dot {    private final float x, y;    private final int color;    private final int diameter;    /**     * @param x horizontal coordinate.     * @param y vertical coordinate.     * @param color the color.     * @param diameter dot diameter.     */    public Dot(float x, float y, int color, int diameter) {        this.x = x;        this.y = y;        this.color = color;        this.diameter = diameter;    }    /** @return the horizontal coordinate. */    public float getX { return x; }    /** @return the vertical coordinate. */    public float getY { return y; }    /** @return the color. */    public int getColor { return color; }    /** @return the dot diameter. */    public int getDiameter { return diameter; }}package com.oreilly.android.intro.model;import java.util.Collections;import java.util.LinkedList;import java.util.List;/** A list of dots. */public class Dots {    /** DotChangeListener. */    public interface DotsChangeListener {        /** @param dots the dots that changed. */        void onDotsChange(Dots dots);    }    private final LinkedList<Dot> dots = new LinkedList<Dot>;    private final List<Dot> safeDots = Collections.unmodifiableList(dots);    private DotsChangeListener dotsChangeListener;    /** @param l the new change listener. */    public void setDotsChangeListener(DotsChangeListener l) {        dotsChangeListener = l;    }    /** @return the most recently added dot, or null. */    public Dot getLastDot {        return (dots.size <= 0) ? null : dots.getLast;    }    /** @return the list of dots. */    public List<Dot> getDots { return safeDots; }    /**     * @param x dot horizontal coordinate.     * @param y dot vertical coordinate.     * @param color dot color.     * @param diameter dot size.     */    public void addDot(float x, float y, int color, int diameter) {        dots.add(new Dot(x, y, color, diameter));        notifyListener;    }    /** Delete all the dots. */    public void clearDots {        dots.clear;        notifyListener;    }    private void notifyListener {        if (null != dotsChangeListener) {            dotsChangeListener.onDotsChange(this);        }    }}  

前面主要介绍了如何使用这个模型,在下一个实例中要介绍的是一个可以用来查看模型的widget库,即DotView。DotView旨在以正确的颜色、在正确的位置描绘出模型中的点。这个应用的完整源代码在本书的Web站点上,可以免费获取。

例6-7是增加了新的模型和视图后的新演示应用。

例6-7:Dots演示


package com.oreilly.android.intro;import java.util.Random;import android.app.Activity;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.LinearLayout;import com.oreilly.android.intro.model.Dot;import com.oreilly.android.intro.model.Dots;import com.oreilly.android.intro.view.DotView;/** Android UI demo program */public class TouchMe extends Activity {    public static final int DOT_DIAMETER = 6;    private final Random rand = new Random;    final Dots dotModel = new Dots;    DotView dotView;    /** Called when the activity is first created. */    @Override public void onCreate(Bundle state) {        super.onCreate(state);        dotView = new DotView(this, dotModel);        // install the View        setContentView(R.layout.main);        ((LinearLayout) findViewById(R.id.root)).addView(dotView, 0);①        // wire up the Controller        ((Button) findViewById(R.id.button1)).setOnClickListener(            new Button.OnClickListener {②                @Override public void onClick(View v) {                    makeDot(dots, dotView, Color.RED);③                } });        ((Button) findViewById(R.id.button2)).setOnClickListener(            new Button.OnClickListener {②                @Override public void onClick(View v) {                    makeDot(dots, dotView, Color.GREEN);③                } });        final EditText tb1 = (EditText) findViewById(R.id.text1);        final EditText tb2 = (EditText) findViewById(R.id.text2);        dots.setDotsChangeListener(new Dots.DotsChangeListener {④            @Override public void onDotsChange(Dots d) {                Dot d = dots.getLastDot;                tb1.setText((null == d) ? "" : String.valueOf(d.getX));                tb2.setText((null == d) ? "" : String.valueOf(d.getY));                dotView.invalidate;            } });    }    /**     * @param dots the dots we're drawing     * @param view the view in which we're drawing dots     * @param color the color of the dot     */    void makeDot(Dots dots, DotView view, int color) {⑤        int pad = (DOT_DIAMETER + 2) * 2;        dots.addDot(            DOT_DIAMETER + (rand.nextFloat * (view.getWidth - pad)),            DOT_DIAMETER + (rand.nextFloat * (view.getHeight - pad)),            color,            DOT_DIAMETER);    }}  

以下是对代码的一些说明:

① 把新的DotView添加到XML定义的布局的上方。

② 把onClickListener回调添加到Red和Green按钮。这些事件handler和前面例子中的handler的区别在于它们是通过proxy(代理)挂接到本地方法makeDot的。这个新的方法生成了一个点(第5项)。

③ onClick内会调用makeDot(当单击按钮时执行这个动作)。

④ 这个例子中的最大变化是Model是连接到View的方式,使用回调安装了一个dotsChangeListener。当模型发生变化时,会调用新的监听器。这个监听器在最左边和最右边的文本框中分别安装了x和y坐标,并请求DotView撤销自己(调用invalidate)。

⑤ 这是makeDot的定义。这个新方法创建一个点,确保它在DotView的边界范围内,并把它加入到模型中。它还支持通过参数指定点的颜色。

图6-5显示了应用在运行时的样子。

图6-5:运行Dots演示程序

单击Red按钮会给DotView增加一个新的红点。单击Green按钮增加一个绿点。文本框中给出的是最后增加的点的坐标。

不难看出,它是在例6-2的基础结构上增加了一些扩展。例如,以下是单击Green按钮后的事件序列:

1.单击按钮,调用onClickHandler方法。

2.调用makeDot,参数包含颜色Color.GREEN。makeDot方法生成随机坐标,并在模型的这个坐标处增加一个新的绿点。

3.当模型更新时,会调用onDotsChangeListener。

4.监听器更新文本视图中的值,并请求重绘DotView。

监听触摸事件

可能你已经猜到,在演示应用中增加对触摸事件的处理实际就是增加tab handler。例6-8所示的代码对应用进行了扩展,在DotView中对应于屏幕上被触摸的点处放置了一个青色点。这段代码应该加到演示应用(例6-7)的onCreate函数的开始位置并在其父方法之后。需要注意的是,由于该代码显示最近添加点的x和y坐标只是连接到模型上,无论View如何添加点,它都可以正常工作。

例6-8:触摸点


dotView.setOnTouchListener(new View.OnTouchListener {    @Override public boolean onTouch(View v, MotionEvent event) {        if (MotionEvent.ACTION_DOWN != event.getAction) {            return false;        }        dots.addDot(event.getX, event.getY, Color.CYAN, DOT_DIAMETER);        return true;    } });  

传递给handler的MotionEvent除了包含触摸位置这个属性之外,还包含一些其他属性。如例子所示,它还包含事件类型,包括DOWN、UP、MOVE或CANCEL。一个简单的触摸事件实际上会生成一个DOWN事件和一个UP事件。触摸并拖曳会生成DOWN事件和一系列MOVE事件以及最后的UP事件。

MotionEvent提供的手势处理工具很有意思。该事件包含触摸点的大小及压力。这意味着对于支持MotionEvent的设备,应用可以区分是一个手指的触摸还是两个手指的触摸,是很轻的触摸还是很重的触摸。

效率在移动世界里还是相当重要的。UI框架在跟踪和报告触摸屏事件时面临着两难境地。报告的事件太少可能会造成准确性不足,而难以跟踪所执行的操作,如手写识别。另一方面,报告的触摸抽样点太多,每个都是一个事件,会给系统造成很重的负担。Android UI框架对这个问题的解决方案是把几组样本关联起来,从而减少负载的同时依然能够保证准确性。要查看和某个事件关联的所有样本,可以使用包含getHistoricalX、getHistoricalY等方法的历史工具。

例6-9演示了如何使用历史工具。它扩展了演示应用,当用户触摸屏幕时,可以跟踪用户的手势。框架把抽样点的x和y坐标传递给对象的onTouch方法,安装为DotView的OnTouchListener。每个样本点会对应绘出一个青色点。

例6-9:跟踪动作


private static final class TrackingTouchListener    implements View.OnTouchListener{    private final Dots mDots;    TrackingTouchListener(Dots dots) { mDots = dots; }    @Override public boolean onTouch(View v, MotionEvent evt) {        switch (evt.getAction) {            case MotionEvent.ACTION_DOWN:             break;          case MotionEvent.ACTION_MOVE:            for (int i = 0, n = evt.getHistorySize; i < n; i++) {                addDot(                    mDots,                    evt.getHistoricalX(i),                    evt.getHistoricalY(i),                    evt.getHistoricalPressure(i),                    evt.getHistoricalSize(i));            }            break;        default:            return false;        }        addDot(            mDots,            evt.getX,            evt.getY,            evt.getPressure,            evt.getSize);        return true;    }    private void addDot(Dots dots, float x, float y, float p, float s) {        dots.addDot(            x,            y,            Color.CYAN,            (int) ((p * s * Dot.DIAMETER) + 1));    }}  

图6-6显示了扩展版本的应用在单击并拖曳几下后的可能样子。

在这个实现中,根据抽样点的尺寸和压力来确定所要绘制的点的直径。遗憾的是,Android模拟器并不会模拟触摸压力和尺寸,因此所有点直径相同。尺寸和压力值在不同设备之间会被范化成0.0~1.0之间的浮点值。然而,这两个值依赖于屏幕的实际精度,它们都有可能超过1.0。模拟器报告事件压力和尺寸只有最小值,即0。

图6-6:演示应用持续运行较长时间

ACTION_MOVE事件的循环处理程序批量处理历史事件。当触摸样本变化速度快于框架的传递速度时,Android框架会把它们绑定成一个事件。MotionEvent事件的getHistorySize方法返回样本点的数量,而各种getHistory方法可以获取后续事件的各个参数。

当轨迹球(trackball)移动时,包含轨迹球的设备也会生成MotionEvent事件。这些事件和触摸屏的触摸动作对应的事件类似,但是处理方式不同。轨迹球的MotionEvent是通过dispatchTrackballEvent传递给View的,而触摸屏的事件使用的是dispatchTouchEvent,传递的是触摸动作。虽然dispatchTrackballEvent确实把事件传递给了onTrackballEvent,但它并不会预先把事件传递给监听器!不但轨迹球生成的MotionEvent在普通的触摸机上不可见,而且为了对MotionEvent进行响应,必须有个widget继承View类,覆盖onTrackballEvent方法。

轨迹球生成的MotionEvent是通过另一种方式处理的。如果没有使用它们(很快会对其进行定义),它们就会转化成D-pad按键事件。如果大部分设备或者配备了D-pad或者配备了轨迹球,而不是两种设备都配备,那这种处理就是有意义的。如果没有这个转换,就不可能在只包含轨迹球的设备上生成D-pad事件。当然,这也说明应用处理轨迹球事件时必须谨慎,因为它有可能会破坏转换。

转换后,轨迹球运动作为一系列D-pad按键,应用可见。

多个指针和手势

很多设备支持同时跟踪多个指针,该功能有时称为“多点触控技术(multitouch)”。当用户触摸屏幕上的不同地方,会分别独立对这些触摸跟踪。这些跟踪可用于判别具有特殊含义的复杂手势,比如滚动、缩放、翻页等。

之前介绍的所有事件方法,返回关于MOVE事件的信息(getX、getY、getHistoricalX、getHistoricalY等)通过额外参数支持多点触控,该参数指定调用所指向的特定轨迹。举个例子,除getX函数外,还有个函数是getX(int pointerIndex)。参数pointerIndex支持调用方访问各种不同轨迹,方法getPointerCount返回事件中记录的不同轨迹数。不幸的是,事件中各个轨迹的索引并不是常量。换句话说,如果用户通过拇指触摸屏幕,食指触摸索引,拇指的轨迹可能会以连续模式显示,首先在索引位置0,然后在索引位置1,然后又回到索引位置0。为了通过几个事件追踪单个轨迹,需要使用轨迹ID,而不是索引。为了完成这一点,使用方法getPointerId和findPointerIndex,对ID和索引进行转换。

例6-10从前面的例子中扩展了onTouch方法,能够追踪多个轨迹。

例6-10:追踪运动

在这段代码中有几点需要注意。首先,注意case语句不是基于事件动作而是基于该动作执行了掩码操作后的版本进行switch判断。这种基于位的触发操作显然有些陈旧。有必要使得回调可以向后兼容。要忽略多个轨迹,不要执行掩码操作,直接基于事件动作进行switch判断,而且在switch判断中要处理default情况。

其次,注意switch中新增了两种情况:MotionEvent.ACTION_POINTER_DOWN和MotionEvent.ACTION_POINTER_UP。在这个简单的例子中,这两种情况用于表示新轨迹的起始和结束。由于连续轨迹是通过ID表示的,该代码在轨迹开始时会添加新的ID,在轨迹结束时删除该ID。

最后,如果事件中包含历史信息,所有的轨迹也包含相同的历史记录数。

在特殊(但很常见的)情况下,多个轨迹组成特定含义的手势(可能是手抓式缩放),Android库会借助手势识别器的支持给出提示信息。官方文档承认提供的两个手势识别器的实现主要是为了给出建议,而不是完整的解决方案。实际上,这两个识别器GestureDetector和ScaleGestureDetector,有时会支持一些常见的手势:单击、双击、长按和短按。

一般来说,使用手势识别器需要创建一个实例,注册监听器,把识别器添加到OnTouch处理器上,并传递一个新的事件。当有手势发生时,会通知监听器。识别器的行为的详细信息(包括监听器类型)专门针对特定的识别器。

监听按键事件

支持跨平台处理键盘输入可能是非常棘手的。有些设备包含的按键要比其他设备多得多,例如有些设备输入字符时需要触摸3次。这是一个绝佳的例子,说明有些处理要尽可能地留给框架(EditText或其子类)来处理。

要扩展部件的KeyEvent处理,使用View的方法setOnKeyListener来安装OnKeyListener。对于用户的每次按键,监听器都会收到多个KeyEvent,每个动作对应如下类型之一:DOWN、UP和MULTIPLE。动作类型DOWN和UP表示一个键被先按下后释放这个操作,与MotionEvent类类似。动作类型MULTIPLE表示长按某个键(自动重复)。KeyEvent的方法getRepeatCount可以得到MULTIPLE事件的按键次数。

例6-11是一个按键处理程序实例。把该处理程序添加到演示程序后,当按下或释放按键时,它会在随机选定的位置显示一个点。当按下并释放空格键时,会添加一个洋红色的点;当按下并释放Enter键时,会添加一个黄点;当按下并释放其他任何键时,会添加一个蓝色的点。

例6-11:按键处理


dotView.setOnKeyListener(new OnKeyListener {    @Override public boolean onKey(View v, int keyCode, KeyEvent event) {        if (KeyEvent.ACTION_UP != event.getAction) {            int color = Color.BLUE;            switch (keyCode) {                case KeyEvent.KEYCODE_SPACE:                    color = Color.MAGENTA;                    break;                case KeyEvent.KEYCODE_ENTER:                    color = Color.YELLOW;                    break;                default: ;            }            makeDot(dots, dotView, color);        }    return true;} });  

处理事件的其他方式

你可能已经注意到,到目前为止,所有介绍过的on...方法(包括onKey)的返回值都是boolean(布尔)类型。这种模式使得监听器可以把后续事件的处理交给调用者来处理。

当把Controller事件交给widget后,widget中的框架代码会调度该Controller事件,根据其类型执行对应的方法:onKeyDown、onTouch Event等。这些方法,无论是在View中还是在View的某个子类中,都已经实现了该部件的行为。然而,正如之前所描述的,框架会首先把事件提交给相应的监听器(onTouchListener、onKeyListener等),如果监听器存在的话。监听器的返回值决定了其后续事件是否要分发给View的方法。

如果监听器返回false,则该事件就被分发到View方法,好像处理程序不存在一样。如果监听器返回true,就认为事件已经处理了。View会放弃任何进一步的处理。View方法一直都没有被调用,它没有机会处理或响应事件。对于View方法而言,相当于该事件不存在。

因此,事件有3种处理方式:

没有监听器

事件分发给View方法以正常执行。通过部件实现覆盖这些方法。

监听器存在并返回true

监听器事件处理完全取代正常的部件事件处理。事件不会分发给View。

监听器存在并返回false

事件先被监听器处理,然后再由View处理。事件被监听器处理完后,就会被分发给View执行正常的处理。

例如,把例6-11的按键监听器添加到EditText widget中,会发生什么情况呢?因为onKey方法总是返回true,只要该方法返回,框架就会丢弃后续的所有KeyEvent事件。这使得EditText按键处理无法看到按键事件,因此文本框中就不会有文本。这可能不是期望的行为!

如果onKey方法对于某些按键事件返回false,那么框架会把这些事件分发给widget以执行进一步的处理。EditText会看到这些事件,相关的字符会如期附加到EditText文本框中。例6-12是例6-11的扩展,其除了会给模型增加新的点,还会过滤掉传递给虚拟的EditText文本框的字符。它只支持数值,其他类型的字符都会被隐藏起来。

例6-12:扩展的按键处理


new OnKeyListener {    @Override public boolean onKey(View v, int keyCode, KeyEvent event) {        if (KeyEvent.ACTION_UP != event.getAction) {            int color = Color.BLUE;            switch (keyCode) {                case KeyEvent.KEYCODE_SPACE:                    color = Color.MAGENTA;                    break;                case KeyEvent.KEYCODE_ENTER:                    color = Color.YELLOW;                    break;                default: ;            }            makeDot(dotModel, dotView, color);        }        return (keyCode < KeyEvent.KEYCODE_0)            || (keyCode > KeyEvent.KEYCODE_9);    }}  

如果应用需要实现全新的事件处理方式(基于onKeyHandler,无法通过合理的行为扩张和过滤实现自己所需的功能),则需要理解和覆盖View类的按键事件处理。处理的核心过程简单说就是,通过DispatchKeyEvent方法把事件分发给View。DispatchKeyEvent实现了前面所描述的行为,其先把事件交给onKeyHandler,然后如果处理程序返回false,就把事件交给实现KeyEvent.Callback接口的View方法:onKeyDown、onKeyUp和onKeyMultiple。

高级连接:聚焦和线程化

P179“监听触摸事件”一节所描述的,把MotionEvent交给部件,该部件的边界矩形框包含生成该矩形框的触摸点。确定哪个部件应该接收KeyEvent不是很容易。要做到这一点,类似大多数其他UI框架,Android UI框架支持选择,即聚焦(focus)。

为了接收焦点,必须把部件的focusable属性设置为true。设置的方式有两种:第一种是使用XML布局属性(例6-3中的EditView视图的facusable属性设置为false);第二种是使用setFocusable方法,如例6-11的第一行代码所示。用户通过D-pad按键或触摸屏幕改变View。

当一个部件在焦点中时,它通常通过某些高亮显示进行渲染,使用户感觉到它是当前的操作目标。例如,当EditText部件在焦点中时,它不但高亮显示,而且还把光标置于文本插入位置。

要接收当View进入或离开焦点时的通知,需要安装OnFocusChangeListener。例6-13说明了监听器需要添加焦点相关的特征到演示应用程序。它会引起随机放置的黑点只要在焦点状态下就随机自动添加到DotView中。

例6-13:处理焦点


dotView.setOnFocusChangeListener(new OnFocusChangeListener {    @Override public void onFocusChange(View v, boolean hasFocus) {        if (!hasFocus && (null != dotGenerator)) {            dotGenerator.done;            dotGenerator = null;        }        else if (hasFocus && (null == dotGenerator)) {            dotGenerator = new DotGenerator(dots, dotView, Color.BLACK);            new Thread(dotGenerator).start;        }} });  

该OnFocusChangeListener函数没有什么特别之处。当DotView成为焦点时,它会创建DotGenerator并生成一个线程来运行它。当该widget离开焦点时,DotGenerator就会被中止并释放。新的数据成员dotGenerator(例子中没有给出其声明)只有当DotView处于焦点时才是非空的。DotGenerator的实现中有另一个重要的强大工具,我们很快就会说到它。

焦点通过View方法requestFocus被传递给特定的widget。当为新的目标widget调用requestFocus时,该请求会通过父节点向上传递,直到树结构的根节点。根节点会记住哪个部件在焦点中,并直接把后续的按键事件传递给它。

这正是UI框架在响应D-pad按键时改变焦点到新的widget的方式。Android UI框架识别出下一个会成为焦点的部件,调用该部件的requestFocus方法。这使得当前获得焦点的部件会失去焦点,而目标部件会得到焦点。

确定获取焦点的部件的过程是复杂的。为此,遍历算法需要执行一些复杂的计算,它可能会依赖于屏幕上的其他部件的位置!

举个例子,设想一下当按下方向键的右键时会发生什么,UI框架会把焦点传递给当前焦点所在部件的右侧的部件。当查看屏幕时,应该是哪个部件可能是一目了然;然而,在视图树中却没有这么明显。在树结构中,目标部件可能在另一层,隔着几个分支。定位下一个焦点所处的部件取决于部件在树结构的另一分支的精确维度。幸运的是,尽管确定下一个部件相当复杂,但Android UI框架通常都能正常工作。

如果Android UI框架给出的部件和期望的不同,还存在4个属性,可以通过方法设置或者通过XML属性设置,从而强制执行焦点遍历行为。这些属性是nextFocusDown、nextFocusLeft、nextFocusRight和nextFocusUp。将任意一个属性的引用指向某个部件就能确保触摸板会沿着相应的方向执行遍历直到把焦点转移到这个部件为止。

焦点机制的另一个复杂之处在于对于支持触摸屏的设备,Android UI框架需要区分触摸板焦点和触摸焦点。要理解这一点,回想一下不支持触摸输入的屏幕,按下某个按键的唯一方式是要对它聚焦,使用方向键遍历,然后使用中心键生成单击。但是,对于支持触摸的屏幕,不需要对按钮执行聚焦。

无论当前的焦点位于哪个部件中,都可以移动到这个按钮上并单击它。但是,即使对于触摸屏,还是有必要能够将焦点定位在接受按键的部件上,例如EditText widget,从而确定它为后续按键事件的目标。为了正确地处理两种聚焦,需要查看View处理模式FOCUSABLE_IN_TOUCH_MODE,以及View的isFocusableInTouchMode和isInTouchMode方法。

对于包含多个窗口的应用,在聚焦机制中至少包含一个转折点。窗口有可能失去焦点而没有通知当前的部件。思考一下,就会发现这种情况是有可能的。如果失去焦点的窗口被放回到最上方,那么该窗口中处于焦点下的部件会重新得到焦点,而不需要其他操作。

在将朋友的电话号码输入到地址簿的应用中。假设你突然回退到电话应用中,看看她的电话号码的最后几位。当你返回地址簿时,如果你还需要再次重新聚焦到文本输入框上,你会觉得很懊恼。你期望的是还回到上次离开时的状态。

另一方面,该行为会产生奇怪的副作用。特别是,例6-13中所示的自动点特征的实现在被其他窗口遮盖时还能够给DotView添加点。如果后台任务只在特定的widget可见的情况下运行,则当该部件失去焦点时,或窗口失去焦点,或Activity暂停或中止时,都必须清除该任务。

大多数焦点机制的实现是在ViewGroup类中,在requestFocus和requestChildFocus这样的方法中。要是需要实现一个全新的焦点机制,需要仔细查看这些方法,并相应地覆盖这些方法。

下面先不探讨焦点这一话题,再回到新增加的自动点特征的实现上。例6-14显示了DotGenerator的实现。

例6-14:处理线程


private final class DotGenerator implements Runnable {    final Dots dots;    final DotView view;    final int color;    private final Handler hdlr = new Handler;①    private final Runnable makeDots = new Runnable {②        public void run { makeDot(dots, view, color); }    };    private volatile boolean done;    // Runs on the main thread    DotGenerator(Dots dots, DotView view, int color) {③        this.dots = dots;        this.view = view;        this.color = color;    }    // Runs on the main thread    public void done { done = true; }    // Runs on a different thread!    public void run {        while (!done) {            try { Thread.sleep(1000); }            catch (InterruptedException e) { }            hdlr.post(makeDots);④        }    }}  

以下是这段代码的一些重点说明:

① 创建一个android.os.Handler对象。

② 创建一个新的线程,运行第4项的makeDot。

③ 在主线程上运行DotGenerator。

④ 在第1项创建的Handler中运行makeDot。

在这个DotGenerator的简单实现中,在run代码块内直接调用了makeDot。但是,这么做不安全,除非makeDot是线程安全的——Dots类和DotView类也是。这种方式很容易出错,而且难以维护。实际上,Android UI框架禁止多个线程访问View类。运行这个实现会导致应用抛出下面这个运行时异常(RuntimeException):


11-30 02:42:37.471: ERROR/AndroidRuntime(162): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.  

我们在第3章已经介绍过这个问题并给出了解决方式。要取消这个限制,DotGenerator需要在构造函数内创建Handler对象。Handler对象和创建该对象的线程关联,使得线程可以对传统的事件队列安全地进行并发访问。

因为DotGenerator在构造时创建了一个Handler,该Handler和主线程关联。现在,DotGenerator可以使用Handler把另一个线程的Runnable对象插入队列,该Runnable对象调用UI线程的makeDot方法。结果正如你所预见的,Handler所指向的传统事件队列即UI框架所使用的队列。从队列中删除makeDot的调用,并分发类似任何其他的UI事件,以合理的顺序进行分发。因此,这使得Runnable方法开始运行。makeDot是从主线程调用的,UI还是单线程的。

值得重申的是,以上是通过Android UI框架进行编程的基础模式。当用户的操作耗时超出几毫秒时,在主线程上执行该操作可能会导致整个UI变得很慢,更糟的是,可能会长时间僵死。如果主应用线程有几秒时间无法处理事件队列,则Android OS会由于应用没有响应而结束该应用。Handler类和AsyncTask类支持程序员把运行慢或需要较长时间运行的任务委派给其他线程以便避免这个问题,进而确保主线程能够继续提供UI服务。这个例子说明了如何使用包含Handler的线程周期性地为UI执行入队更新操作。

这个演示程序比较简单。它把新创建的点加入队列,并把它加入到主线程的点模型中。更复杂的应用可能是在创建时给模型传递一个主线程的Handler,并为UI提供从模型中获取模型线程的Handler的方式。主线程会通过主线程Handler接收模型插入队列中的更新事件。模型运行在自己的线程上,会使用Looper类从队列中删除和分发从UI得到的输入消息。在考虑复杂的架构之前,首先应该考虑使用服务或ContentProvider(参见第13章)。

通过这种方式在UI和长时间运行的线程之间传递信息可以极大地降低维护线程安全的成本。特别地,回顾一下第3章的内容,如果入队线程不包含插入队列的对象的引用,或者如果该对象是不可改变的,那么就不需要额外的同步操作。