読者です 読者をやめる 読者になる 読者になる

tumblr

tumblr(タンブラー)は、メディアミックスブログサービス。ブログとミニブログ、そしてソーシャルブックマークを統合したマイクロブログサービスである。アメリカのDavidville.inc(現: Tumblr, Inc.)により2007年3月1日にサービスが開始された。

SwiftでMultiAutoCompleteTextViewみたいなのを作る

AndriodにはMultiAutoCompleteTextViewというViewがある。ワードの補完候補を表示してくれて、カンマ区切りで複数回候補表示してくれるやつ。 iPhoneにはそういうViewが無いし、オープンソースでもそういうTextFieldが無い。何故無いのか、そもそもソフトウェアキーボードの補完だけで事足りるのか、使いたい状況があまり無いのかはよく分からない。が、使いたかったので作った。

f:id:shim0mura:20160925233343g:plain

github.com

作ったと言っても、若干似たライブラリ(https://github.com/mnbayan/AutocompleteTextfieldSwift)はあったので、それを少し改造しただけ。

インストール

cocoapodに登録してあるので、他のライブラリと同じくPodfileに以下を追加

pod 'MultiAutoCompleteTextSwift'

またはcarthageを使ってCartfileに以下を追加

github "shim0mura/MultiAutoCompleteTextSwift"

使い方

TextFieldにMultiAutoCompleteTextFieldをClassとして設定して、autoCompleteStringsに候補ワードのArrayを設定

import MultiAutoCompleteTextSwift

@IBOutlet weak var textField: MultiAutoCompleteTextField!
override func viewDidLoad() {
    super.viewDidLoad()
    let words = [ "ruby", "rust", "mruby", "php", "perl", "python"]
    textField.autoCompleteStrings = words
}

そうすると上のgif画像みたいな感じで候補が出て来る。

ローマ字で候補表示

「スウィフト」という文字を入力したい場合、AndroidのMultiAutoCompleteTextViewはデフォルトでは「スウ」と入力しないと候補として「スウィフト」が表示されない。できればローマ字の段階、この例であれば「suwi」と入力した時とか、ひらがなで「すう」と入力した段階で「スウィフト」が出てきて欲しい。そういうときのために、内部で「suwifuto」とか「すうぃふと」を「スウィフト」と紐付けられるようにしてある。

let token = MultiAutoCompleteToken(top: "スウィフト", subTexts: "suwifuto", "すうぃふと", "swift")
textField. autoCompleteTokens.append(token)

StringじゃなくてMultiAutoCompleteTokenという独自のクラスを作ってそのインスタンスに出したい候補をもたせて、その候補ワードと入力文字列に一致するところがあるか片っ端から単純比較してるだけ。

UITableViewはいじる必要があるかも

候補ワードはUITableViewで表示している。ただ、TextFieldの下に何か別のViewがあるとTableViewとそのViewが重なるため、入力時はTextFieldと下のViewのconstraintをうまくいじってやる必要がある。onTextChangeとonSelectというコールバックがあるので、それをうまいこと使う。

textField.onTextChange = {[weak self] str in
                        self!.marginBetweenTextFieldBottomView.constant = 100
        }

FilteredArrayAdapterでローマ字入力からサジェスト

android

前回のTokenAutoCompleteの使い方の進んだ版みたいなの。

普通にTokenAutoCompleteを使っただけだと「レイルズ」というタグをサジェストしたいとき、「ra」と入力しても候補は出てこない。「レイ」と入力しないと候補が出てこない。なので「rei」とローマ字入力してカタカナに変換した時点でやっと出てくる。わざわざ変換せず「rei」と入力した時点で「レイルズ」というタグが出てきて欲しいけどそのままじゃそうはいかないので、FilteredArrayAdapterを使ってカスタムフィルターを作ってサジェストさせる。FilteredArrayAdapterはTokenAutoCompleteに含まれているクラス。

カスタムフィルタの作り方

基本は前回のTokenAutoCompleteと同じ。Viewクラスと候補表示用のレイアウト、タグエンティティクラスを作成する。 ただ、今回はローマ字を元にタグをサジェストさせたいのでちょっとだけタグエンティティクラスに細工をしておく。

public class TagEntity implements Serializable {

    private String name;
    private String yomiRoma;

    public TagEntity(){}

    public TagEntity(String name, String yomiRoma){
        this.name = name;
        this.yomiRoma = yomiRoma;
    }

    public String getName() {
        return name;
    }

    public String getRoma() {
        return yomiRoma;
    }

    public String toString(){
        return name;
    }

}

ローマ字での表記を格納できるようにしておく。

アクティビティに組み込む

Acticity側では、ArrayAdapterを使わずにFilteredArrayAdapterを使う。その時にkeepObjectメソッドをオーバーライド。

public class TokenAutoCompActivity extends AppCompatActivity {

    private AutoTagCompletionView completionView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_token_auto_comp);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        TagEntity tags[] = new TagEntity[]{
                new TagEntity("アンドロイド", "android"),
                new TagEntity("レイルズ", "reiruzu"),
                new TagEntity("ルビイ", "rubii")
        };

        ArrayAdapter<TagEntity> adapter = new FilteredArrayAdapter<TagEntity>(this, android.R.layout.simple_list_item_1, tags) {

            @Override
            protected boolean keepObject(TagEntity tag, String mask) {
                mask = mask.toLowerCase();
                return tag.getName().toLowerCase().startsWith(mask) || tag.getRoma().startsWith(mask);
            }
        };

        completionView = (AutoTagCompletionView)findViewById(R.id.tag_comp);
        completionView.setAdapter(adapter);
    }

}

