woshidan's blog

そんなことよりコードにダイブ。

レイアウトファイルを利用してCustomViewを作る

いまさらですが、サポートライブラリの中の人から分かりやすいスライドも出ているし、CustomViewの作り方をあらためて勉強し直しました。

参考

サポート ライブラリのできるまで

qiita.com

qiita.com

目次

  • Viewを継承したクラスを作る
  • values/attrs.xmlにカスタムViewと同名のdeclare-styleableを定義
  • デフォルト属性値をvalues/styles.xmlで定義する
  • カスタムView用のレイアウトファイルを書く
  • カスタム属性値をコンストラクタの中で取得する
  • レイアウトXMLを利用する
  • カスタムViewの要素を取得
  • カスタムViewに値をセット
  • Activityのレイアウトに配置
  • 後から属性値や表示される値を変えたり、値を取得したりしたい
  • 最終的に出来上がったコード

Viewを継承したクラスを作る

まず、ViewかViewのサブクラスを継承したクラスを用意します。

コンストラクタを3つ作成する必要がありますが、初期化に必要な処理を書くのは3つめのコンストラクタにします。

また、自動生成のままだとsuperになっており、うっかりこれをそのままにしておいて「せっかく書いたコンストラクタが呼び出されない、なんで」ということになって焦るので、ちゃんとthisに書き換えるのを忘れないでください。

package com.example.woshidan.customviewtest;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by yoshidanozomi on 2015/12/19.
 */
public class CustomView extends View {
    public CustomView(Context context) {
        this(context, null); // 自動生成のsuperのままにしないように!
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0); // 自動生成のsuperのままにしないように!
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        
        // ここで色々やる
    }
}

values/attrs.xmlにカスタムViewと同名のdeclare-styleableを定義

次に、values/attrs.xmlにカスタムViewと同名のdeclare-styleable要素を定義します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- http://yaraki.github.io/slides/gdm01/#16 -->
    <!-- formatの各属性については http://qiita.com/Hoshi_7/items/57c3a79c43efe05b5368 -->
    <declare-styleable name="CustomView">
        <attr name="name" format="string" />
        <attr name="email" format="string" />
        <!-- enum値 -->
        <attr name="grade">
            <enum name="beginner" value="0"/>
            <enum name="master" value="1"/>
        </attr>
    </declare-styleable>
</resources>

デフォルト属性値をvalues/styles.xmlで定義する

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
    </style>

    <!-- CustomView用 -->
    <style name="Widget.CustomViewTest.CustomView" parent="android:Widget" >
        <item name="name">testUser</item>
        <item name="email">test@example.com</item>
        <item name="grade">beginner</item>
    </style>
</resources>

カスタムView用のレイアウトファイルを書く

今回は下記のようなレイアウトにしようと思います。

f:id:woshidan:20151219164842p:plain

<!-- view_custom.xml -->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="#66f"
    android:padding="16dp"
    android:layout_width="match_parent"
    android:layout_height="200dp">

    <TextView
        android:id="@+id/name"
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#fff"
        android:textSize="20dp"
        android:text="UserName"/>

    <TextView
        android:id="@+id/email"
        android:layout_alignParentBottom="true"
        android:layout_marginLeft="16dp"
        android:layout_toRightOf="@id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#fff"
        android:textSize="15dp"
        android:text="test@example.com"/>

    <TextView
        android:id="@+id/grade"
        android:layout_marginBottom="12dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/name"
        android:padding="4dp"
        android:background="@color/grade_beginner"
        android:textStyle="bold"
        android:textColor="#fff"
        android:text="@string/grade_begginer"/>
</RelativeLayout>

Javaから属性値を与えやすいように、begginerのラベルのところの値はxmlに定義しています。

<!-- values/string.xml -->
    <string name="grade_begginer">begginer</string>
    <string name="grade_master">master</string>

<!-- values/colors.xml -->
    <color name="grade_master">#082</color>
    <color name="grade_beginner">#ec0</color>

カスタム属性値をコンストラクタの中で取得する

public class CustomView extends View {
    private String mName;
    private String mEmail;
    private int mGrade;
    ...

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 参考: http://yaraki.github.io/slides/gdm01/#19

        // 属性リストを取得
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CustomView, // 属性の定義
                defStyleAttr,
                R.style.Widget_CustomViewTest_CustomView); // デフォルトの属性値

        // 属性値を取得
        mEmail = a.getString(R.styleable.CustomView_email);
        mName = a.getString(R.styleable.CustomView_name);
        mGrade = a.getInt(R.styleable.CustomView_grade, 0);
    }

レイアウトXMLを利用する

// 3つめの要素はViewGroupである必要があるので、継承するクラスをViewGroupのクラスに変更しました
public class CustomView extends RelativeLayout {

// さっきのコードの続き
// このViewGroupの親要素はマージの形で元の要素の箇所に渡されます。
 inflate(context, R.layout.view_custom, this);

カスタムViewの要素を取得

public class CustomView extends RelativeLayout {
    ...
    private TextView mNameView;
    private TextView mEmailView;
    private TextView mGradeView;

    // さっきのコードの続き
    // カスタムViewの中の各Viewの要素を取得
    mNameView = (TextView) findViewById(R.id.name);
    mEmailView = (TextView) findViewById(R.id.email);
    mGradeView = (TextView) findViewById(R.id.grade);

カスタムViewに値をセット

    mNameView.setText(mName);
    mEmailView.setText(mEmail);

    if (mGrade == BEGINNER) {
        mGradeView.setText(getResources().getText(R.string.grade_begginer));
        mGradeView.setBackgroundColor(getResources().getColor(R.color.grade_beginner));
        // API23以上でないとdeprectedでない方のgetColor()は動作しません(本当は処理をAPIバージョンで分けるのが推奨)
    } else {
        mGradeView.setText(getResources().getText(R.string.grade_master));
        mGradeView.setBackgroundColor(getResources().getColor(R.color.grade_master));
    }

Activityのレイアウトに配置

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <!-- 属性値がデフォルトのスタイルから設定されているのを見る例 -->
    <com.example.woshidan.customviewtest.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <!-- 属性値をxmlからセットする例 -->
    <com.example.woshidan.customviewtest.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:name="MasterUser"
        custom:email="master@example.com"
        custom:grade="master"
        />
</LinearLayout>

結果はこうなります。

f:id:woshidan:20151219180330p:plain

後から属性値や表示される値を変えたり、値を取得したりしたい

CustomViewのクラスにセッターやゲッターを書きます。

// CustomView

    public void setName(String name) {
        mName = name;
        mNameView.setText(mName);
    }

    public void setEmail(String email) {
        mEmail = email;
        mEmailView.setText(mEmail);
    }

    public void setGrade(int grade) {
        mGrade = grade;
        if (mGrade == BEGINNER) {
            mGradeView.setText(getResources().getText(R.string.grade_begginer));
            mGradeView.setBackgroundColor(getResources().getColor(R.color.grade_beginner));
            // API23以上でないとdeprectedでない方のgetColor()は動作しません(本当は処理をAPIバージョンで分けるのが推奨)
        } else {
            mGradeView.setText(getResources().getText(R.string.grade_master));
            mGradeView.setBackgroundColor(getResources().getColor(R.color.grade_master));
        }
    }

最終的に出来上がったコード

最終的に出来上がったコードは下記のgistに置いてます。

CustomViewを作る · GitHub