Android自定义View之区块选择器

作者: 奶盖ww | 来源:发表于2019-08-13 16:05 被阅读14次

    效果

    先来看下效果吧:

    我们来分析这个view需要实现哪些效果。

    • 首先它有一个刻度尺代表了时间段(也可以是别的什么),并且可以看到完整的刻度尺是比屏幕宽度大的,因此肯定需要可以左右滑动。
    • 其次,可以有不可选的区域(gif中灰色块)和选中的区域(gif中蓝色块),点击刻度的空白位置出现或者移动选中区域到点击位置。
    • 点击并拖动选中的区域可以移动,当移动到屏幕两边的时候,下层的刻度也能跟着移动。
    • 还可以点击并拖动选中区域右边的白色小圆改变选中区域的大小,同样到达屏幕边界时下层刻度跟着移动。
    • 当选中区域与不可选区域重叠时,选中区域变色。
    • 选中区域最小为1个刻度,当移动后手指抬起时,选中区域贴合刻度。
    • 最后还需要监听一些状态的变化,如是否重叠,选中区域改变的位置。

    实现

    刻度尺

    别害怕有这么多的功能,我们一个一个来实现。首先是刻度尺,这个简单。由于完整的刻度尺是比屏幕宽度大的,因此我们先来了解几个概念:

    这里手机屏幕的宽度是width,刻度尺的宽度的时maxWidth,我们其实只需要绘制手机屏幕可见的部分就可以了,这里的offset表示手机屏幕的左边与刻度尺左边的偏移量。

    了解了这个概念,我们就来开始写吧,定义一个View,处理下构造都指向3个参数的那个,然后统一做初始化:

    public class SelectView extends View {
        private final int DEFAULT_HEIGHT = dp2px(100);//wrap_content高度
        private Paint mPaint;
    
        public int dp2px(final float dpValue) {
            final float scale = getContext().getResources().getDisplayMetrics().density;
            return (int) (dpValue * scale + 0.5f);
        }
    
        public SelectView(Context context) {
            this(context, null);
        }
    
        public SelectView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SelectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            scroller = new OverScroller(context);
            init();
        }
    
        private void init() {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setTextSize(textSize);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            width = widthSize;
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            if (heightMode == MeasureSpec.EXACTLY) {
    height = heightSize;
            } else {
    height = DEFAULT_HEIGHT;//wrap_content的高
            }
            setMeasuredDimension(width, height);
        }
    }
    
    

    我们在onMeasure中处理了wrap_content的高度。然后在onSizeChanged中获取尺寸参数:

     private int width;//控件宽度
        private int height;//控件高度
        private int maxWidth;//最大内容宽度
        private int totalWidth;//刻度整体宽度(最后一个刻度的文字在刻度外)
        private int minOffset = 0;
        private int maxOffset;
        private int offset = minOffset;//可视区域左边界与整体内容左边界的偏移量
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            totalWidth = titles.length * space;
            maxWidth = totalWidth - space;
            maxOffset = totalWidth - width;
            if (maxOffset < 0) {
    maxOffset = 0;
            }
            areaTop = (1 - areaRate) * height;
        }
    
    

    接着就开始绘制吧:

        private 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 int space = dp2px(40);//刻度间隔
        private int lineWidth = dp2px(1);//刻度线的宽度
        private int textSize = dp2px(12);
        private int textMargin = dp2px(8);//文字与长刻度的margin值
        private int rate = 1;   //短刻度与长刻度数量的比例(>=1)
        private float lineRate = 0.4f;//短刻度与长刻度长度的比例(0.0~1.0)
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            drawLine(canvas);
        }
    
        private void drawLine(Canvas canvas) {
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(lineWidth);
            mPaint.setColor(Color.BLACK);
            canvas.drawLine(0, height, width, height, mPaint);
            for (int i = 0; i < titles.length; i++) {
    int position = i * space;
    if (position >= offset &amp;&amp; position <= offset + width) {//判断是否可以显示在屏幕中
        int x = position - offset;
        if (i % (rate + 1) == 0) {//绘制长刻度
            canvas.drawLine(x, 0, x, height, mPaint);
    
            mPaint.setStyle(Paint.Style.FILL);
            canvas.drawText(titles[i], x + textMargin, textSize, mPaint);
            mPaint.setStyle(Paint.Style.STROKE);
        } else {//绘制短刻度
            canvas.drawLine(x, height * (1 - lineRate), x, height, mPaint);
        }
    }
            }
        }
    
    

    这里的titles代表了刻度的标识,每一个元素代表一个刻度(这里我字节写死了,实际上可以通过方法set,也不一定是时间,能代表刻度的都可以)。通过rate设置长短刻度的比例,这里我设置了1:1。运行一下看看,目前仅仅能看到从0开始,看不到完整的刻度尺,我们需要实现touch事件产生移动才有效果。

    实现滑动刻度尺

    我们重写onTouchEvent来实现滑动效果:

      private float downX, downY;
        private float lastX;//滑动上一个位置
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            switch (action) {
    case MotionEvent.ACTION_DOWN:
        downX = event.getX();
        lastX = downX;
        break;
    case MotionEvent.ACTION_MOVE:
        float x = event.getX();
        float dx = x - lastX;
        changeOffsetBy(-dx);
        lastX = x;
        postInvalidate();
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
    
        break;
    default:
        break;
            }
            return true;
        }
    
        private void changeOffsetBy(float dx) {
            offset += dx;
            if (offset < minOffset) {
    offset = minOffset;
            } else if (offset > maxOffset) {
    offset = maxOffset;
            }
        }
    
    

    我们计算出每次move事件的X方向的变化量dx,然后通过这个dx改变offset,并且处理一下边界的情况。然后调用postInvalidate刷新界面。
    运行一下看看!现在我们可以滑动刻度尺了。但是好像还有点问题,平时我们使用ScrollView的时候用力划一下,可以看到手指离开了屏幕,但是内容还可以继续滚动。而目前我们自定义的这个view只能通过手指滑动,如果手指离开屏幕就不能滑动了。这样的体验显然不够好,我们来实现这个惯性滑动的效果吧!

    实现惯性滑动

    要实现惯性滑动,我们需要用到两个类:VelocityTracker,OverScroller。
    VelocityTracker简介
    view滑动助手类OverScroller

     private int minFlingVelocity;//最小惯性滑动速度
        private VelocityTracker velocityTracker;
        private OverScroller scroller;
        private int lastFling;//惯性滑动上一个位置
    
        private void init(Context context) {
            ...
            scroller = new OverScroller(context);
            minFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (velocityTracker == null) {
    velocityTracker = VelocityTracker.obtain();
            }
            velocityTracker.addMovement(event);
    
             int action = event.getAction();
            switch (action) {
    case MotionEvent.ACTION_DOWN:
        scroller.forceFinished(true);
        downX = event.getX();
        lastX = downX;
        break;
    case MotionEvent.ACTION_MOVE:
        float x = event.getX();
        float dx = x - lastX;
        changeOffsetBy(-dx);
        lastX = x;
        postInvalidate();
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
         //处理惯性滑动
        velocityTracker.computeCurrentVelocity(1000, 8000);
        float xVelocity = velocityTracker.getXVelocity();
        if (Math.abs(xVelocity) > minFlingVelocity) {
            scroller.fling(0, 0, (int) xVelocity, 0, Integer.MIN_VALUE,
    Integer.MAX_VALUE, 0, 0);
        }
        velocityTracker.clear();
        break;
    default:
        break;
            }
            return true;
        }
    
        @Override
        public void computeScroll() {
            if (scroller.computeScrollOffset()) {
    int currX = scroller.getCurrX();
    float dx = currX - lastFling;
    //已经在边界了,不再处理惯性
    if ((offset <= minOffset &amp;&amp; dx > 0) || offset >= maxOffset &amp;&amp; dx < 0) {
        scroller.forceFinished(true);
        return;
    }
    changeOffsetBy(-dx);
    lastFling = currX;
    postInvalidate();
            } else {
    lastFling = 0;//重置上一次值,避免第二次惯性滑动计算错误的dx
            }
        }
    
    

    velocityTracker.computeCurrentVelocity方法的第二个参数表示最大惯性速度,这里我设置8000,避免刻度尺过快的滑动。通过调用scroller.fling方法将计算出的速度交给scroller,然后在computeScroll方法中获取当前值,并与上一次的值做差算出变化量dx,同样用这个dx变化offset刷新界面实现滑动效果。

    不可选区域

    刻度尺完成了,接下来是不可选的灰色区域。我采用两个int值表示在刻度尺的区域,刻度尺的每个刻度表示一个最小单位,前一个int表示在刻度尺的起始位置,后一个int表示占据的刻度数量。

      private List<int[]> unselectableList = new ArrayList<>();
        private List<RectF> unselectableRectFs = new ArrayList<>();
        private RectF tempRect = new RectF();
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            drawLine(canvas);
            drawUnselectable(canvas);
        }
    
        private void drawUnselectable(Canvas canvas) {
            generateUnselectableRectFs();
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.parseColor("#99878787"));
            for (RectF rectF : unselectableRectFs) {
    float left = Math.max(rectF.left, offset) - offset;
    float right = Math.min(rectF.right, offset + width) - offset;
    tempRect.set(left, rectF.top, right, rectF.bottom);
    canvas.drawRect(tempRect, mPaint);
            }
        }
    
        private void generateUnselectableRectFs() {
            //避免重复生成
            if (unselectableRectFs.size() > 0 
        &amp;&amp; unselectableList.size() == unselectableRectFs.size()) {
    return;
            }
            unselectableRectFs.clear();
            for (int[] ints : unselectableList) {
    int start = ints[0];
    int count = ints[1];
    int max = titles.length - 1;
    if (start > max || start + count > max) {
        throw new IllegalArgumentException("unselectable area has wrong start or count, " +
    "the total limit is" + max);
    }
    if (count > 0) {
        unselectableRectFs.add(new RectF(start * space, areaTop,
    (start + count) * space, height));
    }
            }
        }
    
        public void addUnseletable(int start, int count) {
            unselectableList.add(new int[]{start, count});
            postInvalidate();
        }
    
    

    我用一个list存放设置的不可选区域,然后在另一个list中存放转换成RectF的位置信息。这里的RectF是在相对于整体刻度尺而言的,因此绘制到屏幕的时候需要减去offset,并且需要考虑只有部分在屏幕可见的情况。避免在onDraw方法中创建过多临时变量,我声明一个成员变量tempRect,用来保存绘制时的临时参数。

    可选区域

    完成了不可选区域,可选区域也是同样的。由于只能有一个可选区域,我们只需要定义一个RectF。额外需要考虑与不可选区域相交时会变色,我定了一个overlapping表示是否相交,通过RectF的intersects方法判断。

       private int selectedBgColor = Color.parseColor("#654196F5");
        private int selectedStrokeColor = Color.parseColor("#4196F5");
        private int overlappingBgColor = Color.parseColor("#65FF6666");
        private int overlappingStrokeColor = Color.parseColor("#FF6666");
        private int selectedStrokeWidth = dp2px(2);
        private int extendRadius = dp2px(7);//扩展圆的半径
        private float extendTouchRate = 1.5f;//扩展触摸区域与视图的比率(>=1)
    
        private boolean overlapping;//是否覆盖unselectable
        private RectF selectedRectF;//选择区域位置
        private RectF extendPointRectF;//扩展点位置
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            drawLine(canvas);
            drawUnselectable(canvas);
            drawSelected(canvas);
        }
    
        private void drawSelected(Canvas canvas) {
            if (selectedRectF == null) {
    return;
            }
            overlapping = checkOverlapping();
            float left = Math.max(selectedRectF.left, offset) - offset;
            float right = Math.min(selectedRectF.right, offset + width) - offset;
            tempRect.set(left, selectedRectF.top, right, selectedRectF.bottom);
            //填充
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(overlapping ? overlappingBgColor : selectedBgColor);
            canvas.drawRect(tempRect, mPaint);
            //边框
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(selectedStrokeWidth);
            mPaint.setColor(overlapping ? overlappingStrokeColor : selectedStrokeColor);
            canvas.drawRect(tempRect, mPaint);
            if ((selectedRectF.right - offset) == right) {
    //扩展圆边框
    mPaint.setColor(overlapping ? overlappingStrokeColor : selectedStrokeColor);
    canvas.drawCircle(tempRect.right, tempRect.centerY(), extendRadius, mPaint);
    //扩展圆填充
    mPaint.setColor(Color.WHITE);
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawCircle(tempRect.right, tempRect.centerY(), extendRadius, mPaint);
    //扩展圆的位置信息,处理touch事件需要
    extendPointRectF = new RectF(selectedRectF.right - extendRadius * extendTouchRate,
            selectedRectF.centerY() - extendRadius * extendTouchRate,
            selectedRectF.right + extendRadius * extendTouchRate,
            selectedRectF.centerY() + extendRadius * extendTouchRate);
            } else {
    extendPointRectF = null;
            }
        }
    
        private boolean checkOverlapping() {
            if (selectedRectF != null) {
    for (RectF rectF : unselectableRectFs) {
        if (rectF.intersects(selectedRectF.left, selectedRectF.top,
    selectedRectF.right, selectedRectF.bottom)) {
            return true;
        }
    }
            }
            return false;
        }
    
    

    点击,移动,扩展

    通过前面的分析,我们知道这个view中的事件有很多种:点击,移动刻度尺,移动选中区域,扩展选中区域。我们定义这四种类型便于后续的事件处理:

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

    然后改造一下onTouchEvent:

     private boolean linking;//是否正在联动
        private Handler handler = new BookHandler(this);
        private int boundary = space / 2;//屏幕边界范围
    
        private static class BookHandler extends Handler {
            private static final int DELAY_MILLIS = 10;//刷新率(0~16)
            private WeakReference<SelectView> selectViewWeakReference;
    
            BookHandler(SelectView selectView) {
    super();
    selectViewWeakReference = new WeakReference<>(selectView);
            }
    
            @Override
            public void handleMessage(Message msg) {
    SelectView view = selectViewWeakReference.get();
    if (view != null) {
        float dx = (float) msg.obj;
        view.changeOffsetBy(dx);
        if (msg.what == MESSAGE_EXTEND) {
            float r = view.selectedRectF.right + dx;
            view.resetSelectedRight(r);
        } else if (msg.what == MESSAGE_MOVE) {
            float l = view.selectedRectF.left + dx;
            float r = view.selectedRectF.right + dx;
            view.resetSelectedRectF(l, r);
        }
        view.postInvalidate();
        if (view.linking) {
            sendMessageDelayed(Message.obtain(msg), DELAY_MILLIS);
        }
    }
            }
        }
    
        @Override
        public boolean performClick() {
            return super.performClick();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (velocityTracker == null) {
    velocityTracker = VelocityTracker.obtain();
            }
            velocityTracker.addMovement(event);
    
            int action = event.getAction();
            switch (action) {
    case MotionEvent.ACTION_DOWN:
        scroller.forceFinished(true);
        downX = event.getX();
        lastX = downX;
        downY = event.getY();
        checkTouchType();
        break;
    case MotionEvent.ACTION_MOVE:
        float x = event.getX();
        float dx = x - lastX;
        if (touchType == TYPE_EXTEND) {
            handleExtend(dx);
        } else if (touchType == TYPE_MOVE) {
            handleMove(dx);
        } else if (touchType == TYPE_SLIDE) {
            changeOffsetBy(-dx);
        }
        lastX = x;
        postInvalidate();
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
        float upX = event.getX();
        float upY = event.getY();
        if (Math.abs(upX - downX) < touchSlop &amp;&amp; Math.abs(upY - downY) < touchSlop) {
            touchType = TYPE_CLICK;
            performClick();
        }
    
        handleActionUp(upX);
        break;
    default:
        break;
            }
            return true;
        }
    
        private void checkTouchType() {
            RectF extend = null;
            if (extendPointRectF != null) {
    extend = new RectF(extendPointRectF.left - offset, extendPointRectF.top,
            extendPointRectF.right - offset, extendPointRectF.bottom);
    Timber.i("extend:" + extend.toString());
            }
            RectF selected = null;
            if (selectedRectF != null) {
    selected = new RectF(selectedRectF.left - offset, selectedRectF.top,
            selectedRectF.right - offset, selectedRectF.bottom);
    Timber.i("selected:" + selected.toString());
            }
    
            if (extend != null &amp;&amp; extend.contains(lastX, downY)) {
    touchType = TYPE_EXTEND;
            } else if (selected != null &amp;&amp; selected.contains(lastX, downY)) {
    touchType = TYPE_MOVE;
            } else {
    touchType = TYPE_SLIDE;
            }
        }
    
        private void handleExtend(float dx) {
            //如果正在联动时,避免手指抖动造成不必要停止
            if (linking &amp;&amp; Math.abs(dx) < touchSlop) {
    return;
            }
            float right = selectedRectF.right += dx;
            //下层联动
            Message message = null;
            if (dx > 0 &amp;&amp; width - (right - offset) < boundary //选中区域滑到屏幕右边
        &amp;&amp; offset < maxOffset) {
    message = handler.obtainMessage(MESSAGE_EXTEND, linkDx);
            } else if (dx < 0 &amp;&amp; right > selectedRectF.left
        &amp;&amp; right - offset < boundary &amp;&amp; offset > minOffset) {
    message = handler.obtainMessage(MESSAGE_EXTEND, -linkDx);
            }
            if (message != null) {
    if (!linking) {
        linking = true;
        handler.sendMessage(message);
    }
            } else {
    stopLinking();
    resetSelectedRight(right);
            }
        }
    
        private void handleMove(float dx) {
            //如果正在联动时,避免手指抖动造成不必要停止
            if (linking &amp;&amp; Math.abs(dx) < touchSlop) {
    return;
            }
            float left = selectedRectF.left += dx;
            float right = selectedRectF.right += dx;
            Message message = null;
            if ((dx < 0 &amp;&amp; left - offset < boundary &amp;&amp; offset > minOffset)) {//选中区域滑到屏幕左边并继续向左滑动
    message = handler.obtainMessage(MESSAGE_MOVE, -linkDx);
            } else if (dx > 0 &amp;&amp; width - (right - offset) < boundary &amp;&amp; offset < maxOffset) {//选中区域滑到屏幕右边并且继续向右滑动
    message = handler.obtainMessage(MESSAGE_MOVE, linkDx);
            }
            Timber.e("message:" + message);
            if (message != null) {//处在两边界,需要联动
    if (!linking) {
        linking = true;
        handler.sendMessage(message);
    }
            } else {
    stopLinking();
    resetSelectedRectF(left, right);
            }
        }
    
        private void handleActionUp(float upX) {
            if (touchType == TYPE_CLICK) {
    int start = (int) ((upX + offset) / space);
    int[] area = getSelected();
    setSelected(start, area == null ? CLICK_SPACE : area[1]);
            } else if (touchType == TYPE_EXTEND) {
    stopLinking();
    int right = Math.round(selectedRectF.right / space) * space;
    resetSelectedRight(right);
    postInvalidate();
            } else if (touchType == TYPE_MOVE) {
    stopLinking();
    int[] area = getSelected();
    if (area != null) {
        setSelected(area[0], area[1]);
    }
            } else if (touchType == TYPE_SLIDE) {
    //处理惯性滑动
    velocityTracker.computeCurrentVelocity(1000, 8000);
    float xVelocity = velocityTracker.getXVelocity();
    if (Math.abs(xVelocity) > minFlingVelocity) {
        scroller.fling(0, 0, (int) xVelocity, 0, Integer.MIN_VALUE,
    Integer.MAX_VALUE, 0, 0);
    }
    velocityTracker.clear();
            }
        }
    
        private void stopLinking() {
            linking = false;
            handler.removeCallbacksAndMessages(null);
        }
    
        /**
         * 重置选择区域的位置
         *
         * @param left
         * @param right
         */
        private void resetSelectedRectF(float left, float right) {
            if (left < 0) {
    left = 0;
    right = selectedRectF.right - selectedRectF.left;
            }
            if (right > maxWidth) {
    right = maxWidth;
    left = maxWidth - (selectedRectF.right - selectedRectF.left);
            }
            int minSpace = minSelect * space;
            if (right - left < minSpace) {//最小值
    if (maxWidth - selectedRectF.left < minSpace) {
        right = maxWidth;
        left = maxWidth - minSpace;
    } else {
        right = selectedRectF.left + minSpace;
    }
            }
            selectedRectF.left = left;
            selectedRectF.right = right;
        }
    
        /**
         * 重置选择区域的right
         *
         * @param right
         */
        private void resetSelectedRight(float right) {
            if (right > maxWidth) {
    right = maxWidth;
            }
            int minSpace = minSelect * space;
            if (right - selectedRectF.left < minSpace) {//最小值
    if (maxWidth - selectedRectF.left < minSpace) {
        right = maxWidth;
        selectedRectF.left = maxWidth - minSpace;
    } else {
        right = selectedRectF.left + minSpace;
    }
            }
            selectedRectF.right = right;
        }
    
        /**
         * 将选择内容转换成区域
         *
         * @param start 开始位置
         * @param count 数量
         */
        public void setSelected(int start, int count) {
            if (start > titles.length - 1) {
    throw new IllegalArgumentException("wrong start");
            }
            int right = (start + count) * space;
            if (right > maxWidth) {
    //int cut = Math.round((right - maxWidth) * 1f / space);
    //start -= cut;//整体向左移动
    right = maxWidth;
            }
            int left = start * space;
            if (selectedRectF == null) {
    selectedRectF = new RectF(left, areaTop, right, height);
    if (selectChangeListener != null) {
        selectChangeListener.onSelected();
    }
            } else {
    selectedRectF.set(left, areaTop, right, height);
            }
            notifySelectChangeListener(start, count);
            postInvalidate();
        }
    
        /**
         * 将选中区域转换成选择内容
         *
         * @return [start, count]
         */
        public int[] getSelected() {
            if (selectedRectF == null) {
    return null;
            }
            int[] area = new int[2];
            float w = selectedRectF.right - selectedRectF.left;
            area[0] = Math.round(selectedRectF.left / space);
            area[1] = Math.round(w / space);
            return area;
        }
    
    

    performClick会在你重写onTouchEvent时as提示你需要重写的方法,因为你可能没有考虑到如果给这个view设置OnClickListener的情况。如果你没有在onTouchEvent中调用performClick,那么setOnClickListener方法就失效了。

    你可能注意到这一次比较复杂,并且还有一个linking字段,表示是否正在联动,我解释一下这个联动的概念:
    通过gif其实你可能注意到,当我移动或者扩展选中区域的时候,如果移动到了屏幕的边界,后面的刻度尺就会跟着移动,实际上这个时候选中区域在屏幕中的位置没有改变,只是刻度尺移动了。
    一开始我也是通过dx来改变offset,但是存在一个问题,移动到屏幕边缘之后,手指可以移动的区域已经很小了,不会产生足够的dx(手指不移动的话,不会有新的touch事件产生)。
    最好的体验是我把手机移动到屏幕边缘,刻度尺就会自己按照一定的速率移动直到最大offset或者最小offset。于是我使用了Handler,当满足条件后发送消息,表示开始进行联动,会按照固定速度产生一个dx改变offset。当然,在离开屏幕边缘的时候还需要及时取消handler的任务。

    至此,功能基本已经实现了,运行一下看看效果吧~

    后面需要做什么那?现在这个view只能自己玩,我需要它与其他view有交互,比如选中什么区域,状态的改变生么的。

    状态变化

    声明两个接口,并在适当时候回调它们的方法,这样外部就能感知view的状态变化。

        public interface OverlappingStateChangeListener {
            void onOverlappingStateChanged(boolean isOverlapping);
        }
    
        public interface SelectChangeListener {
            void onSelected();
    
            void onSelectChanged(int start, int count);
        }
    
    

    结束语

    后面的话就是根据业务添加一些api了,例如添加不可选区域,改变刻度范围什么,一切都看需求了。

    希望读到这的您能转发分享和关注一下我,以后还会分享Android知识点及解析,您的支持就是我最大的动力!!

    以下文章强烈推荐!!!

    相关文章

      网友评论

        本文标题:Android自定义View之区块选择器

        本文链接:https://www.haomeiwen.com/subject/cqrbjctx.html