Android自定義View之區(qū)塊選擇器

效果

先來看下效果吧:



我們來分析這個(gè)view需要實(shí)現(xiàn)哪些效果。

  • 首先它有一個(gè)刻度尺代表了時(shí)間段(也可以是別的什么)涡贱,并且可以看到完整的刻度尺是比屏幕寬度大的刹勃,因此肯定需要可以左右滑動(dòng)柄错。
  • 其次剩蟀,可以有不可選的區(qū)域(gif中灰色塊)和選中的區(qū)域(gif中藍(lán)色塊),點(diǎn)擊刻度的空白位置出現(xiàn)或者移動(dòng)選中區(qū)域到點(diǎn)擊位置切威。
  • 點(diǎn)擊并拖動(dòng)選中的區(qū)域可以移動(dòng)育特,當(dāng)移動(dòng)到屏幕兩邊的時(shí)候,下層的刻度也能跟著移動(dòng)先朦。
  • 還可以點(diǎn)擊并拖動(dòng)選中區(qū)域右邊的白色小圓改變選中區(qū)域的大小缰冤,同樣到達(dá)屏幕邊界時(shí)下層刻度跟著移動(dòng)。
  • 當(dāng)選中區(qū)域與不可選區(qū)域重疊時(shí)喳魏,選中區(qū)域變色棉浸。
  • 選中區(qū)域最小為1個(gè)刻度,當(dāng)移動(dòng)后手指抬起時(shí)刺彩,選中區(qū)域貼合刻度迷郑。
  • 最后還需要監(jiān)聽一些狀態(tài)的變化枝恋,如是否重疊,選中區(qū)域改變的位置嗡害。

實(shí)現(xiàn)

刻度尺

別害怕有這么多的功能焚碌,我們一個(gè)一個(gè)來實(shí)現(xiàn)状婶。首先是刻度尺俱诸,這個(gè)簡單。由于完整的刻度尺是比屏幕寬度大的肥橙,因此我們先來了解幾個(gè)概念:


這里手機(jī)屏幕的寬度是width叹螟,刻度尺的寬度的時(shí)maxWidth鹃骂,我們其實(shí)只需要繪制手機(jī)屏幕可見的部分就可以了,這里的offset表示手機(jī)屏幕的左邊與刻度尺左邊的偏移量罢绽。

了解了這個(gè)概念畏线,我們就來開始寫吧,定義一個(gè)View有缆,處理下構(gòu)造都指向3個(gè)參數(shù)的那個(gè)象踊,然后統(tǒng)一做初始化:

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中獲取尺寸參數(shù):

 private int width;//控件寬度
    private int height;//控件高度
    private int maxWidth;//最大內(nèi)容寬度
    private int totalWidth;//刻度整體寬度(最后一個(gè)刻度的文字在刻度外)
    private int minOffset = 0;
    private int maxOffset;
    private int offset = minOffset;//可視區(qū)域左邊界與整體內(nèi)容左邊界的偏移量

    @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;   //短刻度與長刻度數(shù)量的比例(>=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代表了刻度的標(biāo)識棚壁,每一個(gè)元素代表一個(gè)刻度(這里我字節(jié)寫死了杯矩,實(shí)際上可以通過方法set,也不一定是時(shí)間袖外,能代表刻度的都可以)史隆。通過rate設(shè)置長短刻度的比例,這里我設(shè)置了1:1曼验。運(yùn)行一下看看泌射,目前僅僅能看到從0開始,看不到完整的刻度尺鬓照,我們需要實(shí)現(xiàn)touch事件產(chǎn)生移動(dòng)才有效果熔酷。

實(shí)現(xiàn)滑動(dòng)刻度尺

我們重寫onTouchEvent來實(shí)現(xiàn)滑動(dòng)效果:

  private float downX, downY;
    private float lastX;//滑動(dòng)上一個(gè)位置

    @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;
        }
    }

