现在很多App为了让一个页面可以有更多展示的东西。于是乎有一个界面就有几个tab进行切换页面,同时滚动的时候为了方便用户切换tab,这时tab需要悬浮在布局的顶部。所以这样就有了这篇blog咯…….
控件的实现原理,相对来还是比较简单的:
1、首先自定义一个GroupView,实现滑动的效果,同时进行一些判断,比如:当满足一些条件时,把事件处理交给ChildView来处理;当ChildView满足一些条件时(比如ListView滚动到了第一条数据,ScrollView滚动到了顶部),让GroupView滚动,ChildView停止滚动。
2、然后自定义一个ChildView,这个可以是ListView、ScrollView等等可滚动的控件,重写onTouchEvent方法,进行判断查看是否可以滚动,因为是否可以滚动是由GroupView来控制的。
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>
上面就是实现了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) {
}
}
其实整个的实现不难,就是一个事件处理过程,当ChildView不需要滑动时,就给GroupView来滑动,当ChildView需要滑动时,就给ChildView来滑动,通过接口的方式来进行链接。
附上Demo