自定义布局: 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

 

 

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