[Android] ListViewでハマったこと

[もはや過去の産物。今はRecycleViewでも使ってもっとマシなもの書きましょう。]

Androidアプリ開発は初心者なので、プログラマさんのブログをあっちこっちしながら
開発を進めているのですが、ListViewで何点かハマった(手こずった)ところがあるので、
備忘録程度に書き起こしたいと思います。

主にTwitterクライアント制作時の出来事です。
(最善策とは思えません(´∀`;なんせ初心者なので…。もっと良い方法がございましたら
コメント欄で教えてもらえると嬉しいです(*^_^*)!)

スクロールロックとアニメーション

Twitterクライアントを制作していると、ストリームで流れてくるツイートを先頭行に
自動的に追加していかなければなりません。その時に下の方へスクロール中だった場合、
ツイートが追加されると勝手にスクロールされてしまうので、ツイートを追うことが困難
(というか、ユーザへのストレス)になります。

また、先頭を表示中に追加された場合、スムースな追加アニメーションをしてほしいと思います。
普通にInsertしただけだと「パッ」と更新されるだけで味気ないですからね。実はこれが案外簡単だったんです。

まずはスクロールロック。実は、自分コレに対するよい対策方法が分かっていません(´-ω-`
思い浮かぶのは、

  • スクロール中は行を追加しない
  • 追加後に元の位置へ戻す

でしょうか…。本当は前者の方がよさそうですが、なんだか処理が複雑になりそうなので、
自分は後者の方でまかなっています。やり方は、

  1. 追加後に現在の表示(スクロール)位置を取得
  2. 先頭行でない場合に現在の位置+1をする

という感じです。

次にインサートアニメーション?ですが、これは結構悩みました。Anim自作してやるのか
とか行の幅を変えてってとか…。で、辿り着いたのが「追加後にsmoothScrollToPosition(0)を呼ぶ」
です!

具体的なコードは、

// 行追加
mAdapter.insert(status, 0);
// アダプタにデータ変更を通知
mAdapter.notifyDataSetChanged();
// 現在表示している行が先頭かどうか
if (getListView().getChildAt(0).getTop() == 0) {
    // 一旦1行分下に戻す
    getListView().setSelectionFromTop(
             getListView().getFirstVisiblePosition() + 1,
             getListView().getChildAt(0).getTop());
    // 先頭ならばスムースに先頭へ
    getListView().smoothScrollToPosition(0);
} else {
    // 1行分下に戻す
    getListView().setSelectionFromTop(
             getListView().getFirstVisiblePosition() + 1,
             getListView().getChildAt(0).getTop());
}

ちょっと”1行分下に戻す”が重複している気もしますが…。大体これで要望はかないました。しかし、1点デメリットがあります。
それはこの「1行分下に戻す」なのですが、どうやら無理やりすぎるようなのです。
というのは、どうしてもチカチカしてしまうのです(´・ω・`) この事が今後の課題です。

スクロールアニメーション

今度はリストビューを下にスクロールしたときのアニメーションですね。
これも無理やりなところと欠点があるのですが……(´∀`;

まず、アニメーション定義XMLを作成します。これはよくある感じですね。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="350" >

    <alpha
        android:fromAlpha="0"
        android:toAlpha="1.0" />

    <translate
        android:fromXDelta="0"
        android:fromYDelta="100%"
        android:toXDelta="0"
        android:toYDelta="0" />

</set>

最近のカードUIでよく使う?感じです。

次に、アダプタでViewに付加します。

// Classレベルで alreadyAnimated を宣言しておく
/* int alreadyAnimated = 0; */

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final Status item = getItem(position);
        if (null == convertView) {
            convertView = mLayoutInflater.inflate(
                    R.layout.listview_timeline_row, null);
            // null でEclipseに怒られるけどわからんから無視
        }

        if (alreadyAnimated < position) {
            Animation anim = AnimationUtils.loadAnimation(getContext(),
                    R.anim.listview_anim);
            // convertView.startAnimation(anim);
            convertView.setAnimation(anim);
            // なんかこちらでもOK?
            alreadyAnimated = position; // アニメ済み位置にする
        }
        return convertView;
    }

こんな感じですが、問題なのが”Insert(item,0)されたときにアニメ済みがズレる”こと
ですね。がんばればなんとかできそうな気もしますが、ご愛嬌かな(´-ω-`

リスト(行)がタップできない?!

なんだかんだ出来てきたタイムラインが完成してきた最中、なんとリストがタップできない
という本末転倒的な事態が起きました(´_`。)グスン

原因はどうやらinflateしているカスタム行(row)にあるようです(`・ω・´)!
こうした現象は、行内のレイアウトにButtonやTextViewがあると起こるらしいのですが、
Buttonは無いしTextViewがなかったら話にならないしで…

こちらがその行XML((呼び方わからない( ̄ー ̄;

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

    <View
        android:id="@+id/user_color_bar"
        android:layout_width="5dp"
        android:layout_height="match_parent"
        android:background="@color/white" />

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:paddingLeft="0dp" >

        <com.loopj.android.image.SmartImageView
            android:id="@+id/user_icon"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_alignParentTop="true"
            android:contentDescription="@null"
            android:src="@drawable/progress_spinner" />

        <LinearLayout
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:layout_toRightOf="@+id/user_icon"
            android:orientation="horizontal" >

            <TextView
                android:id="@+id/user_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:singleLine="true"
                android:text="@string/text_name_sample"
                android:textSize="@dimen/font_size_micro" />

            <TextView
                android:id="@+id/user_screen_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:paddingLeft="5dp"
                android:singleLine="true"
                android:text="@string/text_screenname_sample"
                android:textSize="@dimen/font_size_micro" />

            <TextView
                android:id="@+id/time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/text_date_sample_rel"
                android:textSize="@dimen/font_size_micro" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/header"
            android:layout_below="@+id/header" >

            <TextView
                android:id="@+id/tweet"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/text_tweet_sample"
                android:textSize="@dimen/font_size_small" />

            <TextView
                android:id="@+id/fav"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="right"
                android:text="@string/text_star"
                android:textSize="@dimen/font_size_small" />
        </LinearLayout>

        <HorizontalScrollView
            android:id="@+id/images"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/content"
            android:layout_below="@+id/content" >

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal" >

                <com.loopj.android.image.SmartImageView
                    android:id="@+id/tweet_image"
                    android:layout_width="95dp"
                    android:layout_height="95dp"
                    android:scaleType="centerCrop"
                    android:src="@drawable/ic_menu_refresh" />

                <com.loopj.android.image.SmartImageView
                    android:id="@+id/tweet_image2"
                    android:layout_width="95dp"
                    android:layout_height="95dp"
                    android:scaleType="centerCrop"
                    android:src="@drawable/ic_menu_refresh" />

                <com.loopj.android.image.SmartImageView
                    android:id="@+id/tweet_image3"
                    android:layout_width="95dp"
                    android:layout_height="95dp"
                    android:scaleType="centerCrop"
                    android:src="@drawable/ic_menu_refresh" />

                <com.loopj.android.image.SmartImageView
                    android:id="@+id/tweet_image4"
                    android:layout_width="95dp"
                    android:layout_height="95dp"
                    android:scaleType="centerCrop"
                    android:src="@drawable/ic_menu_refresh" />
            </LinearLayout>
        </HorizontalScrollView>

        <TextView
            android:id="@+id/descriptions"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/images"
            android:layout_below="@+id/images"
            android:singleLine="true"
            android:text="@string/text_descriptions_sample"
            android:textSize="@dimen/font_size_micro" />

        <com.loopj.android.image.SmartImageView
            android:id="@+id/retweeter_icon"
            android:layout_width="35dp"
            android:layout_height="35dp"
            android:layout_alignRight="@+id/user_icon"
            android:layout_alignTop="@+id/content"
            android:layout_marginTop="14dp"
            android:contentDescription="@null"
            android:src="@drawable/ic_menu_refresh" />
    </RelativeLayout>

</LinearLayout>

Google検索でヒットしたいくつかのサイトに記載されていた
android:focusable=”false”とandroid:focusableInTouchMode=”false”
を付ける奴。これダメでした( ̄ー ̄

で、2時間ほど探して出会った魔法の言葉が…!
「android:descendantFocusability=”blocksDescendants”」
これをルートなViewに付加するそうです!
つまりここでは[@+id=”root_view”]のところですね(^^)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants" >
                ・
                ・
                ・

これで治っちゃいました。これに出会うまでfocusable=”false”をありとあらゆる場所に
付けていたりしてたのですが……(´∀`;

というわけで…

今のところこの3点(厳密には4点)です(・ω・)
もし、ここはこうした方が良いよ~などのご指摘がございましたらご教授いただけると幸いです。

では(^_^)/~