我們計(jì)算出每次move事件的X方向的變化量dx,然后通過這個(gè)dx改變offset豺裆,并且處理一下邊界的情況拒秘。然后調(diào)用postInvalidate刷新界面。
運(yùn)行一下看看臭猜!現(xiàn)在我們可以滑動(dòng)刻度尺了躺酒。但是好像還有點(diǎn)問題,平時(shí)我們使用ScrollView的時(shí)候用力劃一下蔑歌,可以看到手指離開了屏幕羹应,但是內(nèi)容還可以繼續(xù)滾動(dòng)。而目前我們自定義的這個(gè)view只能通過手指滑動(dòng)次屠,如果手指離開屏幕就不能滑動(dòng)了园匹。這樣的體驗(yàn)顯然不夠好雳刺,我們來實(shí)現(xiàn)這個(gè)慣性滑動(dòng)的效果吧!

實(shí)現(xiàn)慣性滑動(dòng)

要實(shí)現(xiàn)慣性滑動(dòng)偎肃,我們需要用到兩個(gè)類:VelocityTracker煞烫,OverScroller。
VelocityTracker簡介
view滑動(dòng)助手類OverScroller

 private int minFlingVelocity;//最小慣性滑動(dòng)速度
    private VelocityTracker velocityTracker;
    private OverScroller scroller;
    private int lastFling;//慣性滑動(dòng)上一個(gè)位置

    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:
     //處理慣性滑動(dòng)
    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;
//已經(jīng)在邊界了累颂,不再處理慣性
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;//重置上一次值滞详,避免第二次慣性滑動(dòng)計(jì)算錯(cuò)誤的dx
        }
    }

velocityTracker.computeCurrentVelocity方法的第二個(gè)參數(shù)表示最大慣性速度,這里我設(shè)置8000紊馏,避免刻度尺過快的滑動(dòng)料饥。通過調(diào)用scroller.fling方法將計(jì)算出的速度交給scroller,然后在computeScroll方法中獲取當(dāng)前值朱监,并與上一次的值做差算出變化量dx岸啡,同樣用這個(gè)dx變化offset刷新界面實(shí)現(xiàn)滑動(dòng)效果。

不可選區(qū)域

刻度尺完成了赫编,接下來是不可選的灰色區(qū)域巡蘸。我采用兩個(gè)int值表示在刻度尺的區(qū)域,刻度尺的每個(gè)刻度表示一個(gè)最小單位擂送,前一個(gè)int表示在刻度尺的起始位置悦荒,后一個(gè)int表示占據(jù)的刻度數(shù)量。

  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() {
        //避免重復(fù)生成
        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();
    }

我用一個(gè)list存放設(shè)置的不可選區(qū)域嘹吨,然后在另一個(gè)list中存放轉(zhuǎn)換成RectF的位置信息搬味。這里的RectF是在相對于整體刻度尺而言的,因此繪制到屏幕的時(shí)候需要減去offset蟀拷,并且需要考慮只有部分在屏幕可見的情況碰纬。避免在onDraw方法中創(chuàng)建過多臨時(shí)變量,我聲明一個(gè)成員變量tempRect问芬,用來保存繪制時(shí)的臨時(shí)參數(shù)悦析。

可選區(qū)域

完成了不可選區(qū)域,可選區(qū)域也是同樣的此衅。由于只能有一個(gè)可選區(qū)域强戴,我們只需要定義一個(gè)RectF。額外需要考慮與不可選區(qū)域相交時(shí)會變色炕柔,我定了一個(gè)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);//擴(kuò)展圓的半徑
    private float extendTouchRate = 1.5f;//擴(kuò)展觸摸區(qū)域與視圖的比率(>=1)

    private boolean overlapping;//是否覆蓋unselectable
    private RectF selectedRectF;//選擇區(qū)域位置
    private RectF extendPointRectF;//擴(kuò)展點(diǎn)位置

    @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) {
//擴(kuò)展圓邊框
mPaint.setColor(overlapping ? overlappingStrokeColor : selectedStrokeColor);
canvas.drawCircle(tempRect.right, tempRect.centerY(), extendRadius, mPaint);
//擴(kuò)展圓填充
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(tempRect.right, tempRect.centerY(), extendRadius, mPaint);
//擴(kuò)展圓的位置信息匕累,處理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;
    }

點(diǎn)擊,移動(dòng)默伍,擴(kuò)展

