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

来源:互联网 发布:lol网络正常ping高 编辑:程序博客网 时间:2024/04/30 00:17

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:

<?xml version="1.0" encoding="utf-8"?><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>

实现效果:
这里写图片描述

上面就是实现了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<String> strs = new ArrayList<>();        for (int i = 0; i <= 100; i++) {            strs.add("数据");        }        ArrayAdapter<String> mAdapter = new ArrayAdapter<String>(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) {    }}

最后看效果:
这里写图片描述

4、总结

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

附上Demo

0 0
原创粉丝点击