2017年3月31日金曜日

アイコンフォントを使って、ボタンのdrawableLeft/Rightに画像を設定する方法(Iconics編)

このエントリーをはてなブックマークに追加

概要

アイコンフォント便利ですよね! アイコンフォントを使うと、テキストの中でアイコン画像を表示することができます。 つまり、アイコン+テキストのボタン等が簡単に作れます。 ただ、一つの要素として表示されるため、テキストとアイコンとで違うサイズを使えなかったり、アイコンとテキストでベースラインが揃ってしまう問題があります。

Androidのボタンは、drawableLeftやdrawableRight属性を使うことで、ボタン内にアイコンを表示できます。 この属性を使うと先の問題が解決できそうですが、イメージを指定する必要があるため、そのままでは使えません。

そこで、アイコンフォントをdrawable vectorに変換して、アイコンとして表示する方法について説明します。

アイコンフォントを使うためのライブラリとしては、Iconicsを利用します。

結果

下記のようにdrawable_icon_font_left属性を指定すると、いい感じにアイコンを表示してくれます。

<Button
    android:text="{faw-android} android"
    />
<Button
    android:text="android"
    app:drawable_icon_font_left="@{`faw_android`}"
    />

image1

仕組み

IconicsのインストールとdataBindingの有効化

build.gradle で下記の設定を追加します。ここでは例として、AwesomeFontを使ってみます。

android {
    ...
    dataBinding {
        enabled = true
    }
}

dependencies {
    ...
    compile "com.mikepenz:iconics-core:2.8.2@aar"
    compile 'com.mikepenz:fontawesome-typeface:4.7.0.0@aar'
}

Iconicsの有効化

利用するActivityやFragmentで下記の設定を追加します。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflaterCompat.setFactory(getLayoutInflater(), new IconicsLayoutInflater(getDelegate()));

        super.onCreate(savedInstanceState);
        DataBindingUtil.setContentView(this, R.layout.activity_main);
    }
}

BindingAdapterの定義

drawable_icon_font_left 属性を定義します。

メソッド内では、指定された文字からDrawableを生成します。 その際に、好みに合わせて、テキストと同じカラーを設定したり、アイコンサイズをテキストサイズと比べて少し大きいものを設定できます。

public class BindingAdapterManager {

    @BindingAdapter("drawable_icon_font_left")
    public static void setDrawableIconFontLeft(final Button button, final String icon) {
        final Context context = button.getContext();
        final IconicsDrawable iconicsDrawable = new IconicsDrawable(context)
                .icon(FontAwesome.Icon.valueOf(icon))
                .color(button.getCurrentTextColor())
                .sizePx((int)(button.getTextSize() * 1.25));
        button.setCompoundDrawables(iconicsDrawable, null, null, null);
    }
}

サンプル

Drawable-Icon-Font@githubに動作するプロジェクトがあります。

バリデーション機能付きのTextInputLayoutの実装方法

このエントリーをはてなブックマークに追加

概要

AndroidのTextInputLayoutを拡張し、バリデーション機能付きのValidationTextInputLayoutの実装方法について説明します。 ValidationTextInputLayoutではカスタム属性を追加することで、必須、バリデーション項目、エラーメッセージをxml上から指定できます。

動作例

animation

目指すべき形

下記は、Emailアドレスの入力欄を必須にし、入力内容がEmail形式か否かをチェックします。 Email内容でない場合には、error_textで指定した文字列を表示します。

<ValidationTextInputLayout
    app:required="true"
    app:validation_type="email"
    app:error_text="Email value is invalidated"
    >
    <EditText
        android:hint="Email (Required)"
        />
</ValidationTextInputLayout>

仕組み

カスタム属性の定義

res/values/attrs.xmlにカスタム属性を定義します。 後で定義するValidationTextInputLayoutでは、属性を受け取り挙動を変更します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ValidationTextInputLayout">
        <attr name="required" format="boolean" />
        <attr name="validation_type">
            <enum name="post_code" value="0" />
            <enum name="email" value="1" />
        </attr>
        <attr name="error_text" format="string" />
    </declare-styleable>
</resources>

バリデーションタイプの定義

ValidationTypeを列挙型として定義します。 この処理は必須ではないですが、ソースコードの見通しが向上します。 enumの値とattrs.xmlで定義した値は一致させる必要があります。

public enum ValidationType {
    PostCode(0), Email(1),
    Null(9999);
    private int value;

    ValidationType(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public static ValidationType valueOf(final int value) {
        ValidationType type = Null;
        for (ValidationType validationType : ValidationType.values()) {
            if (validationType.getValue() == value) {
                type = validationType;
                break;
            }
        }
        return type;
    }
}

TextInputLayoutの拡張

initAttrs()で指定された属性値を取得します。 updateError()では、バリデーションを行い、必要に応じてエラーメッセージを出力します。 外部からは、isValidated()を呼び出すことで、バリデーションが通ったかを判断できます。

public class ValidationTextInputLayout extends TextInputLayout {
    private static final String PATTERN_POST_CODE = "\\d{7}";

    private boolean isRequired = false;
    private ValidationType validationType = ValidationType.Null;
    private String errorText;

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

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

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

    private void initAttrs(final Context context, AttributeSet attrs) {
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ValidationTextInputLayout);

        isRequired = typedArray.getBoolean(R.styleable.ValidationTextInputLayout_required, false);

        final int validationTypeValue = typedArray.getInt(
                R.styleable.ValidationTextInputLayout_validation_type, ValidationType.Null.getValue());
        validationType = ValidationType.valueOf(validationTypeValue);

        String errorText = typedArray.getString(R.styleable.ValidationTextInputLayout_error_text);
        if (TextUtils.isEmpty(errorText)) {
            errorText = getContext().getString(R.string.error_default_text);
        }
        this.errorText = errorText;
    }

    public boolean isValidated() {
        updateError();

        final boolean isValidated = TextUtils.isEmpty(getError());
        setErrorEnabled(!isValidated);
        return isValidated;
    }

    private void updateError() {
        final String text = getEditText().getText().toString();
        final boolean isEmpty = TextUtils.isEmpty(text);
        setError(null);

        switch (validationType) {
            case PostCode:
                if (!isPostCode(text)) {
                    setError(errorText);
                }
                break;
            case Email:
                if (!isEmail(text)) {
                    setError(errorText);
                }
                break;
            default:
                break;
        }

        if (isEmpty) {
            if (isRequired) {
                setError("Fill in this form");
            } else {
                setError(null);
            }
        }
    }

    private boolean isPostCode(final String str) {
        return Pattern.compile(PATTERN_POST_CODE).matcher(str).matches();
    }

    private boolean isEmail(final String str) {
        return Patterns.EMAIL_ADDRESS.matcher(str).matches();
    }
}

サンプル

Validation-Text-Input-Layout@githubに動作するプロジェクトがあります。

iOS版

同じような処理をiOSでも実装しています。 バリデーション&エラー表示付きの入力フォームを作ってみる を御覧ください。