PS:実際の問題を解決しない努力に夢中になることは、永遠に偽の学びです。
最新の更新 20210523。
- 【適応】AndroidX に切り替え
- 【追加】フォントサイズの設定
- 【追加】文字色の設定
- 【最適化】文字描画位置の微調整
カスタム View を実装して使いやすい Android 日付時間ピッカーを作成しました。直接Githubで確認できます。依存関係の設定は以下の通りです:
- プロジェクトのルートディレクトリにある build.gradle ファイルに jitpack リポジトリを追加します:
allprojects {
repositories {
// ...
maven { url 'https://www.jitpack.io' }
}
}
- app ディレクトリ内の build.gradle ファイルに MDatePicker を導入します:
implementation 'com.github.jzmanu:MDatePickerSample:v1.0.6'
- MDatePicker の使用は通常の Dialog と同様で、以下を参考にしてください:
MDatePicker.create(this)
//追加設定(必須ではなく、デフォルト値があります)
.setCanceledTouchOutside(true)
.setGravity(Gravity.BOTTOM)
.setSupportTime(false)
.setTwelveHour(true)
//結果コールバック(必須)
.setOnDateResultListener(new MDatePickerDialog.OnDateResultListener() {
@Override
public void onDateResult(long date) {
// date
}
})
.build()
.show();
効果の画像は以下の通りです:
以下に実装プロセスを簡単に説明します:
- 基本的な考え方
- ベースラインの計算
- スクロールの実現方法
- 具体的な描画
- MDatePicker の実装
- MDatePicker の設定
- MDatePicker の使用
基本的な考え方#
日付ピッカーの最も基本的な要素は、自由にデータを設定できるホイールです。ここでは、日付と時間の選択コンテナとしてカスタム MPickerView を作成し、上下にスクロールして日付または時間を選択します。必要に応じて canvas を使用して描画し、日付でも時間でも MPickerView を使用してデータを表示します。最終的な日付ピッカーは MPickerView でラップし、Calendar を使用して日付時間データを組み立てます。この中で最も重要なのは MPickerView の実装です。
ベースラインの計算#
文字のベースライン(Baseline)は、文字描画の基準となる線であり、文字のベースラインを決定することで、文字を描画したい位置に正確に描画できます。したがって、文字の描画に関わる場合は必ずベースラインに従って描画する必要があります。文字を描画する際、その左端の原点はベースラインの左端にあり、y 軸方向は上が負、下が正です。具体的には以下の通りです:
最終的に選択された日付または時間は描画された View の中央に表示される必要があるため、コード内でどのように計算するのでしょうか?
//ベースライン位置を取得
Paint.FontMetricsInt metricsInt = paint.getFontMetricsInt();
float line = mHeight / 2.0f + (metricsInt.bottom - metricsInt.top) / 2.0f - metricsInt.descent;
スクロールの実現方法#
MPickerView の中央位置には、指定されたデータの特定の位置を描画します。ここで描画される位置は常にデータサイズ size/2 を描画するデータのインデックスとして使用します:
public void setData(@NonNull List<String> data) {
if (mData != null) {
mData.clear();
mData.addAll(data);
//中心位置のインデックスを描画
mSelectPosition = data.size() / 2;
}
}
では、スクロール効果をどのように実現するのでしょうか。指が一定の距離を滑るたびに、上に滑ると最上部のデータが底に移動し、逆に下に滑ると最下部のデータが上に移動します。これによりデータのスクロールをシミュレートします。重要なコードは以下の通りです:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartTouchY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
mMoveDistance += (event.getY() - mStartTouchY);
if (mMoveDistance > RATE * mTextSizeNormal / 2) {//下に滑る
moveTailToHead();
mMoveDistance = mMoveDistance - RATE * mTextSizeNormal;
} else if (mMoveDistance < -RATE * mTextSizeNormal / 2) {//上に滑る
moveHeadToTail();
mMoveDistance = mMoveDistance + RATE * mTextSizeNormal;
}
mStartTouchY = event.getY();
invalidate();
break;
case MotionEvent.ACTION_UP:
//...
}
return true;
}
具体的な描画#
MPickerView の描画は主にデータの描画であり、上、中、下の 3 つの位置のデータの描画に分けられます。上部は mSelectPosition の前のデータ、中間位置は mSelectPosition が指し示すデータ、下部は mSelectPosition の後のデータです。重要なコードは以下の通りです:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//中央位置を描画
draw(canvas, 1, 0, mPaintSelect);
//上のデータを描画
for (int i = 1; i < mSelectPosition - 1; i++) {
draw(canvas, -1, i, mPaintNormal);
}
//下のデータを描画
for (int i = 1; (mSelectPosition + i) < mData.size(); i++) {
draw(canvas, 1, i, mPaintNormal);
}
invalidate();
}
次に draw メソッドの具体的な実装を見てみましょう:
private void draw(Canvas canvas, int type, int position, Paint paint) {
float space = RATE * mTextSizeNormal * position + type * mMoveDistance;
float scale = parabola(mHeight / 4.0f, space);
float size = (mTextSizeSelect - mTextSizeNormal) * scale + mTextSizeNormal;
int alpha = (int) ((mTextAlphaSelect - mTextAlphaNormal) * scale + mTextAlphaNormal);
paint.setTextSize(size);
paint.setAlpha(alpha);
float x = mWidth / 2.0f;
float y = mHeight / 2.0f + type * space;
Paint.FontMetricsInt fmi = paint.getFontMetricsInt();
float baseline = y + (fmi.bottom - fmi.top) / 2.0f - fmi.descent;
canvas.drawText(mData.get(mSelectPosition + type * position), x, baseline, paint);
}
これでデータ部分の描画が完了しました。さらに、デザインに基づいて区切り線を描画したり、年、月、日、時、分などの追加情報を描画したり、表示効果の調整を行ったりします。以下を参考にしてください:
//...
if (position == 0) {
mPaintSelect.setTextSize(mTextSizeSelect);
float startX;
if (mData.get(mSelectPosition).length() == 4) {
//年は4桁
startX = mPaintSelect.measureText("0000") / 2 + x;
} else {
//他は2桁
startX = mPaintSelect.measureText("00") / 2 + x;
}
//年、月、日、時、分の描画
Paint.FontMetricsInt anInt = mPaintText.getFontMetricsInt();
if (!TextUtils.isEmpty(mText))
canvas.drawText(mText, startX, mHeight / 2.0f + (anInt.bottom - anInt.top) / 2.0f - anInt.descent, mPaintText);
//区切り線の描画
Paint.FontMetricsInt metricsInt = paint.getFontMetricsInt();
float line = mHeight / 2.0f + (metricsInt.bottom - metricsInt.top) / 2.0f - metricsInt.descent;
canvas.drawLine(0, line + metricsInt.ascent - 5, mWidth, line + metricsInt.ascent - 5, mPaintLine);
canvas.drawLine(0, line + metricsInt.descent + 5, mWidth, line + metricsInt.descent + 5, mPaintLine);
canvas.drawLine(0, dpToPx(mContext, 0.5f), mWidth, dpToPx(mContext, 0.5f), mPaintLine);
canvas.drawLine(0, mHeight - dpToPx(mContext, 0.5f), mWidth, mHeight - dpToPx(mContext, 0.5f), mPaintLine);
}
上記のコードに関連する座標計算はすべてベースラインに関係しており、具体的なコード実装は文末の原文を参照してください。MPickerView の実装効果は以下の通りです:
MDatePicker の実装#
MDatePickerDialog の実装は非常に簡単で、カスタム Dialog を作成し、年、月、日、時、分などのデータを Calendar 関連 API を通じて取得します。レイアウトファイルは以下の通りです:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="300dp"
android:id="@+id/llDialog"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="@+id/tvDialogTopCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:text="@string/strDateCancel"
android:textColor="#cf1010"
android:textSize="15sp" />
<TextView
android:id="@+id/tvDialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/strDateSelect"
android:textColor="#000000"
android:textSize="16sp" />
<TextView
android:id="@+id/tvDialogTopConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="12dp"
android:text="@string/strDateConfirm"
android:textColor="#cf1010"
android:textSize="15sp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.manu.mdatepicker.MPickerView
android:id="@+id/mpvDialogYear"
android:layout_width="wrap_content"
android:layout_height="160dp"
android:layout_weight="1"
tools:ignore="RtlSymmetry" />
<com.manu.mdatepicker.MPickerView
android:id="@+id/mpvDialogMonth"
android:layout_width="0dp"
android:layout_height="160dp"
android:layout_weight="1" />
<com.manu.mdatepicker.MPickerView
android:id="@+id/mpvDialogDay"
android:layout_width="0dp"
android:layout_height="160dp"
android:layout_weight="1" />
<com.manu.mdatepicker.MPickerView
android:id="@+id/mpvDialogHour"
android:layout_width="0dp"
android:layout_height="160dp"
android:layout_weight="1" />
<com.manu.mdatepicker.MPickerView
android:id="@+id/mpvDialogMinute"
android:layout_width="0dp"
android:layout_height="160dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:id="@+id/llDialogBottom"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvDialogBottomConfirm"
android:layout_width="0.0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:gravity="center"
android:text="@string/strDateConfirm"
android:textColor="#cf1010"
android:textSize="16sp" />
<View
android:layout_width="0.5dp"
android:layout_height="match_parent"
android:background="#dbdbdb" />
<TextView
android:id="@+id/tvDialogBottomCancel"
android:layout_width="0.0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:gravity="center"
android:text="@string/strDateCancel"
android:textColor="#cf1010"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
上記のレイアウトファイルを基に、画面の底部と中央にポップアップできる Dialog を封装します。具体的な実装は文末の原文リンクを参照してください。MDatePicker を使用して設定できる機能を見てみましょう。ここでは Builder の方法で設定を行います。部分的なコードは以下の通りです:
public static class Builder {
private Context mContext;
private String mTitle;
private int mGravity;
private boolean isCanceledTouchOutside;
private boolean isSupportTime;
private boolean isTwelveHour;
private float mConfirmTextSize;
private float mCancelTextSize;
private int mConfirmTextColor;
private int mCancelTextColor;
private OnDateResultListener mOnDateResultListener;
public Builder(Context mContext) {
this.mContext = mContext;
}
public Builder setTitle(String mTitle) {
this.mTitle = mTitle;
return this;
}
public Builder setGravity(int mGravity) {
this.mGravity = mGravity;
return this;
}
public Builder setCanceledTouchOutside(boolean canceledTouchOutside) {
isCanceledTouchOutside = canceledTouchOutside;
return this;
}
public Builder setSupportTime(boolean supportTime) {
isSupportTime = supportTime;
return this;
}
public Builder setTwelveHour(boolean twelveHour) {
isTwelveHour = twelveHour;
return this;
}
public Builder setConfirmStatus(float textSize, int textColor) {
this.mConfirmTextSize = textSize;
this.mConfirmTextColor = textColor;
return this;
}
public Builder setCancelStatus(float textSize, int textColor) {
this.mCancelTextSize = textSize;
this.mCancelTextColor = textColor;
return this;
}
public Builder setOnDateResultListener(OnDateResultListener onDateResultListener) {
this.mOnDateResultListener = onDateResultListener;
return this;
}
private void applyConfig(MDatePicker dialog) {
if (this.mGravity == 0) this.mGravity = Gravity.CENTER;
dialog.mContext = this.mContext;
dialog.mTitle = this.mTitle;
dialog.mGravity = this.mGravity;
dialog.isSupportTime = this.isSupportTime;
dialog.isTwelveHour = this.isTwelveHour;
dialog.mConfirmTextSize = this.mConfirmTextSize;
dialog.mConfirmTextColor = this.mConfirmTextColor;
dialog.mCancelTextSize = this.mCancelTextSize;
dialog.mCancelTextColor = this.mCancelTextColor;
dialog.isCanceledTouchOutside = this.isCanceledTouchOutside;
dialog.mOnDateResultListener = this.mOnDateResultListener;
}
public MDatePicker build() {
MDatePicker dialog = new MDatePicker(mContext);
applyConfig(dialog);
return dialog;
}
}
MDatePicker の設定#
MDatePicker の基本属性は以下の通りです:
設定 | 設定方法 | デフォルト値 |
---|---|---|
タイトル | setTitle(String mTitle) | 日付選択 |
表示位置 | setGravity(int mGravity) | Gravity.CENTER |
外部領域をクリックしてキャンセルをサポートするか | setCanceledTouchOutside(boolean canceledTouchOutside) | false |
時間をサポートするか | setSupportTime(boolean supportTime) | false |
12 時間制をサポートするか | setTwelveHour(boolean twelveHour) | false |
年月のみ表示するか | setOnlyYearMonth(boolean onlyYearMonth) | false |
年のデフォルト値を設定 | setYearValue(int yearValue) | 現在の年 |
月のデフォルト値を設定 | setMonthValue(int monthValue) | 現在の月 |
日のデフォルト値を設定 | setDayValue(int dayValue) | 現在の日 |
MDatePicker の使用#
MDatePicker の使用は非常に簡単です。以下の通りです:
MDatePicker.create(this)
//追加設定(必須ではなく、デフォルト値があります)
.setCanceledTouchOutside(true)
.setGravity(Gravity.BOTTOM)
.setSupportTime(false)
.setTwelveHour(true)
//結果コールバック(必須)
.setOnDateResultListener(new MDatePickerDialog.OnDateResultListener() {
@Override
public void onDateResult(long date) {
// date
}
})
.build()
.show();
具体的な詳細は以下のリンクを参照するか、文末の原文をクリックしてください。ぜひstarをお願いします!