
前言
很多時(shí)候,作為程序猿的我們都會(huì)接到產(chǎn)品的奇奇怪怪的需求,比如我們正要說(shuō)的,要在同一個(gè)界面使用兩個(gè)ScrollView,這個(gè)兩個(gè)ScrollView不是并列的,而是在垂直方向的哦。這時(shí)你的心里肯定有千萬(wàn)只草泥馬飛奔而過(guò),這坑爹需求,怎么可能兩個(gè)ScrollView同時(shí)使用啊,一個(gè)界面里有兩個(gè)ScrollView不是滑動(dòng)各種沖突了啊。別急,這篇文章就是為了幫你解決這樣的變態(tài)需求的,且聽我一一道來(lái)。
知識(shí)要點(diǎn)
首先,你必須掌握如何自定義View,然后還要熟悉事件的傳遞機(jī)制,再來(lái)就是怎么使用Scroller,最后還要知道VelocityTracker(計(jì)算手勢(shì)滑動(dòng)的速度),掌握這些知識(shí)之后,看起這篇文章來(lái)將水到渠成了,如果還不是很了解或者熟悉的同學(xué),就先自行Google或者度娘下了,如果有需要,我也會(huì)把相關(guān)的文章寫下來(lái)的。
先來(lái)看看效果圖:

