TextViewのMarqueeが動かない問題に終止符を

TextViewのMarqueeが動かない問題でいつも悩んでいるので色々なところを参考にして以下のクラスを作った。これならどの場面でも動くことは動く。

以下が参考になる。

http://stackoverflow.com/questions/1827751/is-there-a-way-to-make-ellipsize-marquee-always-scroll

http://saways.blogspot.jp/2011/10/textview-marquee.html

どちらも最終的な結論は「isFocuse()でtrueを返せ!」なんだけど、isFocuse()でtrueを返すとフォーカスがTextViewにあることになるのでキーボードが開けないという困ったことが起こる。なので、StackTraceからMarqueeに必要な関数から呼ばれているかを判定してtrueを返答するようにしてるんだろうけどこれだと関数名が変わったり、別の箇所から呼ばれたりとRefactorされただけで動かなくなってしまいそうで怖い。

実際にTextViewの中身を見ると && (isFocuse() || isSelected()) で判定をしているので、どちらかを返答すればよいのだからisSelected()でtrueを返答してあげたほうが実害が少ないのでこっちを採用することで僕の環境だと上手く動いた。XMLで色々やったりしてもどうしても動かない箇所が出てくるのでこれで動かすのが今のところのベスト。もし違う実装が見つかったらまた書くかも。

rxAndroidのsubscribeOnとobserveOnの違い

よくわからなかったので動かしながら試したけど結論は

スレッドを変えたかったらobserveOnを使え!

ってことだった。

お試し1

お試し2

解説

ログを取って確認してみると以下のように動いている。

subscribeOn
Observableの動き出しのスレッドを指定。最初に指定されたものが使われる。
observeOn
以降が動くスレッドを指定:

となっている。

僕の理解ではObservable.just()の部分もmainで動くのかと思ったけど、Observable.create()からsubscribeOnで指定されたスレッドで動くっぽいように見える。subscribeOnはLibraryとか配るときにObservable.create()を動かすスレッドを固定することが出来るっていうのが利点。ライブラリ作って配布するとかしないのであれば使う必要なくてスレッドを変えたいだけならobserveOnを使わなければならない。

(続)ListViewのEmptyViewが使えないので代替案を考える

これの続き

http://blog.choilabo.com/20150213/498

ViewTypeで分けてみる

前に作った方法でやるとどうしても無理矢理っぽい。なので、ViewTypeにEmptyを追加して0件なら表示するように変更してみた。

public class ListAdapter extends ArrayAdapter {

    // 表示するViewType達
    private enum ViewType {
        NORMAL(0),
        EMPTY(1);

        private int key;

        ViewType(int key) {
            this.key = key;
        }

        public int getKey() {
            return key;
        }

        public static ViewType getByKey(int key) {
            ViewType ret = null;
            for (ViewType viewType : values()) {
                if (key == viewType.getKey()) {
                    ret = viewType;
                    break;
                }
            }
            return ret;
        }
    }

    @Override
    public boolean isEmpty() {
        return (super.getCount() == 0 && mEmptyView == null) ? true : super.isEmpty();
    }

    @Override
    public int getCount() {
        // 0件かつEmptyViewが設定されていたらEmptyView分の1を返答する
        return (super.getCount() == 0 && mEmptyView != null) ? 1 : super.getCount();
    }

    @Override
    public int getViewTypeCount() {
        return ViewType.values().length;
    }

    @Override
    public int getItemViewType(int position) {
        // 0件かつEmptyViewが設定されていたらEmptyViewになる
        return (position == 0 && super.getCount() == 0 && mEmptyView != null) ?
                ViewType.EMPTY.getKey() : ViewType.NORMAL.getKey();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;

        switch (ViewType.getByKey(getItemViewType(position))) {
            case NORMAL:
                view = getNormalView(position, convertView, parent);
                break;
            default:
                // defaultになることはない。
            case EMPTY:
                view = getEmptyView(position, convertView, parent);
                break;
        }

        return view;
    }

    private View getEmptyView(int position, View convertView, ViewGroup parent) {
        // ここでEmptyViewを作成する
        return view;
    }

    private View getNormalView(int position, View convertView, ViewGroup parent) {
        // ここで通常のViewを作成する
        return view;
    }
}

まとめ

前回よりもこっちのほうがいいと思う。convertViewで再利用もちゃんとできるしいい感じ。setEmptyViewとかも合わせてOverrideした方がいいかも。

Toolbarでmarqueeを実現する

追記

こっちのほうが上手く動く

http://blog.choilabo.com/20161020/752


