banner
jzman

jzman

Coding、思考、自觉。
github

A step-by-step guide to implementing an Android date picker

PS: Being obsessed with efforts that do not solve practical problems is always pseudo-learning.

Latest update 20210523.

  • [Adaptation] Switched to AndroidX
  • [New] Set font size
  • [New] Set text color
  • [Optimization] Fine-tune text drawing position

A custom View implements a user-friendly Android date and time picker, which can be viewed directly on Github, with the following dependency method:

  1. Add the jitpack repository in the build.gradle file at the root directory of the project, as follows:
allprojects {
	repositories {
		// ...
		maven { url 'https://www.jitpack.io' }
	}
}
  1. Introduce MDatePicker in the build.gradle file under app, as follows:
implementation 'com.github.jzmanu:MDatePickerSample:v1.0.6'
  1. The usage of MDatePicker is the same as a regular Dialog, refer to the following:
MDatePicker.create(this)
    // Additional settings (optional, with default values)
    .setCanceledTouchOutside(true)
    .setGravity(Gravity.BOTTOM)
    .setSupportTime(false)
    .setTwelveHour(true)
    // Result callback (mandatory)
    .setOnDateResultListener(new MDatePickerDialog.OnDateResultListener() {
        @Override
        public void onDateResult(long date) {
            // date
        }
    })
    .build()
    .show();

The effect is as follows:

MDatePickerDialog.gif

Below is a brief description of the implementation process:

  1. Basic idea
  2. Baseline calculation
  3. How to achieve scrolling
  4. Specific drawing
  5. Implementation of MDatePicker
  6. Settings of MDatePicker
  7. Usage of MDatePicker

Basic Idea#

A fundamental element of a date picker is a wheel that can be freely set with data. Here, a custom MPickerView is used as a container for selecting dates and times, allowing selection through up and down scrolling. Canvas is used for drawing as needed, and both dates and times are displayed using MPickerView. The final date picker is encapsulated using MPickerView, with Calendar assembling date and time data. The most important part is the implementation of MPickerView.

Baseline Calculation#

The baseline of text is the reference line for text drawing. Once the baseline of the text is determined, the text can be accurately drawn at the desired position. Therefore, when drawing text, it must be done according to the baseline. When drawing text, its left origin is at the left end of the baseline, with the y-axis direction upwards being negative and downwards being positive, as follows:

image

Since the selected date or time needs to be displayed in the center of the drawn View, how is this calculated in the code?

 // Get Baseline position
 Paint.FontMetricsInt metricsInt = paint.getFontMetricsInt();
 float line = mHeight / 2.0f + (metricsInt.bottom - metricsInt.top) / 2.0f - metricsInt.descent;

How to Achieve Scrolling#

MPickerView draws a given set of data at a certain position in the middle. The position drawn is always the index of the data size size/2:

public void setData(@NonNull List<String> data) {
    if (mData != null) {
        mData.clear();
        mData.addAll(data);
        // Index of the center position
        mSelectPosition = data.size() / 2;
    }
}

So how to achieve the scrolling effect? Each time the finger slides a certain distance, if sliding up, the topmost data moves to the bottom; conversely, if sliding down, the bottommost data moves to the top, simulating data scrolling. The key code is as follows:

@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) {// Sliding down
                moveTailToHead();
                mMoveDistance = mMoveDistance - RATE * mTextSizeNormal;
            } else if (mMoveDistance < -RATE * mTextSizeNormal / 2) {// Sliding up
                moveHeadToTail();
                mMoveDistance = mMoveDistance + RATE * mTextSizeNormal;
            }
            mStartTouchY = event.getY();
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            //...
    }
    return true;
}

Specific Drawing#

The drawing of MPickerView mainly involves displaying the data, which can be divided into three positions: above, middle, and below. The upper part consists of data before the index at mSelectPosition, the middle position is the data pointed to by mSelectPosition, and the lower part consists of data after the index at mSelectPosition. The key code is as follows:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // Draw middle position
    draw(canvas, 1, 0, mPaintSelect);
    // Draw upper data
    for (int i = 1; i < mSelectPosition - 1; i++) {
        draw(canvas, -1, i, mPaintNormal);
    }
    // Draw lower data
    for (int i = 1; (mSelectPosition + i) < mData.size(); i++) {
        draw(canvas, 1, i, mPaintNormal);
    }
    invalidate();
}

Now let's look at the specific implementation of the draw method:

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);
}

This completes the drawing of the data part. Additionally, there are some extra effects to draw, such as drawing dividing lines, years, months, days, hours, minutes, and adjusting some display effects, as follows:

//...
if (position == 0) {
    mPaintSelect.setTextSize(mTextSizeSelect);
    float startX;
    
    if (mData.get(mSelectPosition).length() == 4) {
        // Year is four digits
        startX = mPaintSelect.measureText("0000") / 2 + x;
    } else {
        // Other two digits
        startX = mPaintSelect.measureText("00") / 2 + x;
    }

    // Drawing year, month, day, hour, minute
    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);
    // Drawing dividing lines
    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);
}

The coordinate calculations in the above code are related to the baseline. For specific code implementation, refer to the original text at the end. The effect of the MPickerView implementation is as follows:

MPickView.gif

Implementation of MDatePicker#

The implementation of MDatePickerDialog is very simple; it is a custom Dialog. The year, month, day, hour, minute, and other data are obtained through the Calendar-related API. The layout file is as follows:

<?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>

Based on the above layout file, encapsulate a Dialog that can pop up at the bottom and center of the screen. For specific implementation, refer to the original text link at the end. Let's see what functions can be set using MDatePicker. Here, settings are done through the Builder method, with some code as follows:

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;
    }
}

Settings of MDatePicker#

The basic properties of MDatePicker are as follows:

SettingMethodDefault Value
TitlesetTitle(String mTitle)Date Selection
Display PositionsetGravity(int mGravity)Gravity.CENTER
Support for clicking outside to cancelsetCanceledTouchOutside(boolean canceledTouchOutside)false
Support for timesetSupportTime(boolean supportTime)false
Support for 12-hour formatsetTwelveHour(boolean twelveHour)false
Only display year and monthsetOnlyYearMonth(boolean onlyYearMonth)false
Set default year valuesetYearValue(int yearValue)Current year
Set default month valuesetMonthValue(int monthValue)Current month
Set default day valuesetDayValue(int dayValue)Current day

Usage of MDatePicker#

Using MDatePicker is very simple, as follows:

MDatePicker.create(this)
    // Additional settings (optional, with default values)
    .setCanceledTouchOutside(true)
    .setGravity(Gravity.BOTTOM)
    .setSupportTime(false)
    .setTwelveHour(true)
    // Result callback (mandatory)
    .setOnDateResultListener(new MDatePickerDialog.OnDateResultListener() {
        @Override
        public void onDateResult(long date) {
            // date
        }
    })
    .build()
    .show();

For specific details, refer to the following link or click the original text at the end. Feel free to star it!

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.