Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

なんとなく作っていたAndroidのカスタムViewを正しく実装する

More than 3 years have passed since last update.

Androidアプリを作っていれば基底のViewを拡張した独自のViewクラスを作ることが多々あると思いますが、いざゼロから書くとなると色々と取りこぼしがありそうだったのでまとめました

CircleView

今回は以下の様な2重の円弧を描画するViewを作成していきます。

639483c5-710f-f296-0131-74232138a46f.png

View#onDrawで描画していきますが、Viewの外側からはシンプルに使えることを意識して作成します。
設定できる独自プロパティとして以下を宣言していきます。

  • 上の円弧の開始角度(StartAngle)
  • 上の円弧の描画角度(DrawAngle)
  • 上の円弧の色(PrimaryLineColor)
  • 下の円弧の色(SecondaryLineColor)
  • 線の太さ(StrokeWidth)

1. 独自Viewクラス作成

まずはViewクラスをextendした空のCircleViewクラスを作成していきます。

CircleView.java
public class CircleView extends View {

    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

2. XMLでの独自プロパティ実装

次にLayoutのXMLで指定できるようにattrs.xmlを作成していきます。
それぞれプロパティ名と型宣言を行います。

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="start_angle" format="float|reference"/>
        <attr name="draw_angle" format="float|reference"/>
        <attr name="primary_line_color" format="color|reference"/>
        <attr name="secondary_line_color" format="color|reference"/>
        <attr name="stroke_width" format="dimension|reference"/>
    </declare-styleable>
</resources>

これでCircleViewクラスをLayoutファイル上に配置した際にXML上で独自プロパティを以下のように指定できます
com.kazakago.test.CircleViewは各自の配置したパッケージのフルパスに置き換えて記載して下さい

activity_main.xml
<FrameLayout 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="match_parent">

    <com.kazakago.test.CircleView
        android:id="@+id/view_circle"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        app:draw_angle="300"
        app:primary_line_color="#ff00ddff"
        app:secondary_line_color="#f0f0f0"
        app:start_angle="-90"
        app:stroke_width="10dp"/>

</FrameLayout>

app名前空間のプロパティが独自に追加したものになります。
ルートレイアウトの要素にxmlns:app="http://schemas.android.com/apk/res-auto"を追加するのも忘れずに!

3. XMLから値を取得する

上記で宣言したXML要素を独自ViewクラスのJavaソースから取得します。
今回はonDrawで描画を行うためその部分で必要なPaint変数等もここで宣言しています。
XMLで宣言した各プロパティはJavaソースからセット、取得することも想定してgetter、setterを必ず用意します。

CircleView.java
public class CircleView extends View {

    private float mStartAngle;
    private float mDrawAngle;
    private final Paint mFirstPaint = new Paint();
    private final Paint mSecondPaint = new Paint();
    private final RectF mOval = new RectF();

    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs, defStyleAttr);
    }

    private void initView(Context context, AttributeSet attrs, int defStyle) {
        mFirstPaint.setAntiAlias(true);
        mFirstPaint.setStyle(Paint.Style.STROKE);

        mSecondPaint.setAntiAlias(true);
        mSecondPaint.setStyle(Paint.Style.STROKE);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyle, 0);
        setStartAngle(typedArray.getFloat(R.styleable.CircleView_start_angle, -90.0f));
        setDrawAngle(typedArray.getFloat(R.styleable.CircleView_draw_angle, 270.0f));
        setPrimaryLineColor(typedArray.getColor(R.styleable.CircleView_primary_line_color, Color.BLACK));
        setSecondaryLineColor(typedArray.getColor(R.styleable.CircleView_secondary_line_color, Color.LTGRAY));
        setStrokeWidth(typedArray.getDimension(R.styleable.CircleView_stroke_width,
                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, getResources().getDisplayMetrics())));
        typedArray.recycle();
    }
}

TypedArrayをClass名を指定して取得し、宣言したattrs.xmlの内容を一つ一つ取り出しています。
レイアウト側でプロパティが宣言がされていない時用に必ずTypedArray#get***の第2引数で初期値を宣言しておきましょう。
getter、setterについては今回は以下のように記載しました。※長くなるので行を詰めてます

