Android 打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)

1、序言

现在很多App为了让一个页面可以有更多展示的东西。于是乎有一个界面就有几个tab进行切换页面,同时滚动的时候为了方便用户切换tab,这时tab需要悬浮在布局的顶部。所以这样就有了这篇blog咯…….

2、实现原理

控件的实现原理,相对来还是比较简单的:
1、首先自定义一个GroupView,实现滑动的效果,同时进行一些判断,比如:当满足一些条件时,把事件处理交给ChildView来处理;当ChildView满足一些条件时(比如ListView滚动到了第一条数据,ScrollView滚动到了顶部),让GroupView滚动,ChildView停止滚动。
2、然后自定义一个ChildView,这个可以是ListView、ScrollView等等可滚动的控件,重写onTouchEvent方法,进行判断查看是否可以滚动,因为是否可以滚动是由GroupView来控制的。
3、通过接口的方式把两者之间判断是否可以滚动联系起来。

3、实现代码

看逻辑不清楚可以跳过直接看代码:
首先是GroupView的代码:

public class SideGroupLayout extends ViewGroup {
    public static final String TAG = "android_xw";

    private int mTouchSlop;
    private float mLastMotionX;
    private float mLastMotionY;
    private boolean mIsBeingDragged;
    protected int mFirstItemHeight;
    private int mScrollY;
    public boolean mScrollToEnd;

    private VelocityTracker mVelocityTracker;
    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;

    private Scroller mScroller;
    private boolean mCanScroller;

    public SideGroupLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mCanScroller = true;
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();

        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();

        mScroller = new Scroller(context);
        reset();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = 0;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                height += child.getMeasuredHeight();
            }
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int height = 0;
        mFirstItemHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != View.GONE) {
                view.layout(0, height, getWidth(), height + view.getMeasuredHeight());
                height += view.getMeasuredHeight();
                if (i == 0) {
                    mFirstItemHeight = height;
                }
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onInterceptTouchEvent(ev);
        }

        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int xDiff = (int) Math.abs(x - mLastMotionX);
            final int yDiff = (int) Math.abs(y - mLastMotionY);
            // Log.i("TAG", "mScrollY == mFirstItemHeight:" + (mScrollY ==
            // mFirstItemHeight));
            if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;
            } else if (yDiff > mTouchSlop * 2 && yDiff >= xDiff) {
                mIsBeingDragged = true;
                mLastMotionY = y;
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            mLastMotionX = ev.getX();
            mLastMotionY = ev.getY();
            mIsBeingDragged = false;
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            break;
        }
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onTouchEvent(event);
        }
        addVelocityTracker(event);

        final int action = event.getAction();
        final float y = event.getY();
        final float x = event.getX();

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 获取相对屏幕的坐标,即以屏幕左上角为原点
            break;
        case MotionEvent.ACTION_MOVE:
            final float scrollX = mLastMotionX - x;
            final float scrollY = mLastMotionY - y;
            onScroll((int) scrollX, (int) scrollY);
            scrollTo(0, mScrollY);
            mLastMotionX = x;
            mLastMotionY = y;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final float velocityX = mVelocityTracker.getXVelocity();
            final float velocityY = mVelocityTracker.getYVelocity();
            if (Math.abs(velocityY) > mMinimumFlingVelocity * 3 && Math.abs(velocityY) > Math.abs(velocityX)) {
                onFling(velocityX, velocityY);
            }
            cancel();
            break;
        }
        return true;
    }

    private void onScroll(int scrollX, int scrollY) {
        if (scrollY > 0) {
            if (mScrollY == mFirstItemHeight)
                return;
            if (mScrollY + scrollY >= mFirstItemHeight) {
                mScrollY = mFirstItemHeight;
            } else {
                mScrollY = mScrollY + scrollY;
            }
        } else if (scrollY < 0) {
            if (mScrollY > 0) {
                scrollY = Math.abs(scrollY);
                if (mScrollY - scrollY <= 0) {
                    mScrollY = 0;
                } else {
                    mScrollY = mScrollY - scrollY;
                }
            }
        }
        mScrollToEnd = mScrollY == mFirstItemHeight;
    }

    private void onFling(float velocityX, float velocityY) {
        int dy = 0;
        if (velocityY > 0) {
            dy = -mScrollY;
        } else {
            dy = (int) (mFirstItemHeight - getScrollY());
        }

        float ratio = getRatio(Math.abs(velocityY));
        dy = (int) (dy * ratio);
        onScroll(0, dy);
        if (mFirstItemHeight > 0) {
            mScroller.startScroll(0, getScrollY(), 0, dy, 500 * Math.abs(dy) / mFirstItemHeight);
        }
        postInvalidate();
    }

    protected float getRatio(float velocityY) {
        return 1;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            onScrollChanged(getScrollX(), getScrollY(), 0, 0);
            postInvalidate();
        }
    }

    private void addVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null)
            mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(event);
    }

    private void cancel() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        mIsBeingDragged = false;
    }

    private void reset() {
        mScrollToEnd = false;
    }

    public boolean isScrollToEnd() {
        return mScrollToEnd;
    }

    private OnGroupScrollListener mAction;

    public void setOnGroupScrollListener(OnGroupScrollListener action) {
        this.mAction = action;
    }


    public void setCanScroller(boolean canScroller) {
        this.mCanScroller = canScroller;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mAction != null) {
            mAction.onScrollChanged(l, t);
        }
    }

    public void onActivityDestory() {
        reset();
        mScroller = null;
        mScrollToEnd = false;
    }

}

