自定義View實現一個日期選擇器

NO IMAGE

原文首發於微信公眾號:躬行之(jzman-blog),歡迎關注交流!

通過自定義 View 來實現一個時期時間選擇器,可以放在底部也可以放在中間位置彈出,先來一張效果圖:

自定義View實現一個日期選擇器
MDatePickerDialog.gif

下面簡述一下實現過程:

  1. 基本思路
  2. Baseline計算
  3. 如何實現滾動
  4. 具體繪製
  5. MDatePickerDoialog的實現
  6. MDatePickerDoialog的設置
  7. MDatePickerDoialog的使用

基本思路

日期選擇器的一個最基本元素都是一個可以隨意設置數據的一個滾輪,這裡也是自定義一個 MPickerView 作為日期和時間的選擇容器,通過上下滾動來完成日期或時間的選擇,根據需求使用 canvas 進行繪製,不管是日期還是時間都使用 MPickerView 來展示數據,最終的日期選擇器使用 MPickerView 進行封裝,使用 Calendar 組裝日期時間數據,這裡面最重要的就是 MPickerView 的實現了。

Baseline計算

文字基準線(Baseline)是文字繪製所參考的基準線,確定了文字的基準線,才可以更確切地將文字繪製到想要繪製的位置,所以,如果涉及到文字的繪製一定要按照 Baseline 來進行繪製,繪製文字時其左邊原點在 Baseline 的左端,y 軸方向向上為負,向下為正,具體如下:

自定義View實現一個日期選擇器
MDataPickerBaseline.PNG

因為最終選中的日期或時間要顯示在所繪製 View 的中間位置,那麼,在代碼中如何計算呢?

 //獲取Baseline位置
 Paint.FontMetricsInt metricsInt = paint.getFontMetricsInt();
 float line = mHeight / 2.0f + (metricsInt.bottom - metricsInt.top) / 2.0f - metricsInt.descent;

如何實現滾動

MPickerView 中間位置繪製給定的一組數據的某個位置,這裡繪製的位置總是數據大小 size/2 作為要繪製的數據的 index:

public void setData(@NonNull List<String> data) {
    if (mData != null) {
        mData.clear();
        mData.addAll(data);
        //繪製中心位置的index
        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 的繪製主要是顯示數據的繪製,可以分為上、中、下三個位置的數據的繪製。上面部分就是 index 在 mSelectPosition 前面的數據,中間位置就是 mSelectPosition 所指向的數據,下面部分則是 index 在 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) {
        //年份是四位數
        startX = mPaintSelect.measureText("0000") / 2 + x;
    } else {
        //其他兩位數
        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);
}

上面代碼相關座標計算都與 Baseline 有關,具體代碼實現參考文末閱讀原文,MPickerView 實現效果如下:

自定義View實現一個日期選擇器
MPickView.gif

MDatePickerDoialog的實現

MDatePickerDoialog 的實現非常簡單就是自定義一個 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 即可,具體實現參考文末原文鏈接,來看一下使用 MDatePickerDoialog 可以設置那些功能,這裡通過 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(MDatePickerDialog 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 MDatePickerDialog build() {
        MDatePickerDialog dialog = new MDatePickerDialog(mContext);
        applyConfig(dialog);
        return dialog;
    }
}

MDatePickerDoialog的設置

MDatePickerDialog 常用設置如下:

自定義View實現一個日期選擇器
MDateDoc.PNG

MDatePickerDoialog的使用

MDatePickerDoialog 的使用非常簡單,和普通的 Dialog 使用方式一致,當然下面是比較完整的設置

public void btnClickDateBottom(View view) {
    MDatePickerDialog dialog = new MDatePickerDialog.Builder(this)
            //附加設置(非必須,有默認值)
            .setCanceledTouchOutside(true)
            .setGravity(Gravity.BOTTOM)
            .setSupportTime(false)
            .setTwelveHour(true)
            .setCanceledTouchOutside(false)
            //結果回調(必須)
            .setOnDateResultListener(new MDatePickerDialog.OnDateResultListener() {
                @Override
                public void onDateResult(long date) {
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTimeInMillis(date);
                    SimpleDateFormat dateFormat = (SimpleDateFormat) SimpleDateFormat.getDateInstance();
                    dateFormat.applyPattern("yyyy-MM-dd HH:mm");
                    Toast.makeText(MainActivity.this, dateFormat.format(new Date(date)), Toast.LENGTH_SHORT).show();
                }
            })
            .build();
    dialog.show();
}

具體細節參考如下鏈接或點擊文末閱讀原文,歡迎 star 一下!

https://github.com/jzmanu/MDatePickerSample

個人微信公眾號:jzman-blog 可以一起交流學習,可以回覆關鍵字【加群】邀你入交流群。

自定義View實現一個日期選擇器

相關文章

高德APP全鏈路源碼依賴分析工程

javaWeb學習總結——文件上傳、下載

阿里巴巴業務平臺-招聘“前端、客戶端”開發,虛位以待

mobx源碼解讀(四):講講autorun和reaction