2010年12月26日日曜日

Android Onscreen Input Methods

ソフトウェアキーボードによって画面の領域がせまくなってしまうので、それに配慮した設計を行わないと、入力できなーい!なんてことになってしまいます。

そこで、画面上に出てくる入力メソッド(=ほとんとソフトウェアキーボード)についてまとめたいと思います。

元ネタはこちら

Onscreen Input Methods

Android 1.5 から Android プラットフォーム で Input Method Framework (IMF) が提供されるようになりました。これによって、開発者は ソフトキーボードのような画面上からの入力方法を作成することができるようになりました。

 ・ Android input method editors (IMEs) の概要
 ・ IMF, IMEs と一緒に動くアプリケーションにはなにが必要か

について簡単に説明してきます。

What is an input method?

 Android IMF は、ソフトウェアキーボードや手書き認識入力やハードキーボード変換器など、複数の IMEs をサポートするようにデザインされています。しかし、ここではソフトウェアキーボードにフォーカスします。ソフトウェアキーボードによる入力方法が現在のプラットフォームの一部だからです。

 一般的には、ユーザーはテキスト入力するために、入力領域をタップして現在の IME にアクセスします。

 ソフトキーボードは画面の下部に、アプリケーションのウィンドウに被さるように配置されます。アプリケーションと IME 双方が使用可能な領域を作れるように、いくつかのアプローチをとっています。
 
 一つは、pan and scan と呼ばれるものです。これは単純に現在フォーカスされている View が見えるようにアプリケーションウィンドウのまわりをスクロールします。これがデフォルトのモードです。

pan and scan




 pan and scan の次に最も適用されるレイアウトはリサイズです。この場合、アプリケーションウィンドウは全体が見えるようにリサイズされます。たとえば、e-mail メッセージを編集しているときなどはスクロールできるようにリサイズされます。

resize



右側にスクロールバーが出現したのは、この編集領域(3つのEditTextのレイアウト)がリサイズできるような設定だったから(だと思う)

 アプリケーションウィンドウのサイズが変更されるため、IME によって隠される部分がなくなり、アプリケーションと IME 両方にフルアクセスできます。もちろん、アプリケーションが(十分なスペースを作るために)リサイズによって縮小可能な領域をもつ場合にのみ働きますが、このモードでの垂直方向の領域は、横向きで使用可能な領域より小さくなるので、多くのアプリケーションはすでに対応できています。


 もう一つのメジャーなモードが fullscreen or extract mode です。これは、アプリケーションと合理的にスペースを分け合うには IME が大きすぎる場合に使われます。標準の IMEs では、横向き画面のときにのみ遭遇することがあるでしょう。他のIMEでは、必要なときにいつでも自由にこのモードを使うことができます。この場合、アプリケーションウィンドウはそのままの状態で残り、単純に IME がその上にフルスクリーンで表示されます。




入力領域も IME の一部

IME がアプリケーションを覆ってしまうため、アプリケーションに入力される文字を表示するための入力領域を IME 自身が持ちます。そのため、ユーザー体験を向上させるために、アプリケーションがこの領域を限定的にカスタマイズできます。(例えば done ボタンに表示される文字列を変更するなど)



Basic XML attributes for controlling IMEs

 IMEs とアプリケーションをよりよくコンビネーションさせるためにシステムが行っていること

  * デフォルトでは pan and scan モードを使う。
   ただし、list や scroll view などの存在によってリサイズモードが
   働くと合理的に推測できる場合を除く
  * TextView 属性を解析して、ソフトキーボードを適切なキー配置で表示する
  * "next field" や "done" などのデフォルトのアクションを
   fullscreen IME に割り当てる

Specifying each EditText control's input type

 * EditText で android:inputType 属性を使う

(この属性は新しい属性で、android:password, android:singleLine, android:numeric, android:phoneNumber, android:capitalize, android:autoText, and android:editable などの古い属性を置き換える。もし古い属性と新しい属性を両方設定した場合、システムは android:inputType を使い、古い属性は無視する。)


android:inputType 属性の 3 パーツ

  * class : 文字の全体的な解釈
    * text (plain text)
    * number (decimal number)
    * phone (phone number)
    * datetime (a date or time)

  * variation : class をより洗練する
  (通常 class と variant 両方指定する)
    * textEmailAddress : キー配列は'@'を含む(べき)
    * numberSigned : 記号付の数値フィールド

  * flag : さらなる改善を提供する(flag は class に特有のもの)
    * text class のフラグ
      * textCapSentences
      * textAutoCorrect
      * textMultiline

