[Androidアプリ開発] iOS 7 みたいなラジオボタンを文字を切り抜いて作ってみた

今回は Android で、 drawable と、 RadioButtonクラスをカスタマイズして、iOS7UISegmentedControl みたいな ラジオボタン を作ってみます。
iOS7UISegmentedControl というのは、下の画像のようなものです。

android-ios7-segmentedcontrol

実装

それでは作っていきます。未選択時の表示のみ drawable で実装します。
選択中のボタンは後ほどjavaで実装するので、drawable では透明にしておきます。

dimens.xml

res/valuesフォルダ 内の dimens.xml に値を追加します。

<resources>

    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="radio_corner_radius">4dp</dimen>

</resources>

color.xml

res/valuesフォルダ に color.xmlファイル を新規作成します。

<resources>
 
    <color name="radio_color">#007aff</color>
    <color name="radio_tap_color">#40007aff</color>
    <color name="radio_clear_color">#00000000</color>
 
</resources>

radio_text_color.xml

res/drawable-xxhdpiフォルダ に radio_text_color.xmlファイル を新規作成します。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 
    <!-- 選択時 -->
    <item android:state_checked="true" android:color="@color/radio_clear_color"/>
 
    <!-- 未選択時 -->
    <item android:color="@color/radio_color"/>
 
</selector>

radio_left.xml

res/drawable-xxhdpiフォルダ に radio_left.xmlファイル を新規作成します。

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  
    <!-- 選択時 -->
    <item android:state_checked="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/radio_clear_color" />
        </shape>
    </item>
      
    <!-- タップ時 -->
    <item android:state_pressed="true" >
        <shape android:shape="rectangle">
            <corners android:bottomLeftRadius="@dimen/radio_corner_radius"
                android:topLeftRadius="@dimen/radio_corner_radius" />
            <solid android:color="@color/radio_tap_color" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
  
    <!-- 未選択時 -->
    <item>
        <shape android:shape="rectangle">
            <corners android:bottomLeftRadius="@dimen/radio_corner_radius" 
                android:topLeftRadius="@dimen/radio_corner_radius" />
            <solid android:color="@color/radio_clear_color" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
  
</selector>

radio_center.xml

res/drawable-xxhdpiフォルダ に radio_center.xmlファイル を新規作成します。

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 
    <!-- 選択時 -->
    <item android:state_checked="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/radio_clear_color" />
        </shape>
    </item>
     
    <!-- タップ時 -->
    <item android:state_pressed="true" >
        <shape android:shape="rectangle">
            <solid android:color="@color/radio_tap_color" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
 
    <!-- 未選択時 -->
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/radio_clear_color" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
 
</selector>

radio_right.xml

res/drawable-xxhdpiフォルダ に radio_right.xmlファイル を新規作成します。

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  
    <!-- 選択時 -->
    <item android:state_checked="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/radio_clear_color" />
        </shape>
    </item>
      
    <!-- タップ時 -->
    <item android:state_pressed="true" >
        <shape android:shape="rectangle">
            <corners android:bottomRightRadius="@dimen/radio_corner_radius"
                android:topRightRadius="@dimen/radio_corner_radius" />
            <solid android:color="@color/radio_tap_color" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
  
    <!-- 未選択時 -->
    <item>
        <shape android:shape="rectangle">
            <corners android:bottomRightRadius="@dimen/radio_corner_radius" 
                android:topRightRadius="@dimen/radio_corner_radius" />
            <solid android:color="#00000000" />
            <stroke android:width="1dp" android:color="@color/radio_color" />
        </shape>
    </item>
  
</selector>

CustomRadioButton.java

選択中の表示の描画処理を作成します。
drawable で文字の領域を切り抜く方法はよくわからなかったので、ここだけjavaでコーディングします。

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.util.AttributeSet;
import android.widget.RadioButton;

public class CustomRadioButton extends RadioButton {

	public static final String POSITION = "position";
	public static final String POSITION_LEFT = "left";
	public static final String POSITION_CENTER = "center";
	public static final String POSITION_RIGHT = "right";

	/** ボタンカラー */
	private int mColor;

	/** 角丸半径 */
	private float mRadius;

	/** ポジション(左側、中央、右側) */
	private String mPosition = POSITION_LEFT;

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

		// layout.xml で指定したポジション(左側、中央、右側)を取得して保持します。
		mPosition = attrs.getAttributeValue(null, POSITION);