實(shí)現(xiàn)
1、自定義一個(gè)Relativelayout。
public class LayoutContainer extends RelativeLayout {
public LayoutContainer(Context context) {
super(context);
}
public LayoutContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
然后我們一步一步往里面添加代碼了。
2、將兩個(gè)ScrollView繪制進(jìn)來(lái)。
我們把這兩個(gè)View分別定義為mTopView和mBottomView。
繪制進(jìn)來(lái)我們使用了onMeasure和onLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//isMeasured的存在是只onMeasure一次,不讓requestLayout調(diào)用的時(shí)候再次onMeasure
if (!isMeasured) {
isMeasured = true;
mViewHeight = getMeasuredHeight();
mViewWidth = getMeasuredWidth();
mTopView = getChildAt(0);
mBottomView = getChildAt(1);
mBottomView.setOnTouchListener(bottomViewTouchListener);
mTopView.setOnTouchListener(topViewTouchListener);
}
}
這里我們讓TopView和BottomView分別實(shí)現(xiàn)了OnTouchListener,目的就是為了判斷,如果當(dāng)前界面在TopView時(shí),是否可以繼續(xù)往下滑動(dòng),如果當(dāng)前界面在BottomView的時(shí)候,是否可以繼續(xù)往上滑動(dòng)。
private OnTouchListener topViewTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ScrollView sv = (ScrollView) v;
//判斷是否在最底部
if (sv.getScrollY() == (sv.getChildAt(0).getMeasuredHeight() - sv.getMeasuredHeight()) && mCurrentViewIndex == 0) {
canPullUp = true;
} else {
canPullUp = false;
}
return mCanScroll;
}
};
private OnTouchListener bottomViewTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ScrollView sv = (ScrollView) v;
//判斷是否在最頂部
if (sv.getScrollY() == 0 && mCurrentViewIndex == 1) {
canPullDown = true;
} else {
canPullDown = false;
}
return mCanScroll;
}
};
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//mMoveLen 手滑動(dòng)距離,這個(gè)是控制布局的主要變量
mTopView.layout(0, (int) mMoveLen, mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen);
mBottomView.layout(0, mTopView.getMeasuredHeight() + (int) mMoveLen,
mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen + mBottomView.getMeasuredHeight());
}
一開始初始化的時(shí)候,就是把TopView和BottomView分別繪制進(jìn)來(lái),把BottomView繪制在TopView的下面。
3、添加事件分發(fā),也就是dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (vt == null) {
vt = VelocityTracker.obtain();
} else {
vt.clear();
}
mLastY = ev.getY();
mLastX = ev.getX();
mTempLastY = ev.getY();
vt.addMovement(ev);
mEvents = 0;
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
mEvents = -1;
break;
case MotionEvent.ACTION_MOVE:
vt.addMovement(ev);
//mCurrentViewIndex:記錄當(dāng)前展示的是哪個(gè)view,0是TopView,1是BottomView
if (canPullUp && mCurrentViewIndex == 0 && mEvents == 0) {
mMoveLen += (ev.getY() - mLastY);//不斷計(jì)算滑動(dòng)的距離
if (mMoveLen > 0) {
mMoveLen = 0;
mCurrentViewIndex = 0;
} else if (mMoveLen < -mViewHeight) {
mMoveLen = -mViewHeight;
mCurrentViewIndex = 1;
}
if (mMoveLen < -5) {
// 防止事件沖突
mCanScroll = true;
} else {
mCanScroll = false;
}
} else if (canPullDown && mCurrentViewIndex == 1 && mEvents == 0) {
mMoveLen += (ev.getY() - mLastY);
if (mMoveLen < -mViewHeight) {
mMoveLen = -mViewHeight;
mCurrentViewIndex = 1;
} else if (mMoveLen > 0) {
mMoveLen = 0;
mCurrentViewIndex = 0;
}
if (mMoveLen > 5 - mViewHeight) {
// 防止事件沖突
mCanScroll = true;
} else {
mCanScroll = false;
}
} else {
mCanScroll = false;
mEvents++;
}
mLastY = ev.getY();
requestLayout();
break;
case MotionEvent.ACTION_UP:
double deltaX = Math.sqrt((ev.getX() - mLastX) * (ev.getX() - mLastX) + (ev.getY() - mTempLastY) * (ev.getY() - mTempLastY));
if (deltaX < 10) {
mIsClick = true;
}
if (mIsClick) {
mIsClick = false;
break;
}
mLastY = ev.getY();
vt.addMovement(ev);
//你想要指定的得到的速度單位,如果值為1,代表1毫秒運(yùn)動(dòng)了多少像素。如果值為500,代表 0.5秒內(nèi)運(yùn)動(dòng)了多少像素
vt.computeCurrentVelocity(500);
// 獲取Y方向的速度 可以通過(guò)getXVelocity()和getYVelocity()獲得橫向和豎向的速率
int initialVelocity = (int) vt.getYVelocity();
if (mMoveLen != 0 && mMoveLen != mTempMoveLen) {
fling(-initialVelocity);
}
mTempMoveLen = mMoveLen;
try {
vt.recycle();
vt = null;
} catch (Exception e) {
e.printStackTrace();
}
break;
}
super.dispatchTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
這里可以看到最后是return super.dispatchTouchEvent(ev),這是為了讓子View也可以接收到事件。
這里的VelocityTracker,就是用來(lái)計(jì)算手滑動(dòng)的速度,這個(gè)要用來(lái)當(dāng)TopView要過(guò)渡到BottomView的時(shí)候,模擬ScrollView的滾動(dòng)慣性的。
模擬ScrollView滾動(dòng)慣性關(guān)鍵就在于Scroller,我們可以看到MotionEvent.ACTION_UP的時(shí)候,有這樣的方法:fling(-initialVelocity);
public void fling(int velocityY) {
if (getChildCount() > 0) {
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, -700, 700);
awakenScrollBars(mScroller.getDuration());
invalidate();
}
}
這里的 -700和700分別是往上滾動(dòng)和往下滾動(dòng)的最大值。
然后重寫computeScroll方法
/**
* 當(dāng)scrollY > 0時(shí),是往下滑動(dòng),當(dāng)scrollY < 0時(shí),是往上滑動(dòng)
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset() && mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
int scrollX = mScroller.getCurrX();
int scrollY = mScroller.getCurrY();
//往上滑動(dòng)
if (scrollY > 0 && oldY < 0) {
oldY = 0;
}
if (scrollY > 0 && scrollY < oldY) {
oldY = 0;
}
//往下滑動(dòng)
if (scrollY < 0 && scrollY > oldY) {
oldY = 0;
}
if (scrollY < 0 && oldY > 0) {
oldY = 0;
}
if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
scrollTo(scrollX, scrollY);
}
if (scrollY > 0) {
mMoveLen = mMoveLen - scrollY + oldY;
}
if (scrollY < 0) {
mMoveLen = mMoveLen - scrollY + oldY;
}
if (mMoveLen > 0) {
mMoveLen = 0;
}
if (mMoveLen <= (-mScreenH + getStatusBarHeight(getContext()))) {
mMoveLen = -mScreenH + getStatusBarHeight(getContext());
}
oldY = scrollY;
if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
requestLayout();
}
}
}
這里進(jìn)行了一系列判斷,當(dāng)時(shí)也是累啊,首先是要判斷是往上滑動(dòng)還是往下滑動(dòng),再來(lái)就是判斷最大的滑動(dòng)距離,然后再進(jìn)行重寫scrollTo方法。
@Override
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate();
}
}
}
這里使用到的是onScrollChanged方法進(jìn)行View的滾動(dòng),然后對(duì)View進(jìn)行invalidate,這樣就可以達(dá)到滾動(dòng)的慣性了。
整個(gè)代碼的流程就是這樣的了,實(shí)現(xiàn)了兩個(gè)ScrollView同時(shí)在一個(gè)界面內(nèi),而且可以做到平滑的滑動(dòng),具有ScrollView本身的滑動(dòng)慣性,而且不局限于ScrollView,這要是有滾動(dòng)條的View都可以使用。
總結(jié)
整個(gè)代碼看下來(lái)是不是有點(diǎn)凌亂,我也覺得有點(diǎn)亂,不過(guò)只要有耐心點(diǎn)就可以很快理解了。
首先,一個(gè)自定義RelativeLayout,然后把兩個(gè)包含ScrollView的子View同時(shí)繪制在這個(gè)自定義的RelativeLayout內(nèi),并且是上下排列,然后重寫dispatchTouchEvent方法,對(duì)事件進(jìn)行處理,主要是在TopView和BottomView過(guò)渡的時(shí)候進(jìn)行處理,要判斷是TopView往BottomView過(guò)渡還是BottomView往TopView過(guò)渡,這個(gè)是關(guān)鍵點(diǎn),最后就使用到Scroller進(jìn)行滾動(dòng)慣性的模擬,需要重寫View的computeScroll和scrollTo兩個(gè)方法,然后在滾動(dòng)的時(shí)候需要判斷是否超出了滾動(dòng)的距離,還有滾動(dòng)的方向。
當(dāng)然到了這里肯定少不了源碼的分享了