j2me进度条与线程化模型

解决的问题

在j2me的UI体系中,UI操作是在一
个独立的线程中运行的。往往在api doc中要求程序员对接口方法立即返回。也就是说非阻塞的。你必须开启一个独立的线程来完成你自定义的复
杂的工作,比如联网等可能发生阻塞的io操作。新的线程如果不和用户交流,告诉用户线程正在工作的话,将会显现的非常不友好。用户可能执行
别的操作而扰乱程序的正常运行。一个简单的方法是提供一个进度条,这样用户就会愿意等待上一会,直到程序运行出结果。为了将程序员从前台进度条与后
台线程的通信中解脱出来,专心于后台线程的开发,有必要设计一个进度条线程模型。
 

应该注意到进度条有多种的形式:

A,
动画形式进度条,仅表示程序正在运行(自
维护的)

B, 可交互增量形式的进度条,后台线程通过调用进度条的相应方法在程序运行中不断的改变进度条的状态

C,
进度条的表现形式应该灵活,不要固定其实

D, 进度条对象要重复利用

进度调和后台线程的交流也有好几种情况:

A,
仅仅将进度条绘画在屏幕上,并等后台任务
完成后,由后台线程跳转到成功画面。

B, 对于可取消的任务,用户可以通过点击进度条的按钮来试图cancel任
务,后台任务应该尽快取消,并跳转到失败的画面

C, 对于不可跳转的任务,用户只有耐心等待

D,
如果背景线程运行失败,应自行跳转到失败
的屏幕

 进度条的设计(前台)

为了实现进度条的表现的多样性,首先抽象一个接口:

//ProgressObserver.java
package com.favo.ui;

import javax.microedition.lcdui.Display;

/**
 * @author Favo
 *
 * 这是仿照Smart Ticket制作的进度条观察者,这个模型的优点是
 * 1,低耦合度。你可以通过Form,Canvas等来实现这个接口
 * 2,支持可中断的任务,因为背景线程是无法强制性中断的,
 * 所以就 没有了在观察者中回调背景线程相应方法的必要,
 * 如果支持可中断的话,可以让背景线程来查询观察者的isStopped()
 * 3,可以说进度条仅仅将自己绘画在屏幕上,他对后台线程毫不关心
 */
public interface ProgressObserver {
    /**
     * 将进度条复位
     */
    public void reset();

    /**
     * 将进度条设置最大
     */
    public void setMax();

    /*
     * 将自己绘制在屏幕上,如果进度条要开启自身的线程用于自动更新画面,
     * 也在这里构造并开启绘画线程(常用于动画滚动条)
     */
    public void show(Display display);

    /**
     * 滚动条退出命令,如果进度条曾经开启自身的线程用于自动更新画面,
     * (常用于动画滚动条),在这里关闭动画线程
     */
    public void exit();

    /**
     * 更新进度条
     */
    public void updateProgress(Object param1);

    public boolean isStoppable();

    public void setStoppable(boolean stoppable);

    public boolean isStopped();

    public void setStopped(boolean stopped);

    public void setTitle(String title);

    public void setPrompt(String prompt);
}

每个方法都很一幕了然,我解释两点:

1)“2,支持可中断
的任务,因为背景线程是无法强制性中断的, 所以就 没有了在观察者中回调背景线程相应方法的必要, 如果支持可中断的话,可以让背
景线程来查询观察者的isStopped()”

如果要支持可中断线程的话,想当然的,我们希望用
户按下按钮后回调后台线程的某个方法来停止线程,并且这个方法要立即返回(前面提过UI的用户响应不能够阻塞)。但是细细想想,线
程是无法被强制停止的,而且即使能够被强制停止也很不安全。所以这个方法也只能够是通过设置某个flag,然后立
即返回。这样的话线程就和前台的UI紧密的耦合在一起了。与其这样,倒不如让后台线程去查询UI的状态。这样UI并不关心到
底是谁在后台维护他状态。

2)如果要实现一
个不交互动画UI,那么显然这个UI是自维护的(也就是说UI单独有自己的绘画线程)。为了能够实现这种
情况,可以在show中开启线程,在exit中结束线程。对于交互UI,可以简单的忽略exit方法。

下面给一个利用Form和Gauge实现
的交互式UI(非自维护的),读者可以看看其中的细节,参照他可以设计自己的用Canvas实
现的,或者自维护的等等不同的实现。

