您的位置:首页 > 移动开发 > Android开发

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

2016-03-05 14:43 495 查看

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息