		// あらかじめ値を取得しておきます。
		mRadius = getResources().getDimension(R.dimen.radio_corner_radius);
		mColor = getResources().getColor(R.color.radio_color);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		if (isChecked()) {
			// 背景画像を生成します。
			Bitmap bgBm = createBg();

			// マスク画像を生成します。
			Bitmap maskBm = createMask();

			// マスク処理を行います。
			mask(bgBm, maskBm, canvas);
		} else {
			// 未選択時はdrawableで表示します。
			super.onDraw(canvas);
		}
	}

	private Bitmap createBg() {
		Bitmap bgBm = Bitmap.createBitmap(getWidth(), getHeight(),
				Bitmap.Config.ARGB_8888);

		float[] outerR = null;
		if (POSITION_LEFT.equals(mPosition)) {
			outerR = new float[] { mRadius, mRadius, 0, 0, 0, 0, mRadius,
					mRadius };

		} else if (POSITION_CENTER.equals(mPosition)) {
			outerR = new float[] { 0, 0, 0, 0, 0, 0, 0, 0 };

		} else if (POSITION_RIGHT.equals(mPosition)) {
			outerR = new float[] { 0, 0, mRadius, mRadius, mRadius, mRadius, 0,
					0 };
		}

		ShapeDrawable shapeDrawable = new ShapeDrawable(new RoundRectShape(
				outerR, null, null));
		shapeDrawable.setBounds(0, 0, getWidth(), getHeight());
		shapeDrawable.getPaint().setAntiAlias(true);
		shapeDrawable.getPaint().setStyle(Paint.Style.FILL);
		shapeDrawable.getPaint().setColor(mColor);

		Canvas bgCv = new Canvas(bgBm);
		shapeDrawable.draw(bgCv);
		return bgBm;
	}

	private Bitmap createMask() {
		Bitmap maskBm = Bitmap.createBitmap(getWidth(), getHeight(),
				Bitmap.Config.ARGB_8888);
		Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		textPaint.setTextSize(getTextSize());
		textPaint.setTextAlign(Paint.Align.CENTER);

		new Canvas(maskBm).drawText(getText().toString(), getWidth() / 2.0f,
				getBaseline(), textPaint);
		return maskBm;
	}

	private void mask(Bitmap baseBm, Bitmap maskBm, Canvas canvas) {
		Bitmap maskedBm = Bitmap.createBitmap(getWidth(), getHeight(),
				Bitmap.Config.ARGB_8888);
		Canvas maskedCv = new Canvas(maskedBm);
		Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
		paint.setFilterBitmap(false);
		maskedCv.drawBitmap(baseBm, 0, 0, paint);
		paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
		maskedCv.drawBitmap(maskBm, 0, 0, paint);
		paint.setXfermode(null);

		canvas.drawBitmap(maskedBm, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
	}
}

activity_main.xml

最後に res/layoutフォルダ の activity_main.xmlファイル を修正して、
作成した drawable が反映されるように設定したボタンを配置します。

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

    <RadioGroup
        android:id="@+id/radioGroup1"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_marginTop="20dp"
        android:checkedButton="@+id/testleftBtn"
        android:orientation="horizontal" >

        <jp.classmethod.sampleradiobuttonmask.CustomRadioButton
            android:id="@+id/testleftBtn"
            android:layout_width="80dp"
            android:layout_height="match_parent"
            position="left"
            android:background="@drawable/radio_left"
            android:button="@null"
            android:gravity="center"
            android:text="左側"
            android:textColor="@drawable/radio_text_color" />

        <jp.classmethod.sampleradiobuttonmask.CustomRadioButton
            android:layout_width="80dp"
            android:layout_height="match_parent"
            position="center"
            android:background="@drawable/radio_center"
            android:button="@null"
            android:gravity="center"
            android:text="中央"
            android:textColor="@drawable/radio_text_color" />

        <jp.classmethod.sampleradiobuttonmask.CustomRadioButton
            android:layout_width="80dp"
            android:layout_height="match_parent"
            position="right"
            android:background="@drawable/radio_right"
            android:button="@null"
            android:gravity="center"
            android:text="右側"
            android:textColor="@drawable/radio_text_color" />
    </RadioGroup>

</RelativeLayout>

これで完成です。Android2.3系で動かす場合は、drawable で設定した下側の角丸が左右逆になってしまうのでご注意ください。
もし対応する場合は、drawable-xxhdpi-v11フォルダを作成して、Android2.3以前と以後でファイルを分けるようにしてください。

動作確認

では、動かしてみます。

android-ios7-segmentedcontrol-mask

はい。できました。
背景が薄いグレーなのでわかりにくいですが、ちゃんと文字の部分で切り抜かれていています。

ではでは。