ProgressGaugeUI.java
package com.favo.ui;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge;

/**
 * @author Favo
 * Preferences - Java - Code Style - Code Templates
 */
public class ProgressGaugeUI implements ProgressObserver, CommandListener {

    private static final int GAUGE_MAX = 8;

    private static final int GAUGE_LEVELS = 4;

    private static ProgressGaugeUI pgUI;

    private Form f;

    private Gauge gauge;

    private Command stopCMD;

    boolean stopped;

    boolean stoppable;

    int current;

    protected ProgressGaugeUI() {
        f = new Form("");
        gauge = new Gauge("", false, GAUGE_MAX, 0);
        stopCMD = new Command("Cancel", Command.STOP, 10);
        f.append(gauge);
        f.setCommandListener(this);
    }

    public static ProgressGaugeUI getInstance() {
        if (pgUI == null) {
            return new ProgressGaugeUI();
        }
        return pgUI;
    }

    public void reset() {
        current = 0;
        gauge.setValue(0);
        stopped = false;
        setStoppable(false);
        setTitle("");
        setPrompt("");
    }

    public void updateProgress(Object param1) { //这里的参数设计为提示语
        current = (current + 1) % GAUGE_LEVELS;
        gauge.setValue(current * GAUGE_MAX / GAUGE_LEVELS);
        if (param1 != null && param1 instanceof String) {
            setPrompt((String) param1);
        }
    }

    public boolean isStoppable() {
        return stoppable;
    }

    public void setStoppable(boolean stoppable) {
        this.stoppable = stoppable;
        if (stoppable) {
            f.addCommand(stopCMD);
        } else {
            f.removeCommand(stopCMD);
        }
    }

    public boolean isStopped() {
        return stopped;
    }

    public void setStopped(boolean stopped) {
        this.stopped = stopped;
    }

    public void setTitle(String title) {
        f.setTitle(title);
    }

    public void setPrompt(String prompt) {
        gauge.setLabel(prompt);
    }

    public void commandAction(Command arg0, Displayable arg1) {
        if (arg0 == stopCMD) {
            if (isStoppable()) {
                stopped = true;
            } else {
                setPrompt("can't stop!");
            }
        }
    }

    public void show(Display display) {
        display.setCurrent(f);
    }

    public void exit() {
        // 忽略
    }

    public void setMax() {
        gauge.setValue(GAUGE_MAX);
    }
}

后台线程的设计

后台线程替我们作以下的内容:

1)执行我们的任
务runTask()

2)如果用户中断线程,那么runTask()运行完后,将会跳转到我们指定的失败屏幕

3)在最后替我们调用UI.exit()

 

我们需要做的:

1)提供一个前台的UI,提供失败
后跳转的画面,提供Display的实例

2)在runTask()中,如果任务完成,手工跳转失败画面

3)在runTask()中,如果任务失败,手工跳转失败画面

4)在runTask()中改变进度栏的状态。

5)在runTask()中查询用户是否取消,如果用户取消,应该尽快退出runTask()

 

这种模型职责清晰,便于使用。但也有一个缺点:如果用户取消了任务,但是此时任务接近完成,或者已
经完成。后台线程依然会显示用户取消了任务,并将会跳转到我们指定的失败屏幕。这时候会产生不一致的情况。为了解决整个问题,程序员可以在runTask()中调用taskComplete()来强制完成任务。这样即使用户取消了任务,依然回显示任务成功。当然你也可以不掉用taskComplete()遵循默认的行为特点。

 

//BackgroundTask.java
package com.favo.ui;

import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Alert;

/**
 * @author Favo
 * Preferences - Java - Code Style - Code Templates
 */
public abstract class BackgroundTask extends Thread {

    ProgressObserver poUI;

    protected Displayable preScreen;

    protected boolean needAlert;

    protected Alert alertScreen;

    private Display display;

    public BackgroundTask(ProgressObserver poUI, Displayable pre,
                          Display display) {
        this.poUI = poUI;
        this.preScreen = pre;
        this.display = display;
        this.needAlert = false;
    }

