最近在项目开发中经常遇到这样的需求:在一个Activity中,上半部分是一个高度比较大(屏幕高度的1/3或者1/2)的Header,下半部分是ListView或者是ViewPager(里面依然包含ListView)。如果Header不能向上活动,则在浏览ListView中的内容时,只能在很小的高度里面滑动,不能发挥大屏幕的优势。如果在滑动过程中,使Header也滑动则会造成外层ViewGroup与内层ListView滑动冲突。
本文从滑动冲突产生的原因、解决方案以及代码实现过程三个方面,介绍了可滑动Header控件的实现过程。
1.滑动冲突产生的原因
滑动冲突的原因很简单,就是一个原因:在界面中只要内外两层同时可以滑动,就会产生滑动冲突。常见的滑动冲突场景有两种:
a.外部View滑动方向与内部View滑动方向一致;
b.外部View滑动方向与内部View滑动方向不一致。
Android系统提供的可滑动控件有的已经解决了滑动冲突问题,例如:ViewPager;而大部分可滑动控件并没有处理滑动冲突,例如:ScrollView、ListView。
2.解决方案
要想解决滑动冲突,首先要对Android的事件传递机制有一个深入的理解,本文的重点是讲述解决滑动冲突,对Android的事件传递机制就不展开了。Android的MotionEvent是从外层View向内层View传递的,一旦MotionEvent被拦截,MotionEvent就交给拦截它的View处理。针对滑动冲突问题,外层滑动View在onInterceptTouchEvent()方法中根据业务逻辑的需要判断是否拦截MotionEvent,在onTouchEvent()方法中实现滑动效果;在滑动的过程中,使内外层View只有一个能拦截MotionEvent,这样子就解决了View的滑动冲突问题。
3.实现过程
外层滑动View的实现`
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;
import java.util.NoSuchElementException;
public class SlideHeaderView extends LinearLayout{
private static final String TAG = "SlideHeaderView";
private static final boolean DEBUG = true;
/**
* 滑动的风格类型,非覆盖
*/
private StyleType mStyleType = StyleType.SlideOut;
/**
* 覆盖式、非覆盖式枚举
*/
public enum StyleType {
SlideOut, SlideIn // SlideOut:整个View滑动,整个View的位置发生改变 SlideIn:整个View的位置没有改变,改变View内HeaderView的高度
}
public interface OnInterceptScrollListener {
boolean isInterceptScrollUpward(MotionEvent event);
boolean isInterceptScrollDownward(MotionEvent event);
}
public interface OnChangeHeaderStateListener {
void onExpandHeader();
void onCollapseHeader();
}
private View mHeader;
private View mContent;
private OnInterceptScrollListener mInterceptScrollListener;
private OnChangeHeaderStateListener mChangeHeaderStateListener;
// header的高度 单位:px
private int mOriginalHeaderHeight = -1;
private int mHeaderHeight;
private int minHeaderHeight;
public int mStatus = STATUS_EXPANDED;
public static final int STATUS_EXPANDED = 1;
public static final int STATUS_COLLAPSED = 2;
public int mAutoScrollOrientation;
private final int mSlideDownward = 1; //向下滑动
private final int mSlideUpward = 2; //向上滑动
private int mTouchSlop;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
// 用来控制滑动角度,仅当角度a满足如下条件才进行滑动:tan a = deltaX / deltaY > 2
private static final int TAN = 2;
private boolean mIsSticky = true;
private boolean mInitDataSucceed = false;
private boolean mDisallowInterceptTouchEventOnHeader = true;
private MarginLayoutParams mTopMarginLayoutParams;
private int mTopMargin;
private int maxTopMargin = -1;
private int mAnimationSpeed = 20;
private int mAnimationInterval = 10;
public SlideHeaderView(Context context) {
super(context);
}
public SlideHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public SlideHeaderView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus && (mHeader == null || mContent == null)) {
initData();
}
}
private void initData() {
int headerId= getResources().getIdentifier("sticky_header", "id", getContext().getPackageName());
int contentId = getResources().getIdentifier("sticky_content", "id", getContext().getPackageName());
if (headerId != 0 && contentId != 0) {
mHeader = findViewById(headerId);
mContent = findViewById(contentId);
if (mOriginalHeaderHeight < 0) {
mOriginalHeaderHeight = mHeader.getMeasuredHeight();
}
mHeaderHeight = mOriginalHeaderHeight;
if (maxTopMargin < 0) {
maxTopMargin = mOriginalHeaderHeight;
}
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (mHeaderHeight > 0) {
mInitDataSucceed = true;
}
if (DEBUG) {
Log.d(TAG, "mTouchSlop = " + mTouchSlop + "mHeaderHeight = " + mHeaderHeight);
}
} else {
throw new NoSuchElementException("Did your view with id \"sticky_header\" or \"sticky_content\" exists?");
}
}
public void setOnInterceptScrollListener(OnInterceptScrollListener l) {
mInterceptScrollListener = l;
}
public void setOnChangeHeaderStateListener(OnChangeHeaderStateListener l) {
mChangeHeaderStateListener = l;
}
public void setAnimationSpeed(int speed) {
mAnimationSpeed = speed;
}
public void setAnimationInterval(int interval) {
mAnimationInterval = interval;
}
public void setMaxTopMargin(int margin) {
maxTopMargin = margin;
}
public void setMinHeaderHeight(int height) {
minHeaderHeight = height;
}
public void setStyleType (StyleType type) {
mStyleType = type;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int intercepted = 0;
int x = getX(event);
int y = getY(event);
Log.d(TAG, "onInterceptTouchEvent-->y = " + y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastXIntercept = x;
mLastYIntercept = y;
mLastX = x;
mLastY = y;
Log.d(TAG, "onInterceptTouchEvent-->mLastY = " + mLastY);
intercepted = 0;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (deltaY <= -mTouchSlop) {
if (mInterceptScrollListener != null && mInterceptScrollListener.isInterceptScrollUpward(event)) {
intercepted = 1;
}
} else if (deltaY >= mTouchSlop) {
if (mInterceptScrollListener != null && mInterceptScrollListener.isInterceptScrollDownward(event)) {
intercepted = 1;
}
}
// Log.d(TAG, "onInterceptTouchEvent-->ACTION_MOVE-->intercepted = " + intercepted);
break;
}
case MotionEvent.ACTION_UP: {
intercepted = 0;
mLastXIntercept = mLastYIntercept = 0;
break;
}
default:
break;
}
if (DEBUG) {
Log.d(TAG, "intercepted=" + intercepted);
}
return intercepted != 0 && mIsSticky;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsSticky) {
return true;
}
int x = getX(event);
int y = getY(event);
Log.d(TAG, "onTouchEvent-->y = " + y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mTopMargin = ((MarginLayoutParams) getLayoutParams()).topMargin;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "onTouchEvent-->deltaY = " + deltaY);
if (DEBUG) {
// Log.d(TAG, "mHeaderHeight=" + mHeaderHeight + " deltaY=" + deltaY + " mlastY=" + mLastY);
}
if (mStyleType == StyleType.SlideIn) {
mHeaderHeight += deltaY;
setHeaderHeight(mHeaderHeight);
} else if (mStyleType == StyleType.SlideOut) {
mTopMargin += deltaY;
setTopMargin();
Log.d(TAG, "onTouchEvent-->ACTION_MOVE-->mTopMargin = " + mTopMargin);
}
break;
}
case MotionEvent.ACTION_UP: {
// 这里做了下判断,当松开手的时候,会自动向两边滑动,具体向哪边滑,要看当前所处的位置
if (mStyleType == StyleType.SlideIn) {
if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {
// mStatus = STATUS_COLLAPSED;
mAutoScrollOrientation = mSlideUpward;//向上滑动
} else {
// mStatus = STATUS_EXPANDED;
mAutoScrollOrientation = mSlideDownward;//向下滑动
}
post(new AutoSlidingRunnable());
} else if (mStyleType == StyleType.SlideOut) {
if (Math.abs(mTopMargin) <= mOriginalHeaderHeight * 0.5) {
// mStatus = STATUS_EXPANDED;
mAutoScrollOrientation = mSlideDownward;//向下滑动
} else {
// mStatus = STATUS_COLLAPSED;
mAutoScrollOrientation = mSlideUpward;//向上滑动
}
post(new AutoSlidingRunnable());
}
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
private int getX(MotionEvent event) {
int x = 0;
if (mStyleType == StyleType.SlideIn) {
x = (int) event.getX();
} else if (mStyleType == StyleType.SlideOut) {
x = (int) event.getRawX();
}
return x;
}
private int getY(MotionEvent event) {
int y = 0;
if (mStyleType == StyleType.SlideIn) {
y = (int) event.getY();
} else if (mStyleType == StyleType.SlideOut) {
y = (int) event.getRawY();
}
return y;
}
public void setOriginalHeaderHeight(int originalHeaderHeight) {
mOriginalHeaderHeight = originalHeaderHeight;
}
public void setHeaderHeight(int height, boolean modifyOriginalHeaderHeight) {
if (modifyOriginalHeaderHeight) {
setOriginalHeaderHeight(height);
}
setHeaderHeight(height);
}
public void setHeaderHeight(int height) {
if (!mInitDataSucceed) {
initData();
}
if (DEBUG) {
// Log.d(TAG, "setHeaderHeight height=" + height);
}
if (height <= minHeaderHeight) {
height = minHeaderHeight;
} else if (height > mOriginalHeaderHeight) {
height = mOriginalHeaderHeight;
}
if (height == minHeaderHeight) {
mStatus = STATUS_COLLAPSED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onCollapseHeader();
}
} else {
mStatus = STATUS_EXPANDED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onExpandHeader();
}
}
if (mHeader != null && mHeader.getLayoutParams() != null) {
mHeader.getLayoutParams().height = height;
mHeader.requestLayout();
mHeaderHeight = height;
} else {
if (DEBUG) {
Log.e(TAG, "null LayoutParams when setHeaderHeight");
}
}
}
public void setTopMargin() {
if (mTopMargin < -maxTopMargin) {
mTopMargin = -maxTopMargin;
} else if (mTopMargin > 0) {
mTopMargin = 0;
}
if (mTopMargin == -maxTopMargin) {
mStatus = STATUS_COLLAPSED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onCollapseHeader();
}
} else {
mStatus = STATUS_EXPANDED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onExpandHeader();
}
}
// Log.d(TAG, "setTopMargin: topMargin = " + mTopMargin);
mTopMarginLayoutParams = (MarginLayoutParams) getLayoutParams();
mTopMarginLayoutParams.topMargin = mTopMargin;
setLayoutParams(mTopMarginLayoutParams);
}
public int getHeaderHeight() {
return mHeaderHeight;
}
public void setSticky(boolean isSticky) {
mIsSticky = isSticky;
}
public void requestDisallowInterceptTouchEventOnHeader(boolean disallowIntercept) {
mDisallowInterceptTouchEventOnHeader = disallowIntercept;
}
public int getHeaderStatus() {
return mStatus;
}
private class AutoSlidingRunnable implements Runnable {
public AutoSlidingRunnable() {}
@Override
public void run() {
switch (mStyleType) {
case SlideOut:
if (mAutoScrollOrientation == mSlideDownward) {
mTopMargin = mTopMargin + mAnimationSpeed;
Log.d(TAG, "向下滑动: topMargin = " + mTopMargin);
if (mTopMargin >= 0 ) {
mTopMargin = 0;
mHandler.sendEmptyMessage(100);
mStatus = STATUS_EXPANDED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onExpandHeader();
}
return;
}
mHandler.sendEmptyMessage(100);
} else if (mAutoScrollOrientation == mSlideUpward) {
mTopMargin = mTopMargin - mAnimationSpeed;
Log.d(TAG, "向上滑动: topMargin = " + mTopMargin);
if (mTopMargin <= -maxTopMargin ) {
mTopMargin = -maxTopMargin;
mHandler.sendEmptyMessage(100);
mStatus = STATUS_COLLAPSED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onCollapseHeader();
}
return;
}
mHandler.sendEmptyMessage(100);
}
postDelayed(this, mAnimationInterval);
break;
case SlideIn:
if (mAutoScrollOrientation == mSlideDownward) {
mHeaderHeight = mHeaderHeight + mAnimationSpeed;
Log.d(TAG, "向下滑动: mHeaderHeight = " + mHeaderHeight);
if (mHeaderHeight >= mOriginalHeaderHeight) {
mHeaderHeight = mOriginalHeaderHeight;
mHandler.sendEmptyMessage(101);
mStatus = STATUS_EXPANDED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onExpandHeader();
}
return;
}
mHandler.sendEmptyMessage(101);
} else if (mAutoScrollOrientation == mSlideUpward) {
mHeaderHeight = mHeaderHeight - mAnimationSpeed;
Log.d(TAG, "向上滑动: mHeaderHeight = " + mHeaderHeight);
if (mHeaderHeight <= minHeaderHeight) {
mHeaderHeight = minHeaderHeight;
mHandler.sendEmptyMessage(101);
mStatus = STATUS_COLLAPSED;
if (mChangeHeaderStateListener != null) {
mChangeHeaderStateListener.onCollapseHeader();
}
return;
}
mHandler.sendEmptyMessage(101);
}
postDelayed(this, mAnimationInterval);
break;
default:
break;
}
}
}
/**
* Handler处理者
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
mTopMarginLayoutParams = (MarginLayoutParams) getLayoutParams();
mTopMarginLayoutParams.topMargin = mTopMargin;
setLayoutParams(mTopMarginLayoutParams);
break;
case 101:
if (mHeader != null && mHeader.getLayoutParams() != null) {
mHeader.getLayoutParams().height = mHeaderHeight;
mHeader.requestLayout();
} else {
if (DEBUG) {
Log.e(TAG, "null LayoutParams when setHeaderHeight");
}
}
break;
default:
break;
}
}
};
public void expandHeader() {
mAutoScrollOrientation = mSlideDownward;
post(new AutoSlidingRunnable());
}
}
使用这种方式解决滑动冲突问题,只需重新外层滑动View,内层滑动View不用做任何处理。
在使用SlideHeaderView的Activity中需要实现SlideHeaderView.OnInterceptScrollListener接口
@Override
public boolean isInterceptScrollUpward(MotionEvent event) {
if (isNeedIntercept) {
return true;
}
return false;
}
@Override
public boolean isInterceptScrollDownward(MotionEvent event) {
if (isNeedIntercept) {
return true;
}
return false;
}
xml文件的实现
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFEEEFEF">
<SlideHeaderView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/sticky_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
<LinearLayout
android:id="@+id/sticky_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFEEEFEF"></android.support.v4.view.ViewPager>
</LinearLayout>
</SlideHeaderView>
</RelativeLayout>
过一段时间,写一个demo上传上来。
reference:Android开发艺术探索