昔のWebサイトでよく見た文字が流れるやつはandroidでも実現できる。TextViewにellisize=marqueeを指定すれば良いんだけど、設定しただけじゃ動いてくれなくてfocusbleとかいろいろいれないといけない。

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:marqueeRepeatLimit="marquee_forever"
            android:scrollHorizontally="true"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:singleLine="true"/>

それで、ツールバーでも同じように設定すればいいかと思わせてそれじゃ動かない。なので、toolbarにmakrkerを設定したTextViewを入れてそこにタイトルを設定するっていう力技しか無いっぽい。

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView
            android:id="@+id/toolbar_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:marqueeRepeatLimit="marquee_forever"
            android:scrollHorizontally="true"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:singleLine="true"/>

</android.support.v7.widget.Toolbar>

コードからTitleを設定する

        mToolbar = (Toolbar) findViewById(R.id.toolbar);
        mToolbarTitle = (TextView) findViewById(R.id.toolbar_title);

        setSupportActionBar(mToolbar);
        mToolbarTitle.setText(getSupportActionBar().getTitle());
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

すごい力技。もうちょっときれいな方法はなかろうか。とりあえず動くは動く。ただ、ListViewとかだとこれでも動かない場合がある。その場合はフォーカスを与えると動く

        mToolbarTitle.setSelected(true);
        mToolbarTitle.requestFocus();

フラグメントでバックキーを押したイベントを受け取る

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {

        final View view = inflater.inflate(R.layout.fragment, container, false);

        // キーを受け取るようにして
        view.setFocusableInTouchMode(true);
        view.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK) {
                    // ここにバックキーで動かすコードを入れる
                    return true;
                }
                return false;
            }
        });
        return view;
    }

ListViewの先頭にアイテムを追加する

先頭に追加

先頭に追加するにはinsertを使うことで出来る。insertだとindexも選べるので0を指定すると先頭に追加できる。

mAdapter.insert(object, 0);

でもがくっとする

ListViewを見ている最中に先頭に追加すると追加されたView分Scrollがずれるので追加した瞬間にがくっとしてしまう。これは追加した瞬間に再描画しているからなので、再描画を止めれば良い。

mAdapter.setNotifyOnChange(false);

このままにしておくと先頭に行った時に表示されないので、先頭に行ったら再描画を再起動するようにする。

    @Override
    public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        super.onScroll(absListView, firstVisibleItem, visibleItemCount, totalItemCount);

        if(先頭かどうかの判定) {
            mAdapter.setNotifyOnChange(true);
            mListAdapter.notifyDataSetChanged();
        }

まとめ

こんな感じで出来ると思う。一番下にいったら自動で読み込むようにしている場合も同じようにsetNotifyOnChange(true)をやるかnotifyDataSetChanged()をやらないといけないのでご注意を。

プログラムからxmlの値を取得する

カスタムビューを作った時にxmlで指定している値をプラグラムから取リたい場合によく使う。

カスタム値じゃない場合

android.Rで指定されているものは簡単

        int[] attrArray = {android.R.attr.background};
        TypedArray typedArray = context.obtainStyledAttributes(attrs, attrArray);
        Color color = typedArray.getColor(0, Color.TRANSPARENT);

カスタム値の場合

ユーザが任意の値を設定したい場合はXMLから作る必要がある。XMLは「/values/attrs.xml」として作る。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomAttr">
        <attr name="custom_value" format="color"/>
    </declare-styleable>
</resources>

そしてこんな感じで取得する

        TypedArray typedArray = obtainStyledAttributes(attrs, R.styleable.CustomAttr);
        Color color = typedArray.getColor(0, Color.TRANSPARENT);

ここでの0はCustomAttrのインデックスになる。なので0がcustom_valueを表していることになる。

Activityのスタックをクリア(全削除)する方法で勘違いされている場合が多い

よくある勘違い

色々なところで書いてある以下のコードとその説明のニュアンスがちょっと違う

