实现类似闹钟时间选择的时间选择器

这篇博客介绍了一种不采用三级联动效果的时间选择器实现,通过自定义控件LoopView来完成。作者展示了时间选择器的最终效果,并提供了布局文件activity_time_picker.xml的引用信息。

时间选择器大多是年月日那种三级联动的,今天实现了一个小小的区别三级联动的时间选择器,先把效果图贴出来给大家展示一下下:


图片有点大,大家将就看一下,1 这个效果需要先自定义个控件如下LoopView:

public class LoopView extends View {
    private static final String TAG = LoopView.class.getSimpleName();

    public static final int MSG_INVALIDATE = 1000;
    public static final int MSG_SCROLL_LOOP = 2000;
    public static final int MSG_SELECTED_ITEM = 3000;

    private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
    private ScheduledFuture<?> mScheduledFuture;
    private int mTotalScrollY;
    private LoopScrollListener mLoopListener;
    private GestureDetector mGestureDetector;
    private int mSelectedItem;
    private GestureDetector.SimpleOnGestureListener mOnGestureListener;
    private Context mContext;
    private Paint mTopBottomTextPaint;  //paint that draw top and bottom text
    private Paint mCenterTextPaint;  // paint that draw center text
    private Paint mCenterLinePaint;  // paint that draw line besides center text
    private ArrayList mDataList;
    private int mTextSize;
    private int mMaxTextWidth;
    private int mMaxTextHeight;
    private int mTopBottomTextColor;
    private int mCenterTextColor;
    private int mCenterLineColor;
    private float lineSpacingMultiplier;
    private boolean mCanLoop;
    private int mTopLineY;
    private int mBottomLineY;
    private int mCurrentIndex;
    private int mInitPosition;
    private int mPaddingLeftRight;
    private int mPaddingTopBottom;
    private float mItemHeight;
    private int mDrawItemsCount;
    private int mCircularDiameter;
    private int mWidgetHeight;
    private int mCircularRadius;
    private int mWidgetWidth;