通過前面的分析欢嘿,我們知道這個(gè)view中的事件有很多種:點(diǎn)擊衰琐,移動(dòng)刻度尺,移動(dòng)選中區(qū)域炼蹦,擴(kuò)展選中區(qū)域羡宙。我們定義這四種類型便于后續(xù)的事件處理:

   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;//是否正在聯(lián)動(dòng)
    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) {
        //如果正在聯(lián)動(dòng)時(shí),避免手指抖動(dòng)造成不必要停止
        if (linking &amp;&amp; Math.abs(dx) < touchSlop) {
return;
        }
        float right = selectedRectF.right += dx;
        //下層聯(lián)動(dòng)
        Message message = null;
        if (dx > 0 &amp;&amp; width - (right - offset) < boundary //選中區(qū)域滑到屏幕右邊
    &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) {
        //如果正在聯(lián)動(dòng)時(shí)掐隐,避免手指抖動(dòng)造成不必要停止
        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)) {//選中區(qū)域滑到屏幕左邊并繼續(xù)向左滑動(dòng)
message = handler.obtainMessage(MESSAGE_MOVE, -linkDx);
        } else if (dx > 0 &amp;&amp; width - (right - offset) < boundary &amp;&amp; offset < maxOffset) {//選中區(qū)域滑到屏幕右邊并且繼續(xù)向右滑動(dòng)
message = handler.obtainMessage(MESSAGE_MOVE, linkDx);
        }
        Timber.e("message:" + message);
        if (message != null) {//處在兩邊界狗热,需要聯(lián)動(dòng)
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) {
//處理慣性滑動(dòng)
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);
    }

    /**
     * 重置選擇區(qū)域的位置
     *
     * @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;
    }

    /**
     * 重置選擇區(qū)域的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;
    }

    /**
     * 將選擇內(nèi)容轉(zhuǎn)換成區(qū)域
     *
     * @param start 開始位置
     * @param count 數(shù)量
     */
    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;//整體向左移動(dòng)
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();
    }

    /**
     * 將選中區(qū)域轉(zhuǎn)換成選擇內(nèi)容
     *
     * @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時(shí)as提示你需要重寫的方法,因?yàn)槟憧赡軟]有考慮到如果給這個(gè)view設(shè)置OnClickListener的情況虑省。如果你沒有在onTouchEvent中調(diào)用performClick匿刮,那么setOnClickListener方法就失效了。

你可能注意到這一次比較復(fù)雜探颈,并且還有一個(gè)linking字段熟丸,表示是否正在聯(lián)動(dòng),我解釋一下這個(gè)聯(lián)動(dòng)的概念:通過gif其實(shí)你可能注意到伪节,當(dāng)我移動(dòng)或者擴(kuò)展選中區(qū)域的時(shí)候光羞,如果移動(dòng)到了屏幕的邊界,后面的刻度尺就會跟著移動(dòng)怀大,實(shí)際上這個(gè)時(shí)候選中區(qū)域在屏幕中的位置沒有改變纱兑,只是刻度尺移動(dòng)了。一開始我也是通過dx來改變offset叉寂,但是存在一個(gè)問題萍启,移動(dòng)到屏幕邊緣之后,手指可以移動(dòng)的區(qū)域已經(jīng)很小了屏鳍,不會產(chǎn)生足夠的dx(手指不移動(dòng)的話勘纯,不會有新的touch事件產(chǎn)生)。最好的體驗(yàn)是我把手機(jī)移動(dòng)到屏幕邊緣钓瞭,刻度尺就會自己按照一定的速率移動(dòng)直到最大offset或者最小offset驳遵。于是我使用了Handler,當(dāng)滿足條件后發(fā)送消息山涡,表示開始進(jìn)行聯(lián)動(dòng)堤结,會按照固定速度產(chǎn)生一個(gè)dx改變offset。當(dāng)然鸭丛,在離開屏幕邊緣的時(shí)候還需要及時(shí)取消handler的任務(wù)竞穷。

至此,功能基本已經(jīng)實(shí)現(xiàn)了鳞溉,運(yùn)行一下看看效果吧~

后面需要做什么那瘾带?現(xiàn)在這個(gè)view只能自己玩,我需要它與其他view有交互熟菲,比如選中什么區(qū)域看政,狀態(tài)的改變生么的朴恳。

狀態(tài)變化

聲明兩個(gè)接口,并在適當(dāng)時(shí)候回調(diào)它們的方法允蚣,這樣外部就能感知view的狀態(tài)變化于颖。

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

    public interface SelectChangeListener {
        void onSelected();

        void onSelectChanged(int start, int count);
    }

完善

后面的話就是根據(jù)業(yè)務(wù)添加一些api了,例如添加不可選區(qū)域嚷兔,改變刻度范圍什么森渐,一切都看需求了。

想學(xué)習(xí)更多Android知識冒晰,或者獲取相關(guān)資料請加入Android開發(fā)交流群:1018342383章母。 有面試資源系統(tǒng)整理分享,Java語言進(jìn)階和Kotlin語言與Android相關(guān)技術(shù)內(nèi)核翩剪,APP開發(fā)框架知識乳怎, 360°Android App全方位性能優(yōu)化。Android前沿技術(shù)前弯,高級UI蚪缀、Gradle、RxJava恕出、小程序询枚、Hybrid、 移動(dòng)架構(gòu)師專題項(xiàng)目實(shí)戰(zhàn)環(huán)節(jié)浙巫、React Native金蜀、等技術(shù)教程!架構(gòu)師課程的畴、NDK模塊開發(fā)渊抄、 Flutter等全方面的 Android高級實(shí)踐技術(shù)講解。還有在線答疑

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丧裁,一起剝皮案震驚了整個(gè)濱河市护桦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌煎娇,老刑警劉巖二庵,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缓呛,居然都是意外死亡催享,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門哟绊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來因妙,“玉大人,你說我怎么就攤上這事±计龋” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵炬称,是天一觀的道長汁果。 經(jīng)常有香客問我,道長玲躯,這世上最難降的妖魔是什么据德? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮跷车,結(jié)果婚禮上棘利,老公的妹妹穿的比我還像新娘。我一直安慰自己朽缴,他們只是感情好善玫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著密强,像睡著了一般茅郎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上或渤,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天系冗,我揣著相機(jī)與錄音,去河邊找鬼薪鹦。 笑死掌敬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的池磁。 我是一名探鬼主播奔害,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼地熄!你這毒婦竟也來了舀武?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤离斩,失蹤者是張志新(化名)和其女友劉穎银舱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跛梗,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寻馏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了核偿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诚欠。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轰绵,到底是詐尸還是另有隱情粉寞,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布左腔,位于F島的核電站唧垦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏液样。R本人自食惡果不足惜振亮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鞭莽。 院中可真熱鬧坊秸,春花似錦、人聲如沸澎怒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喷面。三九已至站超,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乖酬,已是汗流浹背死相。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咬像,地道東北人算撮。 一個(gè)月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像县昂,于是被迫代替她去往敵國和親肮柜。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 最近擼了一個(gè)自定義view倒彰,還是比較復(fù)雜的审洞,感覺有必要分享下實(shí)現(xiàn)的過程。 效果 先來看下效果吧: 我們來分析這個(gè)v...
    胡奚冰閱讀 1,245評論 0 5
  • 一 基礎(chǔ): 自定義View實(shí)現(xiàn)跟隨手指滾動(dòng)的刻度尺待讳,實(shí)現(xiàn)了類似SeekBar的滑動(dòng)選中效果芒澜。項(xiàng)目地址,歡迎star...
    _那個(gè)人閱讀 3,563評論 0 2
  • 一滑動(dòng)效果的產(chǎn)生 滑動(dòng)一個(gè)View创淡,本質(zhì)區(qū)別就是移動(dòng)一個(gè)View痴晦。改變當(dāng)前View所在的坐標(biāo),原理和動(dòng)畫相似不斷改...
    猿萬閱讀 9,848評論 0 14
  • 易經(jīng)起源于河南安陽琳彩,市區(qū)內(nèi)有一座以太極湖誊酌、64卦石等易經(jīng)元素為主的主題公園部凑,稱之為易園。 易園位于寒舍與公干之地之...
    太行客閱讀 369評論 3 2