Intent intent = new Intent(getApplicationContext(), NextActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Intent.FLAG_ACTIVITY_CLEAR_TOP
呼び出すActivity以外のActivityをクリアして起動させる
Intent.FLAG_ACTIVITY_NEW_TASK
スタックに残っていても、新しくタスクを起動させる

以下の説明のほうが正しいと思う。

Intent.FLAG_ACTIVITY_CLEAR_TOP
起動されるAcitivityより前のスタックのAcitvityをクリアして起動させる

例を上げて考えてみると

A → B → C → D → E → F

この状態でAを起動するとそれより上にあるAcitivtyがクリアされるからAだけになるけどCを起動したとすると

A → B → C

こうなる。このスタックに存在しないGというアクティビティを起動した場合は

A → B → C → D → E → F → G

こうなる。つまり「呼び出すActivity以外のActivityをクリアして起動させる」というのは違う。

まとめ

スタックで戻りたくない場合というのはログアウトしてログイン画面へと無理やり遷移し、ログイン後に使用できる画面を表示させたくない場合だと思う。例で記載しているようにAというAcitivityが存在しないと全てのスタックをクリアすることは難しい。そのため基底のActivityを作り、そのActivityが絶対に存在するという状況を作り出す必要がある。この基底のAcitivtyが絶対存在するという状況を作るのがなかなか難しいわけだが、理屈さえわかればstartActivityForResult()とonActivityResult()を使ってうまく作れそうな気がする。


SwipeCardにCSV読込機能をつけました!

要望がありましたので、ファイル読み込み機能をつけました!

ファイル形式を変更しなければならないかもしれませんが以下の形式のCSVであれば読み込むことが可能です。

  • 半角「,」区切り
  • 1列目に「単語」、2列目に「意味」
  • 文字コードはUTF-8

今までは私が作成していた英単語データしか使うことができませんでしたが、お手持ちの単語データを使用することができるようになったため、色々な暗記に使えるようになったと思われます。

他にも追加してほしい機能のご要望がありましたら随時追加していこうと思いますので、「SwipeCard」を引き続きご利用いただけるとありがたいです。

https://play.google.com/store/apps/details?id=com.choilabo.android.englishstudycard

※デフォルトで選択できるデータを増やしたいので、もし自分が持っている単語データを配布しても構わないという人がいましたらGoogle Playのユーザ宛に単語データを送付していただけるとありがたいです。

ListViewのEmptyViewが使えないので代替案を考える

ListViewは便利なんだけど表題の通りEmptyViewが使いものにならない。

普通の使い方

普通にListViewのEmptyViewを使おうとするとこんな感じになると思う。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    <LinearLayout
            android:id="@+id/empty_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

        <!-- EmptyViewの中身 -->

    </LinearLayout>

</FrameLayout>

このレイアウトでコードはこんな感じ

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);
        
        ListView listView = (ListView) findViewById(R.id.list_view);
        LinearLayout emptyView = (LinearLayout) findViewById(R.id.empty_view);
        
        listView.setEmptyView(emptyView);
        
        // その後の処理
    }

これでListViewに設定しているAdapterが0件かどうかでEmtpyViewをVisible or Goneへと制御してくれる。0件表示も簡単にできると喜び勇みたいところだけど実はそうじゃない。以下の問題点がある。

  • Adapterの件数が0件の場合は容赦なく0件表示が表示される。EmptyViewだからそうなんだろうけど、リロードしたりして一瞬でも0件になるとEmptyViewがチラ見えしてしまう。
  • ヘッダー、フッターも消えてしまう。ListViewとEmptyViewを入れ替えているだけなのでヘッダー、フッターが設定していようがしていなかろうが容赦なく消えてしまう。

詳しくはやんざむ先生が書いてるんでそっちを見るといいと思う。

http://y-anz-m.blogspot.jp/2012/04/android-listview.html

理屈はわかったんだけど、ヘッダーを表示して0件表示したいってことは結構ある。やんざむ先生が言われてる方法もできるんだけど、これだとViewは出来上がるけど別のインスタンスなのでイベントの設定とかが非常に煩わしい。なので無理やり設定してしまおうというのが今回の趣旨。具体的にはこんな感じ。

    private View mEmptyView;

    /**
     * 0件表示の表示する
     */
    public void showEmptyView(View emptyView) {
        if (emptyView != null && isEmpty()) {
            // 渡された0件表示を無理やり表示するために適当な値を作成
            mEmptyView = emptyView;
            add((T) new Object());
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        // 0件表示
        if (mEmptyView != null) {
            return mEmptyView;
        }

        return super.getView(position, convertView, parent);
    }

getViewをOverrideしてカスタムビューを返答するというのはよくあると思うんだけど、ここをちょこっと弄くっとEmptyViewが設定されてたらそれを返答してしまうことでListViewの列としてEmptyViewを表示してしまおうという考え方。これだとヘッダーも表示できるし、ListViewの中にEmptyViewが表示できるしで結構良い。問題としてはListViewに追加してるからonItemClickが走ってしまって変な値がとれる場合を考えないといけないってぐらいかと思う。今のところそこまで問題は起こっていない。mEmptyViewの初期化さえ間違えなければちゃんと動くはず。

まとめ

EmptyViewの問題は色々調べてみたんだけど結構根深そうで、あっちをたてればこっちがたたずと非常に悩ましい。ヘッダー、フッターを表示したままEmptyが表示できるとか、Emptyだけじゃなくて初期表示を弄くれるとかそこらへんまで充実してくれるとありがたいなー