    public void run() {
        try {
            runTask();
        } catch (Exception e) {
            Alert al = new Alert("undefine exception",
                                 e.getMessage(), null,
                                 AlertType.ALARM);
            al.setTimeout(Alert.FOREVER);
            display.setCurrent(al);
        } finally {
            if (poUI.isStoppable()) {
                if (poUI.isStopped()) { //如果用户中断了程序
                    if (needAlert) {
                        display.setCurrent(alertScreen, preScreen);
                    } else {
                        display.setCurrent(preScreen);
                    }
                }
            }
            poUI.exit();
        }
    }

    /*
     * 如果任务可中断,查看pgUI.isStopped().并尽快退出此方法;
     * 如果任务需要更新进度栏,调用pgUI.updateProgress(“进度提示”).
     * 习惯上此方法的最后手动调用taskComplete()以防止用户在任务接近
     * 完成时取消
     */
    public abstract void runTask();

    /**
     * 这是一个偷懒的办法,当你构造好BackgroundTask对象后,直接调用这个方法,
     * 可以帮助你初始化进度UI,并显示出来。之后启动你的任务线程
     */
    public static void runWithProgressGauge(BackgroundTask btask, String title,
                                            String prompt, boolean stoppable,
                                            Display display) {
        ProgressObserver po = btask.getProgressObserver();
        po.reset();
        po.setStoppable(stoppable);
        po.setTitle(title);
        po.setPrompt(prompt);
        po.show(display);
        btask.start();
    }

    public ProgressObserver getProgressObserver() {
        return poUI;
    }

    public void taskComplete() {
        getProgressObserver().setStopped(false);
    }
}

  
如何使用

1)产生一个ProgressObserver 对象poUI

如果用默认的,通过调用ProgressGaugeUI.getInstance();

2)构造BackgroundTask对象bkTask,一般可以用匿名类来实现。

3)初始化poUI–>设置后字段–>显示你的poUI–>开启bkTask线程。

第三步可以用一步完成,通过调用静态方法

BackgroundTask.runWithProgressGauge(bkTask, "标题",

                  "提示", 是否可以暂停,
display);

 

下面一个例子,看看你是否理解了,并且会使用了。

//TestProgressGauge.java
package com.favo.ui;

import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

/**
 * @author Favo
 * Preferences - Java - Code Style - Code Templates
 */
public class TestProgressGauge extends MIDlet implements CommandListener {

    Display display;

    Command workCmd;

    Command exitCmd;

    Form f;

    public TestProgressGauge() {
        super();
        // TODO Auto-generated constructor stub
        display = Display.getDisplay(this);
        workCmd = new Command("compute", Command.OK, 10);
        exitCmd = new Command("exit", Command.EXIT, 10);
        f = new Form("Test");
        f.setCommandListener(this);
        f.addCommand(workCmd);
        f.addCommand(exitCmd);
    }

    protected void startApp() throws MIDletStateChangeException {
        // TODO Auto-generated method stub
        display.setCurrent(f);
    }

    protected void pauseApp() {
        // TODO Auto-generated method stub

    }

    protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
    }

    public void commandAction(Command arg0, Displayable arg1) {
        // TODO Auto-generated method stub
        if (arg0 == workCmd) {
            ProgressObserver poUI = ProgressGaugeUI.getInstance();
            BackgroundTask bkTask = new BackgroundTask(poUI, arg1, display) {
                public void runTask() {
                    alertScreen = new Alert(
                            "user cancel",
                            "you press the cancel button and the screen will jump to the main Form",
                            null, AlertType.ERROR);
                    alertScreen.setTimeout(Alert.FOREVER);
                    needAlert = true;
                    //do something first
                    getProgressObserver().updateProgress(null);
                    try {
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    getProgressObserver().updateProgress("sleepd 3s...");
                    if (getProgressObserver().isStopped()) {
                        return;
                    }
                    getProgressObserver().updateProgress(null);
                    //do something second
                    try {
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    getProgressObserver().setMax();
                    display.setCurrent(new Form("complete"));
                    taskComplete();
                }
            };
            BackgroundTask.runWithProgressGauge(bkTask, "Sleep 6s",
                                                "Sleep now...", true, display);
        } else if (arg0 == exitCmd) {
            try {
                destroyApp(false);
            } catch (MIDletStateChangeException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            notifyDestroyed();
        }
    }

}

运行流程画面

 按
下compute–> 用户取消–>

 回到前一屏幕–> 按下compute–>

–>–>完成