スポンサーリンク

2016年9月2日金曜日

Android - カスタムViewにカスタムレイアウトを適用する

ViewGroup をカスタマイズする

内部に複雑なレイアウト構造を持つ ViewGroup をカスタムクラス化すると、内部が隠蔽され、レイアウトを単純な View パーツとして扱うことができます。

TextView や Button 等の単純な View をカスタム化する方法はあらゆるところで解説されていますが、LinearLayout や FrameLayout 等の ViewGroup をカスタム化する方法はあまり見あたりません。今まで自分で試行錯誤しながらやってきましたが、自分の辿ってきた道を基に、ViewGroup をカスタマイズする方法を解説したいと思います。


LinearLayout をカスタム化する

まず LinearLayout のサブクラスを作ります。

CustomView.java
public class CustomView1 extends LinearLayout {

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

このクラスはまだ LinearLayout とまったく同じなので、例えば以下の様に内部に View を配置することができます。

activity_main.xml
<?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">

    <com.android.test.customviewtest.CustomView1
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </com.android.test.customviewtest.CustomView1>

</FrameLayout>

内部に配置した子 View に対する操作を CustomView1 の内部に実装しましょう。

CustomView1.java
public class CustomView1 extends LinearLayout {

    private TextView mTextView;
    private Button mButton;

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mTextView = (TextView) findViewById(R.id.text);
        mTextView.setText("CustomView 1");

        mButton = (Button) findViewById(R.id.button);
        mButton.setText("Button 1");
    }
}

一応これで LinearLayout をカスタム化することができました。しかしこの方法は色々問題点があります。

まずカスタム View の内部構造であるところのレイアウトを常にメインレイアウトにベタに記述しなくてはなりません。他で使いまわそうとした場合、Java コードとレイアウト XML を常にペアで移動させなくてはなりません。Javaコードがレイアウトに依存し、レイアウトもJavaコードに依存しています。つまり依存性の循環が発生し、Java コードとレイアウトが非常に強く結合しています。

更にコンストラクタの中ではまだ子 View の検索ができません。子 View が検索できるのは onFinishInflate() が実行されたでからです。なので子 View をフィールドメンバーとして保持する場合、final にすることができません。つまり子 View が必ず存在することを Java コードとして保証することができないのです。

レイアウトを独立化する


では上の問題を解決するために以下の様な独立したレイアウトXMLを作成します。

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

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

このレイアウトを使用するカスタム View を作成します。

CustomView2.java
public class CustomView2 extends FrameLayout {

    private final TextView mTextView;
    private final Button mButton;

    public CustomView2(Context context, AttributeSet attrs) {
        super(context, attrs);

        // XMLレイアウトをインフレートしてアタッチ
        LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.custom_view_layout_2, this, true);

        mTextView = (TextView) findViewById(R.id.text);
        mTextView.setText("CustomView 2");

        mButton = (Button) findViewById(R.id.button);
        mButton.setText("Button 2");
    }
}

コンストラクタの中で、レイアウトをインフレートし、自分自身にアタッチしています。この場合コンストラクタの中で子 View を検索でき、フィールドを final 化することもできます。
これであれば、見て分かる通り、レイアウトから Java コードへの依存は一切ありません。

また親クラスに FrameLayout を指定している点も注意してください。内部にレイアウトを配置するだけの入れ物なので何でもいいのですが、比較的処理が軽いと思われる FrameLayout を使っています。

このカスタム View をメインレイアウトに配置するには以下の様にします。

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

    <com.android.test.customviewtest.CustomView2
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

一般の View と全く同じ感覚で使うことができます。

更に軽量化する

上の方法で一つだけ気になる点があります。それは View の階層が一つ深くなってしまうことです。親クラスとして使った FrameLayout は単なる入れ物で、ほとんど仕事をしていません。
そこでレイアウトXML を以下の様に変更します。

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

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</merge>

LinearLayout を merge タグに変えただけです。
更にこのレイアウトを使うカスタム View を以下の様に変更します。

CustomView3.java
public class CustomView3 extends LinearLayout {

    private final TextView mTextView;
    private final Button mButton;

    public CustomView3(Context context, AttributeSet attrs) {
        super(context, attrs);

        // XMLレイアウトをインフレートしてアタッチ
        LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.custom_view_layout_3, this, true);

        mTextView = (TextView) findViewById(R.id.text);
        mTextView.setText("CustomView 3");

        mButton = (Button) findViewById(R.id.button);
        mButton.setText("Button 3");
    }
}

継承する親クラスを LinearLayout に変えただけです。元々のレイアウトで使う一番外側のクラスをJavaコードに移動したことになります。これであれあば View 階層が深くならずにカスタム View を使うことができます。

但しこの場合、レイアウトが(Javaコードの) LinearLayout にアタッチされることを暗黙的に仮定しているので、レイアウトから Java コードへの非常に弱いかたちでの依存が発生することになります。これは効率化とのトレードオフと言えるでしょう。

最後にメインレイアウト上に、上の全てのカスタム View を配置したコードを示します。

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

    <com.android.test.customviewtest.CustomView1
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </com.android.test.customviewtest.CustomView1>

    <com.android.test.customviewtest.CustomView2
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <com.android.test.customviewtest.CustomView3
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

実行したイメージはこんなかんじになります。どの方法を使っても外見は全く同じです。


テストに使ったコードを以下に置きました。
https://github.com/masamichi441/CustomViewCustomLayout

0 件のコメント :

コメントを投稿