CircleView.java
    public float getStartAngle() { return mStartAngle; }
    public void setStartAngle(float startAngle) { mStartAngle = startAngle; }
    public float getDrawAngle() { return mDrawAngle; }
    public void setDrawAngle(float drawAngle) { mDrawAngle = drawAngle; }
    public int getPrimaryLineColor() { return mFirstPaint.getColor(); }
    public void setPrimaryLineColor(int color) { mFirstPaint.setColor(color); }
    public int getSecondaryLineColor() { return mSecondPaint.getColor(); }
    public void setSecondaryLineColor(int color) { mSecondPaint.setColor(color); }
    public float getStrokeWidth() { return mFirstPaint.getStrokeWidth(); }
    public void setStrokeWidth(float width) { mFirstPaint.setStrokeWidth(width); mSecondPaint.setStrokeWidth(width); }

ちなみに初期描画後にmStartAngle等にセットした値を実際のUIに反映させるためにはView#invalidate()をコールして描画更新を促す必要があります。
場合によってはsetStartAngleメソッド内で最後に呼び出してしまってもいいかもしれません。

4.実際の描画処理

保持された変数に従って描画を行います。
今回はView#onDrawを使って独自に描画していますが、
場合によってはonDrawを使わないこともあると思うので状況に応じて読み替えて下さい。

CircleView.java
    @Override
    protected void onDraw(Canvas canvas) {
        mOval.set(getStrokeWidth(),
                getStrokeWidth(),
                canvas.getWidth() - getStrokeWidth(),
                canvas.getHeight() - getStrokeWidth());
        canvas.drawArc(mOval, mStartAngle, 360, false, mSecondPaint);
        canvas.drawArc(mOval, mStartAngle, mDrawAngle, false, mFirstPaint);
    }

今回はこれだけですね。シンプル!

5. 画面回転等のActivity再生成対応

忘れちゃいけないのが画面回転対応、正確にはActivityの再生成対応です。
これがないと画面回転させた時などに設定したメンバー変数が全て初期化されてしまいます。
基本的にActivityが保持しているメンバ変数はActivity側でBundleに持たせるなどの対策が必要ですが、独自Viewを使っている場合はView側で対応させたいですね。

そのためにはView#onSaveInstanceStateView#onRestoreInstanceStateを使います。

CircleView.java
    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parent = super.onSaveInstanceState();
        SavedState saved = new SavedState(parent);
        saved.startAngle = getStartAngle();
        saved.drawAngle = getDrawAngle();
        saved.primaryLineColor = getPrimaryLineColor();
        saved.secondaryLineColor = getSecondaryLineColor();
        saved.strokeWidth = getStrokeWidth();
        return saved;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState saved = (SavedState) state;
        super.onRestoreInstanceState(saved.getSuperState());
        setStartAngle(saved.startAngle);
        setDrawAngle(saved.drawAngle);
        setPrimaryLineColor(saved.primaryLineColor);
        setSecondaryLineColor(saved.secondaryLineColor);
        setStrokeWidth(saved.strokeWidth);
    }

    private static class SavedState extends BaseSavedState {
        public float startAngle;
        public float drawAngle;
        public int primaryLineColor;
        public int secondaryLineColor;
        public float strokeWidth;

        public SavedState(Parcel in) {
            super(in);
            startAngle = in.readFloat();
            drawAngle = in.readFloat();
            primaryLineColor = in.readInt();
            secondaryLineColor = in.readInt();
            strokeWidth = in.readFloat();
        }

        public SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(final Parcel out, final int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(startAngle);
            out.writeFloat(drawAngle);
            out.writeInt(primaryLineColor);
            out.writeInt(secondaryLineColor);
            out.writeFloat(strokeWidth);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };

    }

格納用にBaseSavedStateをextendしたクラスを宣言します。
Parcelableに対応させるためCREATERも記述して下さい。
View#onSaveInstanceState内でSavedStateへメンバー変数を一時退去させ、
View#onRestoreInstanceStateで取り出してメンバー変数を復元させる流れになります。

※細かいParcelableの実装方法は公式か↓辺りがわかりやすいです(丸投げ)
http://y-anz-m.blogspot.jp/2010/03/androidparcelable.html

縦画面固定にしていると画面回転しないためActivity再生成が起きにくく忘れがちですが、
フォント変更時やメモリ不足時など様々な状況で発生します。
個人的には、いかなる場合でも対応は絶対条件だと思っています。

おわり

上記まで記載すればXMLからもJavaからも全てのプロパティにアクセスでき、
Activityの再生成にも対応した独自Viewクラスの完成です。

コードの全文は以下のGistにおいておきます。
https://gist.github.com/KazaKago/81123f712ab36bfa77cf

KazaKago
都内でAndroid / iOSアプリ開発をやってます。
https://kazakago.hatenablog.jp/
ignis
累計7000万DL超のスマホアプリを自社企画・開発 ツール系からマンガ、ゲームなど幅広いジャンルのアプリを展開
http://1923.co.jp/service-information
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away