keepObjectは文字を入力する度に呼び出され、その都度各タグが候補になるかをチェックしてくれる。なので、このメソッドの中でカスタムフィルタとしてやりたいことを書けばいい。今回は入力文字とタグのnameまたはローマ字での入力が重なっているかをチェックしている。やろうと思えば複雑な処理もこの中で出来るので色々なフィルタリングが可能。ただ、1文字入力または削除する度に追加したタグの回数分だけkeepObjectが呼ばれるので、余り複雑なことはしないほうがいいかもしれない。

f:id:shim0mura:20160501232611p:plain

なんにしても、これでローマ字からでもタグのサジェストができるようになった。

TokenAutoCompleteを使ってandroidでタグ入力

android

f:id:shim0mura:20160501220828p:plain

画像のようにJqueryTagItみたいなのがandroidでも使いたい。

jQuery Tag-it!

で、そういう時はTokenAutoCompleteというライブラリを使うといい。 github.com

使い方

一行xmlに追加して終わり、という感じじゃなくてちょっと使い方が面倒。色々と作成しないといけない。

セットアップ

dependencies {
    compile "com.splitwise:tokenautocomplete:2.0.7@aar"
}

android studioならばこんな感じでセットアップする。

Viewクラスを用意する

まずはタグの入力支援してくれるViewクラスは自作しないといけない。といってもTokenAutoCompleteに元となるクラスがすでに用意されているので、それを継承したクラスを作る。

public class AutoTagCompletionView extends TokenCompleteTextView<TagEntity> {
    public AutoTagCompletionView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected View getViewForObject(TagEntity tag) {

        LayoutInflater l = (LayoutInflater)getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        LinearLayout view = (LinearLayout)l.inflate(R.layout.tag_comp, (ViewGroup) AutoTagCompletionView.this.getParent(), false);
        ((TextView)view.findViewById(R.id.name)).setText(tag.getName());

        return view;
    }

    @Override
    protected TagEntity defaultObject(String completionText) {
        TagEntity tag = new TagEntity();
        tag.setName(completionText);
        return tag;
    }
}

getViewForObjectはオートコンプリートでタグ候補表示をするときのViewを展開するもの。なのでタグ候補表示するためのレイアウトファイルもいる。 defaultObjectはタグ入力した時、AutoTagCompletionView(クラス名はなんでもいい)からタグオブジェクトを取得するためのメソッド

候補表示レイアウト

これは別にListViewなんかでデフォルトで使われてるsimple_list_item1とか使いまわしてもOK。とりあえず公式に合わせたものを適当に作っておく。

R.layout.tag_comp

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content">

    <TextView android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/token_background"
        android:padding="5dp"
        android:textColor="@android:color/white"
        android:textSize="18sp" />

</LinearLayout>

タグエンティティを作る

これも適当に自分で好きなように作る。公式ではPeopleクラスを作っていたけど、今回はTagEntityクラスを作ってみる。

public class TagEntity implements Serializable {

    public String name;

    public TagEntity(){}