例えば

<EditText android:id="@+id/edtInput"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine"
android:imeOptions="actionSend|flagNoEnterAction"
android:maxLines="4"
android:maxLength="2000"
android:hint="@string/compose_hint"/>



Enabling resize mode and other window features

input method に関連する window の振る舞いを指定する。
AndroidManifest.xml の各 <activity> 定義で android:windowSoftInputMode 属性を指定することで、振る舞いをコントロールできる。いくつかの設定を組み合わせることが可能。

  * ウィンドウの調整モードを指定する
    * adjustResize か adjustPan
    * 常にどちらかを指定することを推奨する

  * Activity が表示されたときに、IME が自動で表示されるか
   どうかコントロールする
    * stateVisible : IME を自動で表示する
    * その他、stateUnspecified, stateUnchanged,
     stateHidden, stateAlwaysHidden, stateAlwaysVisible

例えば

<Activity name="EditContactActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
...
</activity>


Activity ではないウィンドウからこの振る舞いをコントロールするには setSoftInputMode(int) メソッドを使います。


Controlling the action buttons

IME の "action" ボタンをカスタマイズする

2 つの action タイプ

  * 複数行編集 EditText 以外の場合、ソフトキーボード上の
   エンターキーは action にバインドされます。
   例えば、次のフィールドに移動したり、アプリケーションが
   action を実行するために割り込んだり。

  * fullscreen モードでは、一般的なアプリケーションの操作に
   簡単にアクセスするために、IME は付加的なアクションボタンを
   編集テキストの右側に表示します。

これらの操作は TextView の android:imeOptions 属性でコントロールできます。
以下の組み合わせを指定できます。

  * 事前に定義されている action 定数の一つ
    (actionGo, actionSearch, actionSend, actionNext, actionDone)
   指定されていない場合、システムは次にフォーカスできる
   フィールドがあるかどうかで、actionNext か actionDone の
   いずれか選択する。
   action しないように強制するには actionNone を指定する

  * flagNoEnterAction を指定すると、たとえ複数行テキスト
   ではなくても、enter key での action を使用できないようにする。
   例えば、タイピング中に誤って触れることで、送信などの
   回復不可能な action が実行されてしまうのを避けることができる。

  * flagNoAccessoryAction を指定すると、action ボタンを
   テキストエリアから削除できる

  * flagNoExtractUi を指定すると、完全にテキストエリアが
   削除され、背後にアプリケーションが見えるようになる


APIs for controlling IMEs

Context.getSystemService() で取得できる android.view.inputmethod.InputMethodManager class は、グローバルな input method の状態とインタラクトできる。例えば、IME's の入力領域を明示的に隠したり出したりできる。

Window.addFlags() メソッドや Window.setSoftInputMode() メソッドを使ってコントロールすることも可能。

-------------------------------------------------------

 さて、ここからがメインです。



<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="To"
android:inputType="textEmailAddress"
/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="Subject"
android:inputType="text"
/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Compose Mail"
android:gravity="top"
android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine"
/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Send"
/>
</LinearLayout>





AndroidManifest.xml に android:windowSoftInputMode="adjustPan" とすると、コンテンツ部分が上にスクロールできるようになります。





AndroidManifest.xml に android:windowSoftInputMode="adjustResize" とすると、android:layout_weight="1" を指定した部分がリサイズされてスクロールされなくなります。



fullscreen モードの場合、To は次のフォーカスがあたる widget も EditText なので、ボタンの表示が Next になっていて、Composing Mail では次が Button なので Done になっている





android:imeOptions="flagNoAccessoryAction"


android:imeOptions="flagNoExtractUi"


android:imeOptions="actionGo"
右下のキーが Go になる


android:imeOptions="actionSearch"
右下のキーが検索マークになる


android:imeOptions="actionSend"
右下のキーが Send になる



# 横画面で入力フィールドのあるダイアログを表示する場合は注意が必要なのですが、それはまた次のエントリで書きます。。。



 

1 件のコメント:

  1. Android IMF は、ソフトウェアキーボードや手書き認識入力やハードキーボード変換器など、複数の IMEs をサポートするようにデザインされています。しかし、ここではソフトウェアキーボードにフォーカスします。ソフトウェアキーボードによる入力 ... キーボード.blogspot.com

    返信削除

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,''):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

Blogger Syntax Highliter

Version: {V}

http://www.dreamprojections.com/syntaxhighlighter

©2004-2007 Alex Gorbatchev.

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,'\n'):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

ページビューの合計