自定義佈局: HorizontalScrollView——支持橫向滑動

這是一個支持橫向滑動,並處理了滑動衝突的自定義ViewGroup。幾乎涵蓋了自定義viewGroup的所有知識,對於理解View的相關知識有一定的幫助,是一個不錯的實戰Demo。以下爲功能,所做的處理及對應的知識點。

1.支持橫向滑動

   爲了使佈局能夠橫向滑動,需要重寫onTouchEvent()方法,在這個方法中判斷是否爲橫向滑動,如果是的話就使用scrollBy()方法讓佈局內容滑動。當用戶快速滑動時,使用Tracker判斷速度是否爲橫向滑動,如果是的話使用Scroller使佈局內容平滑滑動。具體判斷方法如下代碼。(當然需先判斷是否攔截事件)。

//滑動事件處理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        tracker.addMovement(event);
        int x = (int)event.getX();
        int y = (int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!scroller.isFinished()){
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x-lastX;
                int deltaY = y-lastY;
                //每次進行滑動限制
                scrollBy(-scrollLimit(deltaX),0);
                break;
            case MotionEvent.ACTION_UP:
                int dx=0;
                //處理快速滑動
                tracker.computeCurrentVelocity(1000);
                float xVelocity = tracker.getXVelocity();
                if(Math.abs(xVelocity)>=50){
                       dx = 0-scrollLimit((int)xVelocity);
                }
                //使用Scroller
                smoothScrollBy(dx,0);
                tracker.clear();
                break;
        }
        lastX = x;
        lastY = y;
        return true;
    }

  關於scrollBy()方面的知識,可參考以下文章:更好地理解 scrollBy() / scrollTo()

  

2.處理滑動衝突

   當佈局的子view爲scrollView或ListView等可以滑動的view時,就需要進行滑動衝突處理,否則最終效果可能與你的目的不同。在這裏只處理子view支持縱向滑動與佈局之間的滑動衝突。簡單來說,就是當爲橫向滑動時,佈局攔截事件,否則傳給子view。 

//處理滑動衝突,判斷是否攔截事件
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Boolean intercept = false;
        int x = (int)ev.getX();
        int y = (int)ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                if(!scroller.isFinished()){
                    scroller.abortAnimation();
                    intercept =true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x-lastX;
                int deltaY = y-lastY;
                //橫向滑動
                if(Math.abs(deltaX)>Math.abs(deltaY)){
                    intercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:
                break;
        }
        //記錄上次事件座標
        lastX = x;
        lastY = y;
        return intercept;

    }

 

 3.支持wrap_content屬性

     如果不進行處理的話,我們的自定義佈局設置wrap_content屬性跟match_parent屬性效果一樣。具體原因跟view的測量過程有關。可閱讀文章進行了解:Android:爲什麼你的自定義View wrap_content不起作用?

  這裏附上處理代碼:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        int childCount = getChildCount();
        maxHeight=0;
        maxWidth=0;
        for(int i = 0;i<childCount;i++){
            View v = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
            //記錄子view信息,方便佈局
            setLocation(v,lp);
        }
        maxHeight += getPaddingBottom()+getPaddingTop();
        maxWidth  += getPaddingRight();
        //使wrap_content屬性起作用
        if(getLayoutParams().width == LayoutParams.WRAP_CONTENT && getLayoutParams().height == LayoutParams.WRAP_CONTENT){
            int mWidth = Math.min(maxWidth,widthSize);
            setMeasuredDimension(mWidth,maxHeight);
        }else if(getLayoutParams().height == LayoutParams.WRAP_CONTENT){
            setMeasuredDimension(widthSize,maxHeight);
        }else if(getLayoutParams().width == LayoutParams.WRAP_CONTENT){
            int mWidth = Math.min(maxWidth,widthSize);
            setMeasuredDimension(mWidth,heightSize);
        }else {
            setMeasuredDimension(widthSize,heightSize);
        }
    }

 4.滑動範圍限制

   爲了更好的體驗,我們需對滑動範圍進行限制,不然的話會無限滑動,看到的是一片空白。這裏限制的範圍是佈局子view的總寬度,即滑到子view邊緣就不再滑動了。這裏比較容易搞錯正負值,需要注意。代碼如下:

 //限制滑動距離
    private int scrollLimit(int delta){
        //子view總長度小於佈局寬度,禁止滑動
        if(maxWidth-getWidth()<=0){
            return 0;
        }else {
            if (delta <= 0) {
                //左滑
                if (getScrollX() == maxWidth - getWidth()) {
                    //處於最右邊,右邊緣可見 ,禁止繼續左滑
                    return 0;
                }else{
                    //限制滑動距離,使左滑不超過子view內容的右邊緣
                    int dx = Math.min(maxWidth - getWidth() - getScrollX(), Math.abs(delta));
                    return 0 - dx;
                }

            } else {
                //右滑
                if (getScrollX() == 0) {
                    //處於開始狀態,左邊緣可見,禁止繼續右滑
                    return 0;
                }else{
                    //限制滑動距離,使右滑不超過子view內容的左邊緣
                    return Math.min(Math.abs(getScrollX()),delta);
                }
            }

        }
    }

5.處理margin/padding

  爲了使佈局支持padding及子view間的margin,在進行佈局時需將此考慮在內。當我們在記錄子view位置信息時就將此考慮在內,代碼如下。(其中ViewLocation爲記錄子view位置信息所創建的類)

//保存各view的位置參數(處理margin)
    private void setLocation(View v,MarginLayoutParams lp){
        ViewLocation mLocation = new ViewLocation();
        mLocation.setLeft(left+lp.leftMargin);
        mLocation.setRight(mLocation.getLeft()+v.getMeasuredWidth());
        mLocation.setTop(getPaddingTop()+lp.topMargin);
        mLocation.setBottom(mLocation.getTop()+v.getMeasuredHeight());
        maxWidth += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
        left += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
        maxHeight = (mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin)>=maxHeight?mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin:maxHeight;
        viewLocationList.add(mLocation);
    }

  注意我們獲取子view的margin屬性的方法是先通過獲取它的MarginLayoutParams(上圖方法第2個參數)。

 MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();

  此時我們需要重寫 generateLayoutParams()方法,否則會報錯,類型轉換錯誤,因爲這個方法默認返回空值。更多相關的內容可閱讀文章:你的自定義View是否真的支持Margin

//爲獲取子view margin屬性,重寫方法(否則會報錯)
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

 

完整代碼見GitHub:https://github.com/YangRT/HorizontalScrollView

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章