    public TagEntity(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String toString(){
        return name;
    }

}

これで準備完了。

アクティビティに組み込む

さっき作った自作のViewクラス(今回はAutoTagCompletionView)をActivityのxmlに入れる。

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="タグ"/>
        <shim0mura.testtag.view.AutoTagCompletionView
            android:id="@+id/tag"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

あとはActivity側での処理。表示させたいタグを一気に作って、ArrayAdapterに渡せばOK。

public class TokenAutoCompActivity extends AppCompatActivity {

    private AutoTagCompletionView completionView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_token_auto_comp);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        TagEntity tags[] = new TagEntity[]{
                new TagEntity("android"),
                new TagEntity("rails"),
                new TagEntity("ruby")
        };

        ArrayAdapter<TagEntity> adapter = new ArrayAdapter<TagEntity>(this, android.R.layout.simple_list_item_1, tags);

        completionView = (AutoTagCompletionView)findViewById(R.id.tag);
        completionView.setAdapter(adapter);
    }


}

これで完成。エミュレータで表示させてみるとこんな感じ。

f:id:shim0mura:20160501222827p:plain

android」のanを入力すると候補のタグが表示される。この候補のレイアウトがこの記事で書いてるところのR.layout.tag_comp。で、その候補をタップすると、

f:id:shim0mura:20160501223033p:plain

いい感じにタグっぽく入力できた。

オプションで色々できる

タグのサジェスト自体はArrayAdapterに渡したタグしかしてくれないけど、そうではないタグも自分で入力できる。カンマ区切りやEnterなどの決定ボタンで独自のタグも入力可能。ただし、デフォルトはカンマ区切りなので、半角スペースとか全角スペース区切りにも対応したい。そういう場合はsetSplitChar(' ')としてやればOK。

completionView.setSplitChar(' ');

が、これだと半角スペースしか対応しなくなって今度は逆にカンマ区切りに対応しなくなるので、以下のように複数の区切り文字を定義してからセットするといいっぽい。

char[] splitChar = {',', ';', ' '};
completionView.setSplitChar(splitChar);

あとはタグ候補を表示する閾値も設定できる。デフォルトでは2文字以上入力された時、たとえば今回のようなタグであれば「ru」と入力したところで「ruby」というタグ候補が出てくる。「r」とだけ入力した時に、「ruby」と「rails」のタグ候補が出て欲しい場合、つまり閾値が1文字にしたい場合は以下のようにする。

completionView.setThreshold(1);

こうすると

f:id:shim0mura:20160501224016p:plain

こんな感じで両方とも出てきてくれる。 公式のサンプルはgmailのようにメールアドレスの補完とメールアドレスからのPeopleオブジェクトの取得とかやってるし、使い方次第で結構幅が出てきて面白い。

MultiAutoCompleteTextView SpaceTokenizer

android

MultiAutoCompleteTextViewは複数の文字列をオートコンプリート出来る。デフォルトだと1つめの文字列をオートコンプリート入力してカンマを入力すると、2つ目の文字列を入力できるようになる。が、これはカンマ区切りしか対応していない。スペースと区切り文字としても利用したいところだけど、その場合はSpaceTokenizerを自作しないといけない。

public class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer {

    public int findTokenStart(CharSequence text, int cursor) {
        int i = cursor;

        while (i > 0 && text.charAt(i - 1) != ' ') {
            i--;
        }
        while (i < cursor && text.charAt(i) == ' ') {
            i++;
        }

        return i;
    }

    public int findTokenEnd(CharSequence text, int cursor) {
        int i = cursor;
        int len = text.length();

        while (i < len) {
            if (text.charAt(i) == ' ') {
                return i;
            } else {
                i++;
            }
        }

        return len;
    }

    public CharSequence terminateToken(CharSequence text) {
        int i = text.length();

        while (i > 0 && text.charAt(i - 1) == ' ') {
            i--;
        }

        if (i > 0 && text.charAt(i - 1) == ' ') {
            return text;
        } else {
            if (text instanceof Spanned) {
                SpannableString sp = new SpannableString(text + " ");
                TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
                        Object.class, sp, 0);
                return sp;
            } else {
                return text + " ";
            }
        }
    }
}

MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.comp);
textView.setTokenizer(new SpaceTokenizer());

参考:

android - How to replace the comma with a space when I use the "MultiAutoCompleteTextView" - Stack Overflow