Android自定义View 时段选择器

本文介绍了一种自定义的时间段选择视图,该视图基于Android Canvas实现,允许用户通过触摸操作选择可用的时间段,并提供了视觉反馈以区分可用、已预订和重叠的时间段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先看下效果

这里写图片描述

一开始做的gif一直太大了,无法上传,只能调整了分辨率和播放时间,别嫌看不清~

大致用语言描述下,就是一个选择时间段的自定义view,全部都是通过canvas绘制,红色块表示无法选择,蓝色表示可选择,通过手指拖动蓝色块,拖动蓝色块下方白色小点改变蓝色块的大小,当出现红蓝色块重叠时,蓝色块变色为橙色。

接下来就是代码啦,仔细看注释哦


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.widget.ScrollView;

import com.asiainfo.banbanapp.tools.LocalDisplay;
import com.asiainfo.banbanapp.tools.LogUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by hubert
 * <p>
 * Created on 2017/6/7.
 */

public class TimeSectionPicker extends View implements View.OnTouchListener {

    public static final int TYPE_MOVE = 1;
    public static final int TYPE_EXTEND = 2;
    public static final int TYPE_CLICK = 3;

    private static String[] titles = {"09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", "12:30"
            , "13:00", "13:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00"};

    private static String subTitle = "30m";

    private int lineColor = Color.parseColor("#dedede");

    private int lightTitleColor = Color.parseColor("#71baff");
    private int titleColor = Color.parseColor("#666666");
    private int textSize = LocalDisplay.dp2px(12);

    private int textColor = Color.parseColor("#fefefe");
    private int bookColor = Color.parseColor("#71baff");
    private int bookStrokeColor = Color.parseColor("#71baff");
    private int usedColor = Color.parseColor("#f3928a");
    private int usedStrokeColor = Color.parseColor("#f3928a");

    private int overdueColor = Color.parseColor("#c7c7c7");

    private int overlappingColor = Color.parseColor("#ff9971");
    private float round = 10f;//区域圆角

    private float extendPointR = LocalDisplay.dp2px(8);//拉伸点半径
    private int space = LocalDisplay.dp2px(25);//刻度间隔
    private int offset = 100;//短线偏移量

    private boolean isFrist = true;//初始化padding和宽高值
    private int type;//移动.扩展拉伸.点击

    private Paint mPaint;
    private Point p1;
    private Point p2;
    private Rect titleBounds;
    private RectF bookRect;
    private RectF usedRect;
    private int paddingTop;
    private int paddingLeft;
    private int width;
    private float downY;
    private int bookStart = -1;
    private int bookCount = 0;
    private List<int[]> used = new ArrayList<>();
    private List<RectF> usedAreas = new ArrayList<>();
    private int lineNumber;
    private RectF extendPointRect;
    private float bottom;
    public int[] overdue;

    private OverlappingStateChangeListener listener;
    private boolean lastState;
    private OnBookChangeListener bookChangeListener;


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

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

    public TimeSectionPicker(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOnTouchListener(this);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(textSize);

        titleBounds = new Rect();
        mPaint.getTextBounds(titles[0], 0, titles[0].length(), titleBounds);

        p1 = new Point();
        p2 = new Point();
        bookRect = new RectF();
        usedRect = new RectF();
    }

    public void setOverdue(int[] overdue) {
        this.overdue = overdue;
    }

    public void setBookArea(int start, int count) {
        LogUtil.huI("start:" + start + "/count:" + count);
        bookStart = start;
        bookCount = count;
        setBookRect(start, count);
        postInvalidate();
    }

    public void clearBookArea() {
        bookStart = -1;
        bookCount = 0;
        setBookRect(0, 0);
        postInvalidate();
    }

    public void addUsed(int[] area) {
        used.add(area);
        postInvalidate();
    }

    public List<int[]> getUsed() {
        return used;
    }

    public void clearUsed() {
        used.clear();
        overdue = null;
        postInvalidate();
    }

    public int getTimeNumber(int hour, int minute) {
        int result = (hour - 9) * 2;
        if (minute > 30) {
            result += 2;
        } else if (minute > 0) {
            result += 1;
        }
        if (result > titles.length - 1) {
            result = titles.length - 1;
        }
        return result;
    }

    public String[] getBookTime() {
        if (bookStart == -1) {
            return null;
        }
        String[] strings = new String[2];
        strings[0] = titles[bookStart];
        strings[1] = titles[bookStart + bookCount];
        return strings;
    }

    public int getBookCount() {
        return bookCount;
    }

