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-tuned text drawing position

A custom View implements a user-friendly Android date and time picker, which you can check directly on Github. Its dependency method is as follows:

  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 similar to a regular Dialog, as referenced below:
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 for MDatePicker
  7. Usage of MDatePicker

Basic Idea#

A fundamental element of a date picker is a wheel that can set data freely. Here, a custom MPickerView is used as a container for selecting dates and times, allowing selection through up and down scrolling. Based on requirements, canvas is used for drawing. Both dates and times are displayed using MPickerView. The final date picker is encapsulated using MPickerView, and Calendar is used to assemble date and time data. The most important part here is the implementation of MPickerView.

Baseline Calculation#

The text baseline is the reference line for text drawing. Only by determining the baseline can text be accurately drawn at the desired position. Therefore, when it comes to text drawing, 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 at 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#

The 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 to draw
        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 data, which can be divided into three positions: upper, middle, and lower. 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 the 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 referenced below:

//...
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 implementations, refer to the original text at the end. The implementation effect of MPickerView is as follows:

MPickView.gif

Implementation of MDatePicker#

The implementation of MDatePickerDialog is very simple; it is just a custom Dialog. The year, month, day, hour, and minute 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 middle of the screen. For specific implementation, refer to the original text link at the end. Let's look at the features that can be set using MDatePicker, which are configured through the Builder method. Some code is 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 for 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.