先看下效果
一开始做的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
*
* 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 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
*
* 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});