触控设备的Qwerty键盘画布

这一篇是我在A Canvas Qwerty Keyboard For Touch Devices的中文翻译,网址在触控设备的Qwerty键盘画布
为了备份,并转贴在此:

使用低阶UI的键盘问题

虚拟键盘背后的意图就是再没有实体键盘的触控设备上像是Nokia 5800 XpressMusic及Nokia 5530 XpressMusic手机在MIDlet(内建低阶UI类别)里有一个机制来输入任何种类的文字。

当使用有实体键盘的设备来建立低阶(Canvas或GameCanvas)应用程式时,能够从所按的键来取得事件然后据此行动,每个键的ascii码会传到Canvas类别的keyPressed(int)方法里。

文数键盘: 只有一个ascii码会传到keyPressed方法,这是因为每个键参考几个字元的话就不能去确切知道使用者想要输入什么字元,通常ascii码传到这个方法里的会是关于数值的。

Qwerty键盘:假如键盘是qwerty,就能够获取所有字元集的ascii码,当像shift或数值模式被按下时设备就会负责改变字元集,所以正确的ascii码可以传到这个方法里。

根据上述,在不同的诺基亚手机里输入文字可以用qwerty键盘来解决,而且在文数键盘及非实体键盘的手机里可以存在。

有一种方法来解决的基本问题,它包含了结合低阶及高阶的显示元件,当一个低阶应用程式的指定区域建立,那么这个想法就是援引一个TextBox元件、撷取使用者新增的文字然后再一次在低阶元件里显示。

这个方法有下面的缺点。

  • 高阶元件会占用整个荧幕。
  • TextBox元件的外观感觉是根据原生的外观感觉而不是使用应用程式的外观感觉而这可能会干扰使用者经验,举一个例应用程式的外观感觉整个是蓝的而原生元件是黑的。

在最新的S60 5th版本里有一个附加元件不会占用整个荧幕,它只需要在JAD档里增加一个属性,你可以看看范例如何使用这个机制在下面的连结里。 How to use pop-up TextBox in Java ME

总体设计考虑

可绘区域

Image:vkimage1.jpg
在S60 5th版本里的Canvas的可绘区域可以用属性Nokia-MIDlet-On-Screen-Keypad来改变,不同的可绘区域大小显示在下表里。
Nokia-MIDlet-On-Screen-Keypad

Value Portrait Landscape
*Default (gameactions & navigation keys) 360×360 320×360
navigationkeys 360×384 372×360
no 360×640 640×360

方向

有一个方法可以强迫键盘保持横向,可以使用下面的属性这样作:

Nokia-MIDlet-App-Orientation

可能值:portrait 或 landscape

很重要的备注一下是在早期5800的软体版本里这个属性没有作用,这个问题已在早期的韧体版本中更正。 KIJ001169 – Fixed landscape mode cannot be set by using Java ME in Nokia 5800 XpressMusic
理想情况下在JAD或manifest档里使用这两个属性就可以有一个全荧幕的虚拟键盘(640×360):

  • Nokia-MIDlet-On-Screen-Keypad: no
  • Nokia-MIDlet-App-Orientation: landscape

键盘组成

Image:vkimage2.jpg

TOUCHKEYBOARD

这个类别用在触控设备的虚拟键盘,键盘由TextFieldArea用来显示输入的文字,及一系列可用字元的按钮来允许修改虚拟键的行为等组成。

所有按钮继承自宣告基本方法的Button类别,虚拟键盘包含三种不同的按钮。

  1. MatrixButton:是显示字元键盘的一部份,每个键有一个预览然后当按钮下时采取一个行动。
  2. PushButton:是单独的按钮,这些按钮按下时不会显示这个键的预览,例如Ok、 backspace、shift,按钮释放时也会有行动会采取。
  3. ModeButton:是改变键盘一些行为的按钮,像是字元集或是启用指针,当按钮按下时采取行动。

改变字元区域的外观感觉

MatrixButton也称为字元区域是由TiledLayer组成,TiledLayer包含三种不同的拼布,第一块用在键盘的闲置状态,第二 块用在键盘被选择的状态而第三块则用在键盘按下时在右边预览,在MatrixButton里的字元集会直接绘在GameCanvas的Graphics 上,他们的Font及Color可以用方法setCharactersAreaFont(Font)setCharactersAreaFontColor(int)setCharactersAreaPreviewFontColor(int)来改变,这允许字元区域来调整不同的尺寸来跟应用程式有相同的外观感觉。

改变TextFieldArea的外观感觉

要根据应用程式所用的外观感觉来改变TextFieldArea的外观感觉,可以使用下面的方法setTextFieldFont(Font)setTextFieldBkgColor(int)setTextFieldBorderColor(int)setCursorColor(int)setTextFieldFontColor(int)

改变其余按钮的外观感觉