    public Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == MSG_INVALIDATE)
                invalidate();
            if (msg.what == MSG_SCROLL_LOOP)
                startSmoothScrollTo();
            else if (msg.what == MSG_SELECTED_ITEM)
                itemSelected();
            return false;
        }
    });

    public LoopView(Context context) {
        this(context, null);
    }

    public LoopView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LoopView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context,attrs);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public LoopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context,attrs);
    }


    private void initView(Context context,AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoopView);
        if (array != null) {
            mTopBottomTextColor = array.getColor(R.styleable.LoopView_topBottomTextColor, 0xffafafaf);
            mCenterTextColor = array.getColor(R.styleable.LoopView_centerTextColor, 0xff313131);
            mCenterLineColor = array.getColor(R.styleable.LoopView_lineColor, 0xffc5c5c5);
            mCanLoop = array.getBoolean(R.styleable.LoopView_canLoop, true);
            mInitPosition = array.getInt(R.styleable.LoopView_initPosition, -1);
            mTextSize = array.getDimensionPixelSize(R.styleable.LoopView_textSize, sp2px(context, 16));
            mDrawItemsCount = array.getInt(R.styleable.LoopView_drawItemCount, 7);
            array.recycle();
        }

        lineSpacingMultiplier = 2.0F;

        this.mContext = context;

        mOnGestureListener = new LoopViewGestureListener();

        mTopBottomTextPaint = new Paint();
        mCenterTextPaint = new Paint();
        mCenterLinePaint = new Paint();

        if (android.os.Build.VERSION.SDK_INT >= 11) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }

        mGestureDetector = new GestureDetector(context, mOnGestureListener);
        mGestureDetector.setIsLongpressEnabled(false);
    }


    private void initData() {

        if (mDataList == null) {
            throw new IllegalArgumentException("data list must not be null!");
        }
        mTopBottomTextPaint.setColor(mTopBottomTextColor);
        mTopBottomTextPaint.setAntiAlias(true);
        mTopBottomTextPaint.setTypeface(Typeface.MONOSPACE);
        mTopBottomTextPaint.setTextSize(mTextSize);

        mCenterTextPaint.setColor(mCenterTextColor);
        mCenterTextPaint.setAntiAlias(true);
        mCenterTextPaint.setTextScaleX(1.05F);
        mCenterTextPaint.setTypeface(Typeface.MONOSPACE);
        mCenterTextPaint.setTextSize(mTextSize);

        mCenterLinePaint.setColor(mCenterLineColor);
        mCenterLinePaint.setAntiAlias(true);
        mCenterLinePaint.setTypeface(Typeface.MONOSPACE);
        mCenterLinePaint.setTextSize(mTextSize);

        measureTextWidthHeight();

        //计算半圆周 -- mMaxTextHeight * lineSpacingMultiplier 表示每个item的高度  mDrawItemsCount = 7
        //实际显示5个,留两个是在圆周的上下面
        //lineSpacingMultiplier是指text上下的距离的值和maxTextHeight一样的意思 所以 = 2
        //mDrawItemsCount - 1 代表圆周的上下两面各被剪切了一半 相当于高度少了一个 mMaxTextHeight
        int mHalfCircumference = (int) (mMaxTextHeight * lineSpacingMultiplier * (mDrawItemsCount - 1));
        //the diameter of circular 2πr = cir, 2r = height
        mCircularDiameter = (int) ((mHalfCircumference * 2) / Math.PI);
        //the radius of circular
        mCircularRadius = (int) (mHalfCircumference / Math.PI);
        // FIXME: 7/8/16  通过控件的高度来计算圆弧的周长

        if (mInitPosition == -1) {
            if (mCanLoop) {
                mInitPosition = (mDataList.size() + 1) / 2;
            } else {
                mInitPosition = 0;
            }
        }
        mCurrentIndex = mInitPosition;
        invalidate();
    }

    private void measureTextWidthHeight() {
        Rect rect = new Rect();
        for (int i = 0; i < mDataList.size(); i++) {
            String s1 = (String) mDataList.get(i);
            mCenterTextPaint.getTextBounds(s1, 0, s1.length(), rect);
            int textWidth = rect.width();
            if (textWidth > mMaxTextWidth) {
                mMaxTextWidth = textWidth;
            }
            int textHeight = rect.height();
            if (textHeight > mMaxTextHeight) {
                mMaxTextHeight = textHeight;
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mWidgetWidth = getMeasuredWidth();
        mWidgetHeight = MeasureSpec.getSize(heightMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        Log.i(TAG, "onMeasure -> heightMode:" + heightMode);

        mItemHeight = lineSpacingMultiplier * mMaxTextHeight;
        //auto calculate the text's left/right value when draw
        mPaddingLeftRight = (mWidgetWidth - mMaxTextWidth) / 2;
        mPaddingTopBottom = (mWidgetHeight - mCircularDiameter) / 2;

        //topLineY = diameter/2 - itemHeight(mItemHeight)/2 + mPaddingTopBottom
        mTopLineY = (int) ((mCircularDiameter - mItemHeight) / 2.0F) + mPaddingTopBottom;
        mBottomLineY = (int) ((mCircularDiameter + mItemHeight) / 2.0F) + mPaddingTopBottom;
    }


    @Override
    protected void onDraw(Canvas canvas) {

        if (mDataList == null) {
            super.onDraw(canvas);
            return;
        }

        super.onDraw(canvas);

        //the length of single item is mItemHeight
        int mChangingItem = (int) (mTotalScrollY / (mItemHeight));
        mCurrentIndex = mInitPosition + mChangingItem % mDataList.size();
        if (!mCanLoop) { // can loop
            if (mCurrentIndex < 0) {
                mCurrentIndex = 0;
            }
            if (mCurrentIndex > mDataList.size() - 1) {
                mCurrentIndex = mDataList.size() - 1;
            }
        } else { //can not loop
            if (mCurrentIndex < 0) {
                mCurrentIndex = mDataList.size() + mCurrentIndex;
            }
            if (mCurrentIndex > mDataList.size() - 1) {
                mCurrentIndex = mCurrentIndex - mDataList.size();
            }
        }

        int count = 0;
        String itemCount[] = new String[mDrawItemsCount];
        //reconfirm each item's value from dataList according to currentIndex,
        while (count < mDrawItemsCount) {
            int templateItem = mCurrentIndex - (mDrawItemsCount / 2 - count);
            if (mCanLoop) {
                if (templateItem < 0) {
                    templateItem = templateItem + mDataList.size();
                }
                if (templateItem > mDataList.size() - 1) {
                    templateItem = templateItem - mDataList.size();
                }
                itemCount[count] = (String) mDataList.get(templateItem);
            } else if (templateItem < 0) {
                itemCount[count] = "";
            } else if (templateItem > mDataList.size() - 1) {
                itemCount[count] = "";
            } else {
                itemCount[count] = (String) mDataList.get(templateItem);
            }
            count++;
        }

        //draw top and bottom line
        canvas.drawLine(0.0F, mTopLineY, mWidgetWidth, mTopLineY, mCenterLinePaint);
        canvas.drawLine(0.0F, mBottomLineY, mWidgetWidth, mBottomLineY, mCenterLinePaint);

        count = 0;
        int changingLeftY = (int) (mTotalScrollY % (mItemHeight));
        while (count < mDrawItemsCount) {
            canvas.save();
            // L= å * r -> å = rad
            float itemHeight = mMaxTextHeight * lineSpacingMultiplier;
            //get radian  L = (itemHeight * count - changingLeftY),r = mCircularRadius
            double radian = (itemHeight * count - changingLeftY) / mCircularRadius;
            // a = rad * 180 / π
            //get angle
            float angle = (float) (radian * 180 / Math.PI);

            //when angle >= 180 || angle <= 0 don't draw
            if (angle >= 180F || angle <= 0F) {
                canvas.restore();
            } else {
                // translateY = r - r*cos(å) -
                //(Math.sin(radian) * mMaxTextHeight) / 2 this is text offset
                int translateY = (int) (mCircularRadius - Math.cos(radian) * mCircularRadius - (Math.sin(radian) * mMaxTextHeight) / 2) + mPaddingTopBottom;
                canvas.translate(0.0F, translateY);
                //scale offset = Math.sin(radian) -> 0 - 1
                canvas.scale(1.0F, (float) Math.sin(radian));
                if (translateY <= mTopLineY) {
                    //draw text y between 0 -> mTopLineY,include incomplete text
                    canvas.save();
                    canvas.clipRect(0, 0, mWidgetWidth, mTopLineY - translateY);
                    canvas.drawText(itemCount[count], mPaddingLeftRight, mMaxTextHeight, mTopBottomTextPaint);
                    canvas.restore();
                    canvas.save();
                    canvas.clipRect(0, mTopLineY - translateY, mWidgetWidth, (int) (itemHeight));
                    canvas.drawText(itemCount[count], mPaddingLeftRight, mMaxTextHeight, mCenterTextPaint);
                    canvas.restore();
                } else if (mMaxTextHeight + translateY >= mBottomLineY) {
                    //draw text y between  mTopLineY -> mBottomLineY ,include incomplete text
                    canvas.save();
                    canvas.clipRect(0, 0, mWidgetWidth, mBottomLineY - translateY);
                    canvas.drawText(itemCount[count], mPaddingLeftRight, mMaxTextHeight, mCenterTextPaint);
                    canvas.restore();
                    canvas.save();
                    canvas.clipRect(0, mBottomLineY - translateY, mWidgetWidth, (int) (itemHeight));
                    canvas.drawText(itemCount[count], mPaddingLeftRight, mMaxTextHeight, mTopBottomTextPaint);
                    canvas.restore();
                } else if (translateY >= mTopLineY && mMaxTextHeight + translateY <= mBottomLineY) {
                    //draw center complete text
                    canvas.clipRect(0, 0, mWidgetWidth, (int) (itemHeight));
                    canvas.drawText(itemCount[count], mPaddingLeftRight, mMaxTextHeight, mCenterTextPaint);
                    //center one indicate selected item
                    mSelectedItem = mDataList.indexOf(itemCount[count]);
                }
                canvas.restore();
            }
            count++;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionevent) {

        switch (motionevent.getAction()) {
            case MotionEvent.ACTION_UP:
            default:
                if (!mGestureDetector.onTouchEvent(motionevent)) {
                    startSmoothScrollTo();
                }
        }
        return true;
    }

    public final void setCanLoop(boolean canLoop) {
        mCanLoop = canLoop;
        invalidate();
    }

    /**
     * set text size
     *
     * @param size size indicate sp,not px
     */
    public final void setTextSize(float size) {
        if (size > 0) {
            mTextSize = sp2px(mContext, size);
        }
    }

    public void setInitPosition(int initPosition) {
        this.mInitPosition = initPosition;
        invalidate();
    }

    public void setLoopListener(LoopScrollListener LoopListener) {
        mLoopListener = LoopListener;
    }

    /**
     * All public method must be called before this method
     * @param list data list
     */
    public final void setDataList(List<String> list) {
        this.mDataList = (ArrayList) list;
        initData();
    }

    public int getSelectedItem() {
        return mSelectedItem;
    }


    private void itemSelected() {
        if (mLoopListener != null) {
            postDelayed(new SelectedRunnable(), 200L);
        }
    }

    private void cancelSchedule() {

        if (mScheduledFuture != null && !mScheduledFuture.isCancelled()) {
            mScheduledFuture.cancel(true);
            mScheduledFuture = null;
        }
    }

    private void startSmoothScrollTo() {
        int offset = (int) (mTotalScrollY % (mItemHeight));
        cancelSchedule();
        mScheduledFuture = mExecutor.scheduleWithFixedDelay(new HalfHeightRunnable(offset), 0, 10, TimeUnit.MILLISECONDS);
    }

    private void startSmoothScrollTo(float velocityY) {
        cancelSchedule();
        int velocityFling = 20;
        mScheduledFuture = mExecutor.scheduleWithFixedDelay(new FlingRunnable(velocityY), 0, velocityFling, TimeUnit.MILLISECONDS);
    }

    class LoopViewGestureListener extends android.view.GestureDetector.SimpleOnGestureListener {

        @Override
        public final boolean onDown(MotionEvent motionevent) {
            cancelSchedule();
            Log.i(TAG, "LoopViewGestureListener->onDown");
            return true;
        }

        @Override
        public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            startSmoothScrollTo(velocityY);
            Log.i(TAG, "LoopViewGestureListener->onFling");
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.i(TAG, "LoopViewGestureListener->onScroll");
            mTotalScrollY = (int) ((float) mTotalScrollY + distanceY);
            if (!mCanLoop) {
                int initPositionCircleLength = (int) (mInitPosition * (mItemHeight));
                int initPositionStartY = -1 * initPositionCircleLength;
                if (mTotalScrollY < initPositionStartY) {
                    mTotalScrollY = initPositionStartY;
                }

                int circleLength = (int) ((float) (mDataList.size() - 1 - mInitPosition) * (mItemHeight));
                if (mTotalScrollY >= circleLength) {
                    mTotalScrollY = circleLength;
                }
            }

            invalidate();
            return true;
        }
    }

    public int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    class SelectedRunnable implements Runnable {

        @Override
        public final void run() {
            LoopScrollListener listener = LoopView.this.mLoopListener;
            int selectedItem = getSelectedItem();
            mDataList.get(selectedItem);
            listener.onItemSelect(selectedItem);
        }
    }

    /**
     * Use in ACTION_UP
     */
    class HalfHeightRunnable implements Runnable {

        int realTotalOffset;
        int realOffset;
        int offset;

        public HalfHeightRunnable(int offset) {
            this.offset = offset;
            realTotalOffset = Integer.MAX_VALUE;
            realOffset = 0;
        }

        @Override
        public void run() {
            //first in
            if (realTotalOffset == Integer.MAX_VALUE) {

                if ((float) offset > mItemHeight / 2.0F) {
                    //move to next item
                    realTotalOffset = (int) (mItemHeight - (float) offset);
                } else {
                    //move to pre item
                    realTotalOffset = -offset;
                }
            }

            realOffset = (int) ((float) realTotalOffset * 0.1F);

            if (realOffset == 0) {

                if (realTotalOffset < 0) {
                    realOffset = -1;
                } else {
                    realOffset = 1;
                }
            }
            if (Math.abs(realTotalOffset) <= 0) {
                cancelSchedule();
                mHandler.sendEmptyMessage(MSG_SELECTED_ITEM);
                return;
            } else {
                mTotalScrollY = mTotalScrollY + realOffset;
                mHandler.sendEmptyMessage(MSG_INVALIDATE);
                realTotalOffset = realTotalOffset - realOffset;
                return;
            }
        }
    }

    /**
     * Use in {@link LoopViewGestureListener#onFling(MotionEvent, MotionEvent, float, float)}
     */
    class FlingRunnable implements Runnable {

        float velocity;
        final float velocityY;

        FlingRunnable(float velocityY) {
            this.velocityY = velocityY;
            velocity = Integer.MAX_VALUE;
        }

        @Override
        public void run() {
            if (velocity == Integer.MAX_VALUE) {
                if (Math.abs(velocityY) > 2000F) {
                    if (velocityY > 0.0F) {
                        velocity = 2000F;
                    } else {
                        velocity = -2000F;
                    }
                } else {
                    velocity = velocityY;
                }
            }
            Log.i(TAG, "velocity->" + velocity);
            if (Math.abs(velocity) >= 0.0F && Math.abs(velocity) <= 20F) {
                cancelSchedule();
                mHandler.sendEmptyMessage(MSG_SCROLL_LOOP);
                return;
            }
            int i = (int) ((velocity * 10F) / 1000F);
            mTotalScrollY = mTotalScrollY - i;
            if (!mCanLoop) {
                float itemHeight = lineSpacingMultiplier * mMaxTextHeight;
                if (mTotalScrollY <= (int) ((float) (-mInitPosition) * itemHeight)) {
                    velocity = 40F;
                    mTotalScrollY = (int) ((float) (-mInitPosition) * itemHeight);
                } else if (mTotalScrollY >= (int) ((float) (mDataList.size() - 1 - mInitPosition) * itemHeight)) {
                    mTotalScrollY = (int) ((float) (mDataList.size() - 1 - mInitPosition) * itemHeight);
                    velocity = -40F;
                }
            }
            if (velocity < 0.0F) {
                velocity = velocity + 20F;
            } else {
                velocity = velocity - 20F;
            }
            mHandler.sendEmptyMessage(MSG_INVALIDATE);
        }
    }

}
其中,需要在values 文件夹下的attrs.xml文件中定义
<declare-styleable name="LoopView">
        <attr name="lineColor" format="color"/>
        <attr name="topBottomTextColor" format="color"/>
        <attr name="centerTextColor" format="color"/>
        <attr name="textSize" format="dimension"/>
        <attr name="canLoop" format="boolean"/>
        <attr name="initPosition" format="integer"/>
        <attr name="drawItemCount" format="integer"/>
    </declare-styleable>

2 自定义的控件中需要定义一个接口
LoopScrollListener 
很简单,只需要定义
public interface LoopScrollListener {
 void onItemSelect(int item)
}

即可。

3 布局文件中我们直接引用即可布局文件的名称:activity_time_picker.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:background="#33000000">

    <LinearLayout
        android:id="@+id/container_picker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/container_toolbar"
            android:layout_width="660px"
            android:layout_height="90px"
            android:layout_marginBottom="10px"
            android:gravity="center"
            android:background="#f2f7fa"
            android:padding="20px">

            <Button
                android:id="@+id/btn_cancel"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:background="@null"
                android:text="@string/alarmclock_naozhong_dingshi_quxiao"
                android:textColor="@color/dcjbqzi"
                android:textSize="@dimen/size_24px"/>
            <TextView
                android:id="@+id/tv_date_info"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@null"
                android:text="@string/alarmclock_naozhong_dingshi_naozhongsheding"
                android:textColor="@color/dcjbqzi"
                android:layout_marginLeft="80px"
                android:textSize="@dimen/size_32px"
                android:layout_toRightOf="@id/btn_cancel" />

            <Button
                android:id="@+id/btn_confirm"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:layout_alignParentRight="true"
                android:background="@null"
                android:textColor="@color/dcjbqzi"
                android:text="@string/alarmclock_naozhong_dingshi_queren"
                android:textSize="@dimen/size_24px"/>
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:background="#f2f7fa"
            android:padding="3dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginTop="5px">

                <LoopView的全路径
                    android:id="@+id/picker_year"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:padding="10px"
                    app:centerTextColor="@color/dcjbqzi"
                    app:textSize="@dimen/size_30px"
                    app:canLoop="false"/>
public class DatePickerPopWin extends PopupWindow implements View.OnClickListener{
    public Button cancelBtn;
    public TextView infoTv;
    public Button confirmBtn;
    public LoopView yearLoopView;
    public LoopView monthLoopView;
    public LoopView dayLoopView;
    public View pickerContainerV;
    public View contentView;//root view

    private int am; // 上午
    private int pm;     //下午
    private int yearPos = 0;
    private int monthPos = 0;
    private int dayPos = 0;
    private Context mContext;
    private String textCancel;
    private String textConfirm;
    private int colorCancel;
    private int colorConfirm;
    private int btnTextsize;//text btnTextsize of cancel and confirm button
    private int viewTextSize;
    private boolean showDayMonthYear;

    List<String> yearList = new ArrayList();
    List<String> monthList = new ArrayList();
    List<String> dayList = new ArrayList();

    public static class Builder{

        //Required
        private Context context;
        private OnDatePickedListener listener;
        public Builder(Context context,OnDatePickedListener listener){
            this.context = context;
            this.listener = listener;
        }

        //Option
        private boolean showDayMonthYear = false;
        private int am ;
        private int pm ;
        private String textCancel = "取消";
        private String textConfirm = "确认";
        private String textInfo = "确认";
        private String dateChose = getStrDate();
        private int colorCancel = Color.parseColor("#a1aeb3");
        private int colorConfirm = Color.parseColor("#2c95ee");
        private int btnTextSize = 16;//text btnTextsize of cancel and confirm button
        private int viewTextSize = 25;

        public Builder minYear(String am){
            this.am = 1;
            return this;
        }

        public Builder maxYear(String pm){
            this.pm = 2;
            return this;
        }

        public Builder textCancel(String textCancel){
            this.textCancel = textCancel;
            return this;
        }

        public Builder textInfo(String textInfo){
            this.textInfo = textInfo;
            return this;
        }

        public Builder textConfirm(String textConfirm){
            this.textConfirm = textConfirm;
            return this;
        }

        public Builder dateChose(String dateChose){
            this.dateChose = dateChose;
            return this;
        }

        public Builder colorCancel(int colorCancel){
            this.colorCancel = colorCancel;
            return this;
        }

        public Builder colorConfirm(int colorConfirm){
            this.colorConfirm = colorConfirm;
            return this;
        }

        /**
         * set btn text btnTextSize
         * @param textSize dp
         */
        public Builder btnTextSize(int textSize){
            this.btnTextSize = textSize;
            return this;
        }

        public Builder viewTextSize(int textSize){
            this.viewTextSize = textSize;
            return this;
        }

        public DatePickerPopWin build(){
            if(am > pm){
                throw new IllegalArgumentException();
            }
            return new DatePickerPopWin(this);
        }

        public Builder showDayMonthYear(boolean useDayMonthYear) {
            this.showDayMonthYear = useDayMonthYear;
            return this;
        }
    }

    public DatePickerPopWin(Builder builder){
        this.am = builder.am;
        this.pm = builder.pm;
        this.textCancel = builder.textCancel;
        this.textConfirm = builder.textConfirm;
        this.mContext = builder.context;
        this.mListener = builder.listener;
        this.colorCancel = builder.colorCancel;
        this.colorConfirm = builder.colorConfirm;
        this.btnTextsize = builder.btnTextSize;
        this.viewTextSize = builder.viewTextSize;
        this.showDayMonthYear = builder.showDayMonthYear;
        setSelectedDate(builder.dateChose);
        initView();
    }

    private OnDatePickedListener mListener;

    private void initView() {

        contentView = LayoutInflater.from(mContext).inflate(showDayMonthYear ? R.layout.layout_date_picker_inverted : R.layout.activity_time_picker, null);
        cancelBtn = (Button) contentView.findViewById(R.id.btn_cancel);
        infoTv = (TextView)contentView.findViewById(R.id.tv_date_info);
        confirmBtn = (Button) contentView.findViewById(R.id.btn_confirm);
        yearLoopView = (LoopView) contentView.findViewById(R.id.picker_year);
        monthLoopView = (LoopView) contentView.findViewById(R.id.picker_month);
        dayLoopView = (LoopView) contentView.findViewById(R.id.picker_day);
        pickerContainerV = contentView.findViewById(R.id.container_picker);

//
        //set checked listen 
        yearLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                yearPos = item;
//                initDayPickerView();
            }
        });

        monthLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                monthPos = item;
                initDayPickerView();
            }
        });

        dayLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                dayPos = item;
            }
        });

        initPickerViews(); // init year and month loop view
        initDayPickerView(); //init day loop view

        cancelBtn.setOnClickListener(this);
        confirmBtn.setOnClickListener(this);
        contentView.setOnClickListener(this);

        if(!TextUtils.isEmpty(textConfirm)){
            confirmBtn.setText(textConfirm);
        }

        if(!TextUtils.isEmpty(textCancel)){
            cancelBtn.setText(textCancel);
        }

        setTouchable(true);
        setFocusable(true);
        // setOutsideTouchable(true);
        setBackgroundDrawable(new BitmapDrawable());
        setAnimationStyle(R.style.FadeInPopWin);
        setContentView(contentView);
        setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
    }

    /**
     * Init year and month loop view,
     * Let the day loop view be handled separately
     */
    private void initPickerViews() {
        int yearCount = pm - am;
        for (int i = 0; i <= yearCount; i++) {
            if (i == 0){
                yearList.add("上午");
            }else{
                yearList.add("下午");
            }

        }

        
        for (int j = 0; j < 12; j++) {
            monthList.add(format2LenStr(j + 1));
        }

        yearLoopView.setDataList((ArrayList) yearList);
        yearLoopView.setInitPosition(yearPos);

        monthLoopView.setDataList((ArrayList) monthList);
        monthLoopView.setInitPosition(monthPos);
    }

    /**
     * Init day item
     */
    private void initDayPickerView() {

        Calendar calendar = Calendar.getInstance();
        dayList = new ArrayList<String>();
        for (int i = 0; i<60;i++){
            if (i < 10){
                //灏嗗崟浣嶆暟瀛楁敼鎴愬弻浣嶆暟瀛?                String str = String.valueOf(i);
                if (str.length() == 1){
                    str = "0"+str;
                    dayList.add(String.valueOf(str));
                }
            }else{
                dayList.add(String.valueOf(i));
            }

        }

        calendar.set(Calendar.MONTH, monthPos);

        dayLoopView.setDataList((ArrayList) dayList);
        dayLoopView.setInitPosition(dayPos);
    }

    /**
     * set selected date position value when initView.
     *
     * @param dateStr
     */
    public void setSelectedDate(String dateStr) {

        if (!TextUtils.isEmpty(dateStr)) {

            long milliseconds = getLongFromyyyyMMdd(dateStr);
            Calendar calendar = Calendar.getInstance(Locale.CHINA);

            if (milliseconds != -1) {

                calendar.setTimeInMillis(milliseconds);

                monthPos = calendar.get(Calendar.MONTH);
            }
        }
    }

    /**
     * Show date picker popWindow
     *
     * @param activity
     */
    public void showPopWin(Activity activity) {

        if (null != activity) {

            TranslateAnimation trans = new TranslateAnimation(
                    Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF,
                    0, Animation.RELATIVE_TO_SELF, 1,
                    Animation.RELATIVE_TO_SELF, 0);

            showAtLocation(activity.getWindow().getDecorView(), Gravity.BOTTOM,
                    0, 0);
            trans.setDuration(400);
            trans.setInterpolator(new AccelerateDecelerateInterpolator());

            pickerContainerV.startAnimation(trans);
        }
    }

    /**
     * Dismiss date picker popWindow
     */
    public void dismissPopWin() {

        TranslateAnimation trans = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
                Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1);

        trans.setDuration(400);
        trans.setInterpolator(new AccelerateInterpolator());
        trans.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {

                dismiss();
            }
        });

        pickerContainerV.startAnimation(trans);
    }

    @Override
    public void onClick(View v) {

        if (v == contentView || v == cancelBtn) {

            dismissPopWin();
        } else if (v == confirmBtn) {

            if (null != mListener) {

                //涓婂崍涓嬪崍锛屽皬鏃讹紝鍒嗛挓
                int amOrpm = am + yearPos;
                int hour = monthPos + 1;
                int minute = dayPos + 1;
                StringBuffer sb = new StringBuffer();

                if (amOrpm == 1){
                    sb.append("涓婂崍");
                }else{
                    sb.append("涓嬪崍");
                }
                sb.append("-");
                sb.append(format2LenStr(hour));
                sb.append("-");
                sb.append(format2LenStr(minute-1));
                mListener.onDatePickCompleted(amOrpm, hour, minute, sb.toString());
            }

            dismissPopWin();
        }
    }

    /**
     * get long from yyyy-MM-dd
     *
     * @param date
     * @return
     */
    public static long getLongFromyyyyMMdd(String date) {
        SimpleDateFormat mFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
        Date parse = null;
        try {
            parse = mFormat.parse(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        if (parse != null) {
            return parse.getTime();
        } else {
            return -1;
        }
    }

    public static String getStrDate() {
        SimpleDateFormat dd = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
        return dd.format(new Date());
    }

    /**
     * Transform int to String with prefix "0" if less than 10
     * @param num
     * @return
     */
    public static String format2LenStr(int num) {

        return (num < 10) ? "0" + num : String.valueOf(num);
    }

    public static int spToPx(Context context, int spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }


    public interface OnDatePickedListener {

        /**
         * Listener when date has been checked
         *
         * @param amOrpm
         * @param hour
         * @param minute
         * @param dateDesc  yyyy-MM-dd
         */
        void onDatePickCompleted(int amOrpm, int hour, int minute,
                                 String dateDesc);
    }
}

<LoopView的全路径 android:id="@+id/picker_month" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="1dp" android:layout_weight="1" android:padding="10px" app:centerTextColor="@color/dcjbqzi" app:textSize="@dimen/size_38px" app:canLoop="false"/> <LoopView的全路径 android:id="@+id/picker_day" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="1dp" android:padding="10px" android:layout_weight="1" app:centerTextColor="@color/dcjbqzi" app:textSize="@dimen/size_38px" app:canLoop="false"/> </LinearLayout> </RelativeLayout> </LinearLayout></RelativeLayout>


4 我们知道这个时间选择器时通过一个popupwindow弹出的窗口,所以接下来我们应该定义一个弹窗:DatePickerPopWin

public class DatePickerPopWin extends PopupWindow implements View.OnClickListener{
    public Button cancelBtn;
    public TextView infoTv;
    public Button confirmBtn;
    public LoopView yearLoopView;
    public LoopView monthLoopView;
    public LoopView dayLoopView;
    public View pickerContainerV;
    public View contentView;//root view

    private int am; // 上午
    private int pm;     //下午
    private int yearPos = 0;
    private int monthPos = 0;
    private int dayPos = 0;
    private Context mContext;
    private String textCancel;
    private String textConfirm;
    private int colorCancel;
    private int colorConfirm;
    private int btnTextsize;//text btnTextsize of cancel and confirm button
    private int viewTextSize;
    private boolean showDayMonthYear;

    List<String> yearList = new ArrayList();
    List<String> monthList = new ArrayList();
    List<String> dayList = new ArrayList();

    public static class Builder{

        //Required
        private Context context;
        private OnDatePickedListener listener;
        public Builder(Context context,OnDatePickedListener listener){
            this.context = context;
            this.listener = listener;
        }

        //Option
        private boolean showDayMonthYear = false;
        private int am ;
        private int pm ;
        private String textCancel = "取消";
        private String textConfirm = "确认";
        private String textInfo = "确认";
        private String dateChose = getStrDate();
        private int colorCancel = Color.parseColor("#a1aeb3");
        private int colorConfirm = Color.parseColor("#2c95ee");
        private int btnTextSize = 16;//text btnTextsize of cancel and confirm button
        private int viewTextSize = 25;

        public Builder minYear(String am){
            this.am = 1;
            return this;
        }

        public Builder maxYear(String pm){
            this.pm = 2;
            return this;
        }

        public Builder textCancel(String textCancel){
            this.textCancel = textCancel;
            return this;
        }

        public Builder textInfo(String textInfo){
            this.textInfo = textInfo;
            return this;
        }

        public Builder textConfirm(String textConfirm){
            this.textConfirm = textConfirm;
            return this;
        }

        public Builder dateChose(String dateChose){
            this.dateChose = dateChose;
            return this;
        }

        public Builder colorCancel(int colorCancel){
            this.colorCancel = colorCancel;
            return this;
        }

        public Builder colorConfirm(int colorConfirm){
            this.colorConfirm = colorConfirm;
            return this;
        }

        /**
         * set btn text btnTextSize
         * @param textSize dp
         */
        public Builder btnTextSize(int textSize){
            this.btnTextSize = textSize;
            return this;
        }

        public Builder viewTextSize(int textSize){
            this.viewTextSize = textSize;
            return this;
        }

        public DatePickerPopWin build(){
            if(am > pm){
                throw new IllegalArgumentException();
            }
            return new DatePickerPopWin(this);
        }

        public Builder showDayMonthYear(boolean useDayMonthYear) {
            this.showDayMonthYear = useDayMonthYear;
            return this;
        }
    }

    public DatePickerPopWin(Builder builder){
        this.am = builder.am;
        this.pm = builder.pm;
        this.textCancel = builder.textCancel;
        this.textConfirm = builder.textConfirm;
        this.mContext = builder.context;
        this.mListener = builder.listener;
        this.colorCancel = builder.colorCancel;
        this.colorConfirm = builder.colorConfirm;
        this.btnTextsize = builder.btnTextSize;
        this.viewTextSize = builder.viewTextSize;
        this.showDayMonthYear = builder.showDayMonthYear;
        setSelectedDate(builder.dateChose);
        initView();
    }

    private OnDatePickedListener mListener;

    private void initView() {

        contentView = LayoutInflater.from(mContext).inflate(showDayMonthYear ? R.layout.layout_date_picker_inverted : R.layout.activity_time_picker, null);
        cancelBtn = (Button) contentView.findViewById(R.id.btn_cancel);
        infoTv = (TextView)contentView.findViewById(R.id.tv_date_info);
        confirmBtn = (Button) contentView.findViewById(R.id.btn_confirm);
        yearLoopView = (LoopView) contentView.findViewById(R.id.picker_year);
        monthLoopView = (LoopView) contentView.findViewById(R.id.picker_month);
        dayLoopView = (LoopView) contentView.findViewById(R.id.picker_day);
        pickerContainerV = contentView.findViewById(R.id.container_picker);

//
        //set checked listen 
        yearLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                yearPos = item;
//                initDayPickerView();
            }
        });

        monthLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                monthPos = item;
                initDayPickerView();
            }
        });

        dayLoopView.setLoopListener(new LoopScrollListener() {
            @Override
            public void onItemSelect(int item) {
                dayPos = item;
            }
        });

        initPickerViews(); // init year and month loop view
        initDayPickerView(); //init day loop view

        cancelBtn.setOnClickListener(this);
        confirmBtn.setOnClickListener(this);
        contentView.setOnClickListener(this);

        if(!TextUtils.isEmpty(textConfirm)){
            confirmBtn.setText(textConfirm);
        }

        if(!TextUtils.isEmpty(textCancel)){
            cancelBtn.setText(textCancel);
        }

        setTouchable(true);
        setFocusable(true);
        // setOutsideTouchable(true);
        setBackgroundDrawable(new BitmapDrawable());
        setAnimationStyle(R.style.FadeInPopWin);
        setContentView(contentView);
        setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
    }

    /**
     * Init year and month loop view,
     * Let the day loop view be handled separately
     */
    private void initPickerViews() {
        int yearCount = pm - am;
        for (int i = 0; i <= yearCount; i++) {
            if (i == 0){
                yearList.add("上午");
            }else{
                yearList.add("下午");
            }

        }

        
        for (int j = 0; j < 12; j++) {
            monthList.add(format2LenStr(j + 1));
        }

        yearLoopView.setDataList((ArrayList) yearList);
        yearLoopView.setInitPosition(yearPos);

        monthLoopView.setDataList((ArrayList) monthList);
        monthLoopView.setInitPosition(monthPos);
    }

    /**
     * Init day item
     */
    private void initDayPickerView() {

        Calendar calendar = Calendar.getInstance();
        dayList = new ArrayList<String>();
        for (int i = 0; i<60;i++){
            if (i < 10){
                String str = String.valueOf(i);
                if (str.length() == 1){
                    str = "0"+str;
                    dayList.add(String.valueOf(str));
                }
            }else{
                dayList.add(String.valueOf(i));
            }

        }

        calendar.set(Calendar.MONTH, monthPos);

        dayLoopView.setDataList((ArrayList) dayList);
        dayLoopView.setInitPosition(dayPos);
    }

    /**
     * set selected date position value when initView.
     *
     * @param dateStr
     */
    public void setSelectedDate(String dateStr) {

        if (!TextUtils.isEmpty(dateStr)) {

            long milliseconds = getLongFromyyyyMMdd(dateStr);
            Calendar calendar = Calendar.getInstance(Locale.CHINA);

            if (milliseconds != -1) {

                calendar.setTimeInMillis(milliseconds);

                monthPos = calendar.get(Calendar.MONTH);
            }
        }
    }

    /**
     * Show date picker popWindow
     *
     * @param activity
     */
    public void showPopWin(Activity activity) {

        if (null != activity) {

            TranslateAnimation trans = new TranslateAnimation(
                    Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF,
                    0, Animation.RELATIVE_TO_SELF, 1,
                    Animation.RELATIVE_TO_SELF, 0);

            showAtLocation(activity.getWindow().getDecorView(), Gravity.BOTTOM,
                    0, 0);
            trans.setDuration(400);
            trans.setInterpolator(new AccelerateDecelerateInterpolator());

            pickerContainerV.startAnimation(trans);
        }
    }

    /**
     * Dismiss date picker popWindow
     */
    public void dismissPopWin() {

        TranslateAnimation trans = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
                Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1);

        trans.setDuration(400);
        trans.setInterpolator(new AccelerateInterpolator());
        trans.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {

                dismiss();
            }
        });

        pickerContainerV.startAnimation(trans);
    }

    @Override
    public void onClick(View v) {

        if (v == contentView || v == cancelBtn) {

            dismissPopWin();
        } else if (v == confirmBtn) {

            if (null != mListener) {

          
                int amOrpm = am + yearPos;
                int hour = monthPos + 1;
                int minute = dayPos + 1;
                StringBuffer sb = new StringBuffer();

                if (amOrpm == 1){
                    sb.append("上午");
                }else{
                    sb.append("下午");
                }
                sb.append("-");
                sb.append(format2LenStr(hour));
                sb.append("-");
                sb.append(format2LenStr(minute-1));
                mListener.onDatePickCompleted(amOrpm, hour, minute, sb.toString());
            }

            dismissPopWin();
        }
    }

    /**
     * get long from yyyy-MM-dd
     *
     * @param date
     * @return
     */
    public static long getLongFromyyyyMMdd(String date) {
        SimpleDateFormat mFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
        Date parse = null;
        try {
            parse = mFormat.parse(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        if (parse != null) {
            return parse.getTime();
        } else {
            return -1;
        }
    }

    public static String getStrDate() {
        SimpleDateFormat dd = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
        return dd.format(new Date());
    }

    /**
     * Transform int to String with prefix "0" if less than 10
     * @param num
     * @return
     */
    public static String format2LenStr(int num) {

        return (num < 10) ? "0" + num : String.valueOf(num);
    }

    public static int spToPx(Context context, int spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }


    public interface OnDatePickedListener {

        /**
         * Listener when date has been checked
         *
         * @param amOrpm
         * @param hour
         * @param minute
         * @param dateDesc  yyyy-MM-dd
         */
        void onDatePickCompleted(int amOrpm, int hour, int minute,
                                 String dateDesc);
    }
}
5 接下来只需要在我们想要弹出时间选择器的那个activity的某一个控件的点击事件中直接new 出来我们创建的popwindow就好

private void showTimeDialog() {
        DatePickerPopWin pickerPopWin = new DatePickerPopWin.Builder(AlarmClockActivity.this, new DatePickerPopWin.OnDatePickedListener() {
            @Override
            public void onDatePickCompleted(int year, int month, int day, String dateDesc) {
                //将选择的结果进行操作,此处你可以弹出一个土司或者直接打印log来看看结果
            }
        }).textConfirm("确认") //text of confirm button
                .textInfo("闹钟定时")
                .textCancel("取消") //text of cancel button
                .btnTextSize(12) // button text size
                .viewTextSize(16) // pick view text size
                .colorCancel(Color.parseColor("#183b7b")) //color of cancel button
                .colorConfirm(Color.parseColor("#183b7b"))//color of confirm button
                .minYear("上午") //min year in loop
                .maxYear("下午") // max year in loop
                .dateChose("上午-01-00") // date chose when init popwindow
                .build();
        pickerPopWin.showPopWin(AlarmClockActivity.this);
    }

第4步中的红色部分忘记贴出来了,再次补充一下
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#33000000">

    <LinearLayout
        android:id="@+id/container_picker"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#f2f7fa"
        android:orientation="vertical" >

        <RelativeLayout
            android:id="@+id/container_toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3px"
            android:padding="5dp" >

            <Button
                android:id="@+id/btn_cancel"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:background="@null"
                tools:text="取消"
                android:textSize="14sp" />
            <TextView
                android:id="@+id/tv_date_info"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:background="@null"
                android:text="闹钟设定"
                android:gravity="center"
                android:layout_marginLeft="110px"
                android:layout_toRightOf="@id/btn_cancel"
                android:textSize="20sp" />

            <Button
                android:id="@+id/btn_confirm"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:layout_alignParentRight="true"
                android:background="@null"
                tools:text="确定"
                android:textSize="14sp"/>
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:background="#f2f7fa"
            android:padding="3dp" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent" >

                <com.bruce.pickerview.LoopView
                    android:id="@+id/picker_day"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="1dp"
                    android:layout_weight="1" />

                <com.bruce.pickerview.LoopView
                    android:id="@+id/picker_month"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="1dp"
                    android:layout_weight="1" />

                <com.bruce.pickerview.LoopView
                    android:id="@+id/picker_year"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1" />
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

这样还有一个小小的问题就是弹窗出现和消失的方式那个只需要你在res 文件夹下创建一个anim的文件夹,在这文件下我们可以创建弹出弹出和消失的动画,大家可以随意创建,我就不贴出来啦。这样就实现了最初的效果,很简单,希望对大家有那么一小丢丢的帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值