    private String getTimeString(int start, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < titles.length; i++) {
            if (start == i) {
                sb.append(titles[i]);
                sb.append("~");
            }
            if (start + count == i) {
                sb.append(titles[i]);
            }
        }
        return sb.toString();
    }

    public void setOverlappingStateChangeListener(OverlappingStateChangeListener listener) {
        this.listener = listener;
    }

    public void setBookChangeListener(OnBookChangeListener bookChangeListener) {
        this.bookChangeListener = bookChangeListener;
    }

    public boolean isOverlapping() {
        if (bookCount == 0) {
            return false;
        }
        for (RectF usedArea : usedAreas) {
            if (usedArea.intersect(bookRect)) {
                return true;
            }
        }
        return false;
    }

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = 800;//wrap_content的宽
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = space * titles.length;//wrap_content的高
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isFrist) {//初始化参数
            //处理padding
            paddingTop = getPaddingTop();
            int paddingBottom = getPaddingBottom();
            paddingLeft = getPaddingLeft();
            int paddingRight = getPaddingRight();
            width = getWidth() - paddingLeft - paddingRight;
            int height = getHeight() - paddingTop - paddingBottom;

            lineNumber = titles.length;
            LogUtil.huI("titles.length:" + titles.length);
            bookRect.set(paddingLeft + 180, paddingTop + space * bookStart
                    , width - 30, paddingTop + space * (bookStart + bookCount));

            usedRect.set(paddingLeft + 180, paddingTop, width - 30, paddingTop);

            bottom = paddingTop + space * (titles.length - 1);
            isFrist = false;
        }

        //预定框与已预定是否交叠
        boolean overlapping = isOverlapping();
        if (overlapping != lastState && listener != null) {
            listener.onOverlappingStateChanged(overlapping);
            lastState = overlapping;
        }

        //画刻度线
        mPaint.setColor(lineColor);
        for (int i = 0; i < lineNumber; i++) {
            p1.set(i % 2 == 1 ? paddingLeft + offset : paddingLeft, paddingTop + space * i);
            p2.set(width, paddingTop + space * i);
            canvas.drawLine(p1.x, p1.y, p2.x, p2.y, mPaint);
        }

        //画时间文字
        mPaint.setTextAlign(Paint.Align.LEFT);
        for (int i = 0; i < lineNumber; i++) {
            if (i >= bookStart && i <= bookStart + bookCount) {
                mPaint.setColor(lightTitleColor);
            } else {
                mPaint.setColor(titleColor);
            }
            if (i % 2 == 0) {
                canvas.drawText(titles[i], paddingLeft, paddingTop + titleBounds.height() * 1.3f + space * i, mPaint);
            } else {
                if (i == bookStart || i == bookStart + bookCount) {
                    canvas.drawText(subTitle, paddingLeft + titleBounds.width() / 2, paddingTop + titleBounds.height() * 1.2f + space * i, mPaint);
                }
            }
        }
        //画已使用区域
        usedAreas.clear();
        for (int[] ints : used) {
            RectF rectF = new RectF();
            rectF.set(usedRect.left, usedRect.top + space * ints[0]
                    , usedRect.right, usedRect.bottom + space * (ints[0] + ints[1]));
            usedAreas.add(rectF);
            drawUsedRect(rectF, canvas, mPaint, "会议室已预定 " + getTimeString(ints[0], ints[1]));
        }
        //画过期的区域
        if (overdue != null) {
            RectF rectF = new RectF();
            rectF.set(usedRect.left, usedRect.top + space * overdue[0]
                    , usedRect.right, usedRect.bottom + space * (overdue[0] + overdue[1]));
            usedAreas.add(rectF);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(overdueColor);
            canvas.drawRoundRect(rectF, round, round, mPaint);
        }

        //画预定区域
        drawBookRect(canvas, mPaint, overlapping);
    }

    private void drawUsedRect(RectF rectF, Canvas canvas, Paint paint, String text) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(usedStrokeColor);
        canvas.drawRoundRect(rectF, round, round, paint);

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(usedColor);
        canvas.drawRoundRect(rectF, round, round, paint);

        //不需要文字了
