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
- Android 打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)
- Android ScrollView滚动条控件,滚动到底部或顶部
- 修改Scrollview嵌套其他可滚动控件 如listview或者recycleView等自动滚动的问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- android解决同一个界面上ScrollView和百度地图(ListView等可滚动控件)滚动冲突问题
- Android自定义控件:小米应用市场Banner轮播、可拉伸回弹的ListView与ScrollView
- Android ScrollView 上推至顶部使控件悬浮
- Android ScrollView中控件顶部悬浮
- Android支持横行滚动的ListView控件
- Android支持横行滚动的ListView控件
- Android支持横行滚动的ListView控件
- Android支持横行滚动的ListView控件
- Android支持横行滚动的ListView控件
- Android支持横行滚动的ListView控件
- java的Switch用法简介
- JQ 加在文字后边没事 但是加在链接文件里边就不出现效果
- 安卓图片加载之使用universalimageloader加载圆形圆角图片
- 错误处理与异常抛出_Swift基础知识学习
- inotify
- Android 打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)
- 安卓使用cookie注意事项
- 28-题目1108:堆栈的使用
- CSS布局模型
- Redis客户端Java服务接口封装
- mongodb数据库DB入门知识
- Java集合
- 谈谈UIView的几个layout方法-layoutSubviews、layoutIfNeeded、setNeedsLayout
- php批量去除bom的代码