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

ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

【Android】ScrollViewにListViewを入れる

お久しぶりです。ホサカです。

Android開発をする上で、最近では数多の便利なライブラリが存在しており、いろいろな場面でサポートしてくれるようになりました。

とはいえ、やはり自分で解決しなければいけない問題には度々遭遇するものです。

かくいう私も、先日少々カスタマイズしなければならないUIに遭遇しましたので、その際の対応を備忘録を兼ねてご紹介します。

1画面に複数のListViewを表示

これを実現する必要に迫られました。Adapterを工夫して1つのListViewで実現すればよいかなと考えていましたが、いろいろ表示するものが多かったので断念し、ScrollViewにListViewを入れる対応で実現しようとなくなく決断することに。

何故なくなく決断なのか

ご存知の方は多いとは思いますが、ScrollViewは子のViewの高さが明確になっていなければなりません。

そのため、何の工夫もなくScrollViewにListViewを入れ込んでも、ListViewが極端に小さく表示されるという現象が発生してしまいます。 (ListViewの他にViewPager等も同様)

この問題に立ち向かうのは特別面白くもないし、ただただ面倒という感情が。。。しかし憂鬱になっていても時間が無駄なので、取り組むことにします。

まずはググる

ググれば解決でしょ!と思いきや、意外と記事がないw

あまり使わないのでしょうね。。。

一応回避できたという記事がちらほらありましたが、ActivityやAdapterからListViewの要素の高さを設定するというものしか出てこず。

viewの高さを設定する処理ををcontrollerで行うのは個人的には好ましくなかったので(というより、毎回実現するためにコードで記述するなんて面倒くさい)、これらの記事を参考にListViewをカスタマイズすることにします。

解決方法

カスタムListViewをScrollViewにぶち込むだけでOKというのが理想(部品化できて使い回せて便利)だと思ったので、ListViewを下記の用に拡張します。

拡張と言っても、onMesure()をいじるだけなので簡単です。

public class MesuredListView extends ListView {

    private static final int MAX_SIZE = 99;

    public MesuredListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int newHeight = 0;
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY) {
            ListAdapter listAdapter = getAdapter();
            if (listAdapter != null && !listAdapter.isEmpty()) {
                int listPosition = 0;
                for (listPosition = 0; listPosition < listAdapter.getCount()
                        && listPosition < MAX_SIZE; listPosition++) {
                    View listItem = listAdapter.getView(listPosition, null, this);
                    if (listItem instanceof ViewGroup) {
                        listItem.setLayoutParams(new LayoutParams(
                                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
                    }
                    listItem.measure(widthMeasureSpec, heightMeasureSpec);
                    newHeight += listItem.getMeasuredHeight();
                }
                newHeight += getDividerHeight() * listPosition;
            }
            if ((heightMode == MeasureSpec.AT_MOST) && (newHeight > heightSize)) {
                if (newHeight > heightSize) {
                    newHeight = heightSize;
                }
            }
        } else {
            newHeight = getMeasuredHeight();
        }
        setMeasuredDimension(getMeasuredWidth(), newHeight);
    }
}

このMesuredListViewをListViewと置き換えれば完了です。 こんな感じ。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <yourpackage.MesuredListView
                android:id="@+id/list_first"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
            </yourpackage.MesuredListView>
            <yourpackage.MesuredListView
                android:id="@+id/list_second"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
            </yourpackage.MesuredListView>
        </LinearLayout>
    </ScrollView>
</RelativeLayout>

特別複雑なカスタムViewではなく普通のListViewと同じApiが使えるのでコードは同じで問題ないです。用途によって切り替えて使えます。

※このMesuredListViewを使用しても、うまく表示されない場合はListViewの要素のレイアウトに問題がある可能性があります。その場合、要素のrootをLinearLayoutにしてheightをwrap_contentに変更して試してみてください。

おまけ

Viewをちょっとカスタマイズしたいときは、onMesure()をいじればできるかもということを頭の角に置いておくといいかもしれないですね。

きっとコードも分割されていい具合になります。

onMesureを利用したカスタマイズ例 (ImageView)

public class CustomImageView extends ImageView {

    public CustomImageView(Context context) {
        super(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = (int) (width / 10 * 4);
        setMeasuredDimension(width, height);
    }

}

これは、横幅に対して縦幅を40%で表示するImageViewです。

これを応用して正方形のImageViewなんかも作成できますね。

あとはTextViewの行数を取得して、何かするとかもできそう。いろいろ試してみてはいかがでしょうか。