虚拟键盘其余的按钮是PushButton及ModeButton,这些按钮基本上是由Sprite物件组成,要改变另一个按钮的外观感觉需要修改 resources资料夹里相关的图档,每个图档会参考一个私有常数,所以假如图档名称改变也要确认这个私有常数有变更是很重要的。

限制字元的数目

要限制键盘里输入的字元数目可以使用函式 setMaxSize(int)

TEXTFIELDAREA

这个元件能够保持并显示文字,新增时指定一个尺寸并在元件整个生命周期里都保留着,TextFieldArea类似S60原生文字输入区域的行为这个行为可以在写入一个文字讯息时见到。

改变外观感觉

根据应用程式所使用的外观感觉来改变是可能的。

要这样做使用下面的方法setTextFieldFont(Font)、setTextFieldBkgColor(int)、setTextFieldBorderColor(int)、setCursorColor(int)、setTextFieldFontColor(int)

安排文字

这个部份负责拥有的文字安排,要辨别一个字可以藉着字元后的空白字原来做,假如这个字太长不能适合这行的其余空间,它会被推到下一行,假如单独的一 个字还是大于这个TextFieldArea的水平空间,这个字就会被分成几段来适合这个部份的宽度,每次一个新的字元新增或删除整个textArea必 须重组,这是因为文字可以在任何地方输入的关系。

关于游标

游标标记新的字元可以新增的地方,当新增发生时,游标移到下个位置,游标的颜色可以使用方法setCursorColor(int)来修改或使用setCursorVisible(boolean)来隐藏,游标可以使用方法moveCursorLeft()moveCursorRight()移 到左边或右边,当游标到最后一行的最后一个位置时,输入的下一个字元会让这部份向下移一行所以游标始终可以见到并且在最后一行,同样地假如游标移到这部份 的右上方并且其上还隐藏有几行文字,那么他会使得这部份向上移一行所以游标还是保持可见,在这几个字里游标控制TextFieldArea显示的己行文 字。

绘制TextFieldArea

这个部份需要一个Graphics物件来绘制,要这样做需要一个明确的呼叫方法paint(Graphics, int, int)来做,最后两个参数表示这部份将绘制的Graphics的xy位置,这机制的最大好处是这部份的同一实体可以不同的Graphics及不同的位置来绘制,这个部份会将文字分成几行,只有在荧幕的几行文字会被绘制,其余的会被忽略,游标所在的那一行始终保持可见。

新增新的字元及取得文字

TextFieldArea最大字元数可以使用方法setMaxSize(int)来确定,假如没有指定这个值,就不会限制输入的字元数,要在目前游标位置新增一个新的字元可以使用方法insertCharacterInCursorPosition(char)来做,在游标位置删除一个字元可以使用方法deleteLastCharBeforeCursor()来做,要在任何时间设定文字可以使用方法 setText(String),这个方法将置换原有的文字然后将游标置于该行的结尾,要取得TextFieldArea现有的文字使用getText()方法。 要充分利用键盘在设备里的文字安排功能,这个部份提供insertAscii(int)方 法,当Canvas或GameCanvas使用时,keyPressed方法提供按钮按下的ascii码,对于正常键盘手机来说这是按下数字的 ascii,对qwerty设备来说它会是按下键的值,假如shift或数值键被按下该设备会自动改变字元集,所以所收到的ascii是不同的并且对应于 所要的键,倒退键字元也会以ascii码来收取,所以没有必要用明确的方式来呼叫删除的函式。

下载原始码及执行档

在这里你可以找到完整的NetBeans专案包括原始码及执行档。 Image:TouchKeyboard.zip

下载JAVADOCS

这里你可以找到API参考。 Image:Javadocs.zip

下载范例

下面你将发现使用这个元件的范例,你可以下载完整的范例。 Image:KeyBoardExample.zip

使用元件

下面的范例显示如何使用TouchKeyboard及TextFieldArea类别,这个范例包含有两个TextFieldAreas的一个主要 的canvas,负责检测属于这个应用程式的使用者点到那个区域,一旦应用程式侦测到TextFieldArea有被接触它就会显示虚拟键盘,当使用者在 虚拟键盘选择OK按钮时,输入的文字会储在一个变数里,这个变数是根据资料(name或address)的然后这个值会传到正确的 TextFieldArea,下一次键盘援引时,它会提供TextFieldArea所存的文字。

记住TextFieldArea会调整它所持有的文字成自己定义的大小,那就是为什么在TouchKeyboard里的文字跟这两个TextFieldAreas里的文字可能会不同。

下面这张图片显示两张应用程式的荧幕快照

Image:vkimage3.jpg

Image:vkimage4.jpg

JAD & Manifest属性

Nokia-MIDlet-On-Screen-Keypad: no
Nokia-MIDlet-App-Orientation: landscape

BigKeyboard.java

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import keyboard.TextFieldArea;
import keyboard.TouchKeyboard;
import keyboard.VirtualKeyboardListener;

