Android自定義View之區塊選擇器

最近擼了一個自定義view,仍是比較複雜的,感受有必要分享下實現的過程。java

效果

先來看下效果吧:git

咱們來分析這個view須要實現哪些效果。github

  • 首先它有一個刻度尺表明了時間段(也能夠是別的什麼),而且能夠看到完整的刻度尺是比屏幕寬度大的,所以確定須要能夠左右滑動。
  • 其次,能夠有不可選的區域(gif中灰色塊)和選中的區域(gif中藍色塊),點擊刻度的空白位置出現或者移動選中區域到點擊位置。
  • 點擊並拖動選中的區域能夠移動,當移動到屏幕兩邊的時候,下層的刻度也能跟着移動。
  • 還能夠點擊並拖動選中區域右邊的白色小圓改變選中區域的大小,一樣到達屏幕邊界時下層刻度跟着移動。
  • 當選中區域與不可選區域重疊時,選中區域變色。
  • 選中區域最小爲1個刻度,當移動後手指擡起時,選中區域貼合刻度。
  • 最後還須要監聽一些狀態的變化,如是否重疊,選中區域改變的位置。

實現

刻度尺

別懼怕有這麼多的功能,咱們一個一個來實現。首先是刻度尺,這個簡單。因爲完整的刻度尺是比屏幕寬度大的,所以咱們先來了解幾個概念:面試

這裏手機屏幕的寬度是width,刻度尺的寬度的時maxWidth,咱們其實只須要繪製手機屏幕可見的部分就能夠了,這裏的offset表示手機屏幕的左邊與刻度尺左邊的偏移量。canvas

瞭解了這個概念,咱們就來開始寫吧,定義一個View,處理下構造都指向3個參數的那個,而後統一作初始化:api

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事件產生移動纔有效果。app

實現滑動刻度尺

咱們重寫onTouchEvent來實現滑動效果:ide

   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了,例如添加不可選區域,改變刻度範圍什麼,一切都看需求了。

源碼

最後附上代碼:
https://github.com/huburt-Hu/...

更多資料分享歡迎Android工程師朋友們加入安卓開發技術進階互助:856328774免費提供安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於Android面試的題目彙總。
相關文章
相關標籤/搜索