//        paint.setTextAlign(Paint.Align.CENTER);
//        paint.setColor(textColor);
//        canvas.drawText(text, rectF.centerX(), rectF.centerY(), paint);
    }

    public void drawBookRect(Canvas canvas, Paint paint, boolean overlapping) {
        if (bookCount == 0) {
            return;
        }
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(overlapping ? overlappingColor : bookStrokeColor);
        canvas.drawRoundRect(bookRect, round, round, paint);

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(overlapping ? overlappingColor : bookColor);
        canvas.drawRoundRect(bookRect, round, round, paint);

        paint.setTextAlign(Paint.Align.CENTER);
        paint.setColor(textColor);
        canvas.drawText(overlapping ? "该时段不可预定" : ("会议室预定 " + getTimeString(bookStart, bookCount))
                , bookRect.centerX(), bookRect.centerY(), paint);

        paint.setColor(Color.WHITE);
        canvas.drawCircle(bookRect.centerX(), bookRect.bottom, extendPointR, paint);
        paint.setColor(overlapping ? overlappingColor : bookStrokeColor);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(bookRect.centerX(), bookRect.bottom, extendPointR, paint);

        extendPointRect = new RectF(bookRect.centerX() - extendPointR * 2, bookRect.bottom - extendPointR * 2
                , bookRect.centerX() + extendPointR * 2, bookRect.bottom + extendPointR * 2);

        //查看扩展点触发区域
//        paint.setColor(Color.BLACK);
//        Log.i("tag", extendPointRect.toString());
//        canvas.drawRect(extendPointRect, paint);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //view独享事件,即父view不可以获取后续事件,scrollview默认是false
        getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float x = event.getX();
                downY = event.getY();
                Log.i("tag", "action down -- x,y:" + x + "," + downY);
                if (extendPointRect != null && extendPointRect.contains(x, downY)) {
                    type = TYPE_EXTEND;
                    return true;
                }
                if (bookRect.contains(x, downY)) {
                    type = TYPE_MOVE;
                    return true;
                }
                if (bookCount == 0 && checkClick(downY) && x > 150) {
                    type = TYPE_CLICK;
                    return true;
                }
                return false;
            case MotionEvent.ACTION_MOVE:
                float currentY = event.getY();
//                Log.i("tag", "action move -- y:" + currentY);
                float dY = currentY - downY;
                //外层联动
                ViewParent p = getParent();
                if (p instanceof ScrollView && type != TYPE_CLICK) {
                    ScrollView parent = (ScrollView) p;
                    parent.scrollBy(0, (int) dY / 2);
                }

                if (bookChangeListener != null) {
                    bookChangeListener.onBookChange();
                }

                switch (type) {
                    case TYPE_MOVE:
                        bookRect.set(bookRect.left, bookRect.top + dY, bookRect.right, bookRect.bottom + dY);
                        bookStart = Math.round((bookRect.top - paddingTop) / space);
                        //边缘修正
                        if (bookRect.top < paddingTop) {
                            bookStart = 0;
                            setBookRect(bookStart, bookCount);
                        }
                        if (bookRect.bottom > bottom) {
                            bookStart = titles.length - 1 - bookCount;
                            setBookRect(bookStart, bookCount);
                        }
                        break;
                    case TYPE_EXTEND:
                        bookRect.set(bookRect.left, bookRect.top, bookRect.right, bookRect.bottom + dY);
                        int end = (int) ((bookRect.bottom - paddingTop) / space);
                        bookCount = end - bookStart;
                        if (bookCount < 1) {
                            bookCount = 1;
                            setBookRect(bookStart, bookCount);
                        }
                        if (bookRect.bottom > bottom) {
                            end = titles.length - 1;
                            bookCount = end - bookStart;
                            setBookRect(bookStart, bookCount);
                        }
                        break;
                    case TYPE_CLICK:
                        break;
                }
                downY = currentY;
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
//                Log.i("tag", "action up --");
                switch (type) {
                    case TYPE_MOVE:
                        if (bookRect.top < paddingTop) {
                            bookStart = 0;
                        }
                        break;
                    case TYPE_EXTEND:
                        int end = Math.round((bookRect.bottom - paddingTop) / space);
                        if (bookRect.bottom > bottom) {
                            end = titles.length - 1;
                        }
                        bookCount = end - bookStart;
                        break;
                    case TYPE_CLICK:
                        bookStart = (int) ((downY - paddingTop) / space);
                        if (bookStart > titles.length - 1 - 2) {
                            bookStart = titles.length - 1 - 2;
                        }
                        bookCount = 2;
                        break;
                }
                setBookRect(bookStart, bookCount);
                postInvalidate();
                break;
        }
        return false;
    }

    private boolean checkClick(float y) {
        for (RectF rectF : usedAreas) {
            if (y >= rectF.top && y <= rectF.bottom) {
                return false;
            }
        }
        //防止点击最下方边界外也绘制book区域
        int max = paddingTop + (lineNumber - 2) * space;
        LogUtil.huI("max:" + max);
        return y <= max;
    }

    private void setBookRect(int start, int count) {
        if (bookChangeListener != null) {
            bookChangeListener.onBookCountChanged(count);
        }
        bookRect.set(bookRect.left, paddingTop + space * start
                , bookRect.right, paddingTop + space * (start + count));
    }

    public interface OverlappingStateChangeListener {
        void onOverlappingStateChanged(boolean isOverlapping);
    }

    public interface OnBookChangeListener {
        void onBookChange();

        void onBookCountChanged(int bookCount);
    }


}

有兴趣的可以直接复制过去试试,估计只有这个LocalDisplay.dp2px(8)会报红,知识一个dp转px的工具,网上很多。
使用时候建议外面嵌套一层ScrollView,这样可以控制控件的大小而不会显得被压缩

附赠稍作修改的ScrollView,只是判断滑动数字区域必然移动ScrollView


/**
 * Created by hubert
 * <p>
 * Created on 2017/6/8.
 */

public class TimeSectionScroller extends ScrollView {

    public TimeSectionScroller(Context context) {
        super(context);
    }

    public TimeSectionScroller(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float x = ev.getX();
                if (x < 150) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_HOVER_MOVE:

                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

小提示:
添加使用的区块,getTimeNumber第一个参数为24制的小时数,第二个参数为分钟数

start = timeSectionPicker.getTimeNumber(13, 0);
end = timeSectionPicker.getTimeNumber(18, 0);
timeSectionPicker.addUsed(new int[]{start, end});
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值