public class BigKeyboard extends MIDlet
        implements VirtualKeyboardListener {

    public static BigKeyboard instance;     //static instance for the MIDlet

    private TouchKeyboard tkb;              //Virtual keyboard

    private Display display;
    private CanvasScreen canvas;            //Main application canvas

    int nameTransId;                        //transaction id to identify name

    int addressTransId;                     //transaction id to identify address

    public BigKeyboard() {
        instance = this;
        canvas = new CanvasScreen();
        display = Display.getDisplay(this);
    }

    public void startApp() {
        showCanvas();
    }

    /**
     * Shows the main application Canvas
     */
    public void showCanvas() {
        display.setCurrent(canvas);
    }

    /**
     * Shows the virtual keyboard in order to retrieve the name.
     * The name will be limited to 20 characters.
     * @param name
     */
    public void getName(String name) {
        if (tkb == null) {
            createKeyboard();            //create keyboard if it does not exists
        }
        //when keyboard is reseted, it returns a transactionId
        nameTransId = tkb.resetKeyBoard();
        tkb.setMaxSize(20);
        tkb.setText(name);
        display.setCurrent(tkb);
    }

    /**
     * Shows the virtual keyboard in order to retrieve the name.
     * The name will be limited to 80 characters.
     * @param address
     */
    public void getAddress(String address) {
        if (tkb == null) {              //create keyboard if it does not exists
            createKeyboard();
        }
        //when keyboard is reseted, it returns a transactionId
        addressTransId = tkb.resetKeyBoard();
        tkb.setMaxSize(80);
        tkb.setText(address);
        display.setCurrent(tkb);
    }

    /**
     * Creates a new instance of the keyboard and sets it with the right
     * customization parameters
     */
    private void createKeyboard() {
        tkb = new TouchKeyboard(TouchKeyboard.KEYBOARD_BIG, 0, 0, false);
        tkb.setTextFieldBkgColor(0xE0DBCA);
        tkb.setTextFieldBorderColor(0x040477);
        tkb.setTextFieldFontColor(0x0033CC);
        tkb.setBackgroundColor(0x44A51C);
        tkb.togglePointer();
        tkb.setVirtualKeyboardListener(this);
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
    }

    /**
     * Method of the interface VirtualKeyboardListener.
     * This method is invoked when the OK button is pressed at the moment
     * the keyboard is displayed on the screen.
     * @param transactionId
     * @param text
     */
    public void okPressed(int transactionId, String text) {
        //use the transactionId to identify the purpose of the call
        if (transactionId == nameTransId) {
            canvas.setName(text);
        } else if (transactionId == addressTransId) {
            canvas.setAddress(text);
        }
        showCanvas();
    }
}

CanvasScreen.java

class CanvasScreen extends Canvas {

    /**
     * TextFieldArea containing the data of the name.  This textfield is only
     * to show information and so cursor should not be visible.
     */
    private TextFieldArea tfName;
    /**
     * TextFieldArea containing the data of the address. This textfield is only
     * to show information and so cursor should not be visible.
     */
    private TextFieldArea tfAddress;

    public CanvasScreen() {
        tfName = new TextFieldArea(200, 40);
        tfName.setCursorVisible(false);
        tfName.setTextFieldBorderColor(0xE024C1C);
        tfName.setTextFieldBkgColor(0xffffff);
        tfName.setTextFieldFontColor(0x0033CC);

        tfAddress = new TextFieldArea(200, 80);
        tfAddress.setCursorVisible(false);
        tfAddress.setTextFieldBorderColor(0xE024C1C);
        tfAddress.setTextFieldBkgColor(0xffffff);
        tfAddress.setTextFieldFontColor(0x0033CC);
    }

    /**
     * paints two rectangles in the screen
     * @param g
     */
    protected void paint(Graphics g) {
        g.setColor(68, 165, 28);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(255, 255, 255);
        g.drawString("Name (max 20 chars):", 10, 10, 0);
        tfName.paint(g, 10, 40);
        g.setColor(255, 255, 255);
        g.drawString("Address (max 80 chars):", 10, 100, 0);
        tfAddress.paint(g, 10, 140);
    }

    /**
     * sets the text of the TextFieldArea destined to name
     * @param name
     */
    public void setName(String name) {
        tfName.setText(name);
    }

    /**
     * sets the text of the TextFieldArea destined to address
     * @param address
     */
    public void setAddress(String address) {
        tfAddress.setText(address);
    }

    /**
     * Detects if user points the first or the second textFieldArea in the
     * touch device
     * @param x
     * @param y
     */
    protected void pointerPressed(int x, int y) {
        if (x > 10 && x < 210) {
            if ((y > 40) && (y < 80)) {       //Pointer hits the first textfield

                BigKeyboard.instance.getName(tfName.getText());
            } else if ((y > 140) && (y < 220)) {  //pointer hits the second textfield

                BigKeyboard.instance.getAddress(tfAddress.getText());
            }
        }
    }
}