SwipeCardに学習結果のリセット機能をつけました!

SwipeCardに要望がありました以下の機能追加・修正をしました。

  • 学習結果のリセット
  • 単語帳で全てのデータを学習完了するとクラッシュする不具合修正
  • 学習が始まっていない状態でテストを開始するとクラッシュする不具合修正
  • その他軽微なバグ修正

引き続きSwipeCardで楽しい学習ライフを送って下さい!

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

何か要望がありましたらTwitterまでご連絡下さい。

@sathu20xx

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だけじゃなくて初期表示を弄くれるとかそこらへんまで充実してくれるとありがたいなー

WordPressでTwitterに同時投稿する

Twitterと投稿連携したいと思っていたのでやってみた。動作確認がてらやったことを書いてみる

プラグインのインストール

余計なものがついてないのがいいと思って「WP to Twitter」を選択した。

設定

以下の項目を入れないといけない

  • Consumer Key
  • Consumer Secret
  • Access Token
  • Access Token Secret

APIとして公開されるわけじゃないのでさくっとやる。

Twitterにアプリ登録

以下から登録。値とかは他に公開しないし適当でいい。権限だけ「Read and Write」にしましょう。

https://apps.twitter.com/

アクセストークンの作成

「keys and access tokens」から自分用のトークンを作れるみたい。便利になったね。

まとめ

以上で飛ぶはず。飛ばなかったら書き直す。

Support v7のToolbarのテキストスタイルを変更する

結論android:titleTextAppearanceではなくて、app:titleTextAppearanceを設定すれば変わる

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:titleTextAppearance="@style/TextAppearance.Toolbar"
        android:minHeight="?attr/actionBarSize"/>

一応、TextAppearanceはかんな感じで設定

    <style name="TextAppearance.Toolbar" parent="TextAppearance.AppCompat.Medium">
        <item name="android:textSize">12sp</item>
    </style>

カレントバージョンが21以上だと動くかも。試してないからしらない。

参考資料

http://stackoverflow.com/questions/26453409/support-actionbar-wont-display-the-right-color-with-api-21