代码还是不复杂的,玩过自定义控件都知道怎么回事,根据这个需求重点说下几个方法:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int height = 0;
        mFirstItemHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != View.GONE) {
                view.layout(0, height, getWidth(), height + view.getMeasuredHeight());
                height += view.getMeasuredHeight();
                if (i == 0) {
                    mFirstItemHeight = height;
                }
            }
        }
    }



    private void onScroll(int scrollX, int scrollY) {
        if (scrollY > 0) {
            if (mScrollY == mFirstItemHeight)
                return;
            if (mScrollY + scrollY >= mFirstItemHeight) {
                mScrollY = mFirstItemHeight;
            } else {
                mScrollY = mScrollY + scrollY;
            }
        } else if (scrollY < 0) {
            if (mScrollY > 0) {
                scrollY = Math.abs(scrollY);
                if (mScrollY - scrollY <= 0) {
                    mScrollY = 0;
                } else {
                    mScrollY = mScrollY - scrollY;
                }
            }
        }
        mScrollToEnd = mScrollY == mFirstItemHeight;
    }


重点看下if(i == 0)时会执行的代码,mFirstItemHeight 这是获取第一个ChildView的高度,然后可以在onScroll()方法里面一个赋值:mScrollToEnd = mScrollY == mFirstItemHeight; ChildView就是通过这个参数mScrollToEnd,来判断是否要进行滚动。可以看到当我们滚动的Y轴的距离等于第一控件的高度,这时会把mScrollToEnd复制为true,这个时候事件就会被ChildView给消化掉,这个时候滚动的时候,就是滚动ChildView了。

再看下事件拦截方法里面:


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onInterceptTouchEvent(ev);
        }

        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int xDiff = (int) Math.abs(x - mLastMotionX);
            final int yDiff = (int) Math.abs(y - mLastMotionY);
            // Log.i("TAG", "mScrollY == mFirstItemHeight:" + (mScrollY ==
            // mFirstItemHeight));
            if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;
            } else if (yDiff > mTouchSlop * 2 && yDiff >= xDiff) {
                mIsBeingDragged = true;
                mLastMotionY = y;
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            mLastMotionX = ev.getX();
            mLastMotionY = ev.getY();
            mIsBeingDragged = false;
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            break;
        }
        return mIsBeingDragged;
    }

我们主要看这句代码:

if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;

当父控件滑动的距离等于第一个ChildView高度时,会做一个判断:当是向上滑动,并且滚动的ChidlView让GroupView滚动时,会把事件拦截下来,交给GroupView来进行处理,所以这时就是GroupView进行滚动,而滚动的ChildView就会停止滚动。

GroupView其他的代码稍作讲解:

onMeasure()里面对所有ChildView进行一个高度的计算,然后才能得知GroupView的高度;
onLayout()里面对ChildView进行位置的确认;
onInterceptTouchEvent()已经说过了,跳过;
onTouchEvent()是进行事件处理,因为是集成的GroupView,不能自己滚动,所以我们要利用Scroller来实现一个类似于ScrollView滚动的效果,写过这种控件相信都明白的。
其他的一些方法都是为实现滚动而写的一些方法。

GroupView的实现比较复杂一些,相对来说ChildView的实现就非常简单了:
来看一个可以嵌套这个SideGroupLayout的ScrollView:


public class SideTopScrollView extends ScrollView {

    private OnChildScrollListener mAction;

    private boolean isScrollTop = false;

    public SideTopScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean bool = mAction != null && mAction.isChildScroll() && super.onInterceptTouchEvent(ev);
        return bool;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mAction != null && mAction.isChildScroll() && super.onTouchEvent(event);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        if (t == 0) {
            isScrollTop = true;
        } else {
            isScrollTop = false;
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    public boolean isScrollToTop() {
        return isScrollTop;
    }

    public void setOnChildScrollListener(OnChildScrollListener action) {
        this.mAction = action;
    }


}

可以看到逻辑是非常简单的:
1、重写onTouchEvent()方法,问一下SideGroupLayout,我是不是可以滚动了。
2、重写onScrollChanged()方法,告诉SideGroupLayout,你是不是可以滚动了。

主要的代码都在上面,下面我们看使用方式:

public class ScrollViewActivity extends Activity implements OnGroupScrollListener, OnChildScrollListener {

    private SideGroupLayout mHoverLayout;
    private SideTopScrollView mSideTopScrollView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_scrollview);
        initView();
    }

    private void initView() {
        mHoverLayout = (SideGroupLayout) findViewById(R.id.hoverlayout);
        mSideTopScrollView = (SideTopScrollView) findViewById(R.id.sidescrollview);
        mHoverLayout.setOnGroupScrollListener(this);
        mSideTopScrollView.setOnChildScrollListener(this);
    }

    @Override
    public boolean isChildScroll() {
        return mHoverLayout != null && mHoverLayout.isScrollToEnd();
    }

    @Override
    public boolean isGroupScroll() {
        return mSideTopScrollView != null && mSideTopScrollView.isScrollToTop();
    }

    @Override
    public void onScrollChanged(int left, int top) {
    }

}

layout_scrollview:


<widget.SideGroupLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/hoverlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="可滚动的区域" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@android:color/black"
        android:gravity="center"
        android:text="停留的位置"
        android:textColor="@android:color/white" />

    <widget.SideTopScrollView
        android:id="@+id/sidescrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:text="内容" />
        LinearLayout>
    widget.SideTopScrollView>

widget.SideGroupLayout>

实现效果:
Android 打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)_第1张图片

上面就是实现了ScrollView效果的顶部停留了。
下面把ListView的实现方式,其实跟ScrollView的效果差不多。

代码:

public class SideTopListView extends ListView  {

    private OnChildScrollListener mAction;

    public SideTopListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return mAction != null && mAction.isChildScroll() && super.onTouchEvent(ev);
    }

    public void setOnChildScrollListener(OnChildScrollListener action) {
        this.mAction = action;
    }

    /**
     * 判断是否滑动到了第一条数据
     */
    public boolean isChildScrollToEnd() {
        if (getFirstVisiblePosition() == 0) {
            View view = getChildAt(0);
            if (view != null) {
                return view.getTop() == getPaddingTop();
            } else {
                return true;
            }
        }
        return false;
    }



}

isChildScrollToEnd()方法是用来判断是否滑动到第一条数据
具体实现:

public class ListViewActivity extends Activity implements OnChildScrollListener, OnGroupScrollListener {

    private SideGroupLayout mHoverLayout;
    private SideTopListView mSideTopListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        initView();
    }

    private void initView() {
        mHoverLayout = (SideGroupLayout) findViewById(R.id.hoverlayout);
        mSideTopListView = (SideTopListView) findViewById(R.id.listview);
        mHoverLayout.setOnGroupScrollListener(this);
        mSideTopListView.setOnChildScrollListener(this);
        List strs = new ArrayList<>();
        for (int i = 0; i <= 100; i++) {
            strs.add("数据");
        }
        ArrayAdapter mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, strs);
        mSideTopListView.setAdapter(mAdapter);
    }


    @Override
    public boolean isChildScroll() {
        return mHoverLayout != null && mHoverLayout.isScrollToEnd();
    }

    @Override
    public boolean isGroupScroll() {
        return mSideTopListView != null && mSideTopListView.isChildScrollToEnd();
    }


    @Override
    public void onScrollChanged(int left, int top) {
    }

}

最后看效果:
Android 打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)_第2张图片

4、总结

其实整个的实现不难,就是一个事件处理过程,当ChildView不需要滑动时,就给GroupView来滑动,当ChildView需要滑动时,就给ChildView来滑动,通过接口的方式来进行链接。

附上Demo

你可能感兴趣的:(android,自定义View)