自己定義ViewGroup實現仿淘寶的商品詳情頁
近期公司在新版本號上有一個須要。 要在首頁加入一個滑動效果, 詳細就是仿照X寶的商品詳情頁, 拉到頁面底部時有一個粘滯效果,
例如以下圖 X東的商品詳情頁,假設用戶繼續向上拉的話就進入商品圖文描寫敘述界面:
剛開始是想拿來主義。直接從網上找個現成的demo來用, 可是網上無一例外的答案都特別統一: 差點兒所有是ScrollView中再套兩個ScrollView,或者是一個LinearLayout中套兩個ScrollView。 通過指定父view和子view的focus來切換滑動的處理界面---即通過view的requestDisallowInterceptTouchEvent方法來決定是哪一個ScrollView來處理滑動事件。
使用以上方法盡管能夠解一時之渴, 可是存在幾點缺陷:
1 擴展性不強 : 假設興許產品要求不止是兩頁滑動呢。是三頁滑動呢。 難道要嵌3個ScrollView并通過N個推斷來實現嗎
2 兼容性不強 : 假設須要在某一個子頁中須要處理左右滑動事件或者雙指操作事件呢, 此方法就無法實現了
3 個人原因 : 個人喜歡自己掌握主動性,事件的處理自己來控制更靠譜一些(PS:就如同一份感情一樣,須要細心去經營^_^)
總和以上原因, 自己實現了一個ViewGroup,實現文章開頭提到的效果。 廢話不多說 直接上源代碼,下面僅僅是部分主要源代碼,并對每個方法都做了凝視,能夠參照凝視理解。
文章最后對這個ViewGroup加了一點實現的細節以及怎樣使用此VIewGroup。 以及demo地址
package com.mcoy.snapscrollview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* @author jiangxinxing---mcoy in English
*
* 了解此ViewGroup之前。 有兩點一定要做到心中有數
* 一個是對Scroller的使用。 還有一個是對onInterceptTouchEvent和onTouchEvent要做到非常熟悉
* 下面幾個站點能夠做參考用
* http://blog.csdn.net/bigconvience/article/details/26697645
* http://blog.csdn.net/androiddevelop/article/details/8373782
* http://blog.csdn.net/xujainxing/article/details/8985063
*/
public class McoySnapPageLayout extends ViewGroup {
。。。。
public interface McoySnapPage {
/**
* 返回page根節點
*
* @return
*/
View getRootView();
/**
* 是否滑動到最頂端
* 第二頁必須自己實現此方法。來推斷是否已經滑動到第二頁的頂部
* 并決定是否要繼續滑動到第一頁
*/
boolean isAtTop();
/**
* 是否滑動到最底部
* 第一頁必須自己實現此方法,來推斷是否已經滑動到第二頁的底部
* 并決定是否要繼續滑動到第二頁
*/
boolean isAtBottom();
}
public interface PageSnapedListener {
/**
* @mcoy
* 當從某一頁滑動到還有一頁完畢時的回調函數
*/
void onSnapedCompleted(int derection);
}
。
。。。。。
/**
* 設置上下頁面
* @param pageTop
* @param pageBottom
*/
public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) {
mPageTop = pageTop;
mPageBottom = pageBottom;
addPagesAndRefresh();
}
private void addPagesAndRefresh() {
// 設置頁面id
mPageTop.getRootView().setId(0);
mPageBottom.getRootView().setId(1);
addView(mPageTop.getRootView());
addView(mPageBottom.getRootView());
postInvalidate();
}
/**
* @mcoy add
* computeScroll方法會調用postInvalidate()方法。 而postInvalidate()方法中系統
* 又會調用computeScroll方法, 因此會一直在循環互相調用。 循環的終結點是在computeScrollOffset()
* 當computeScrollOffset這種方法返回false時。說明已經結束滾動。
*
* 重要:真正的實現此view的滾動是調用scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
*/
@Override
public void computeScroll() {
//先推斷mScroller滾動是否完畢
if (mScroller.computeScrollOffset()) {
if (mScroller.getCurrY() == (mScroller.getFinalY())) {
if (mNextDataIndex > mDataIndex) {
mFlipDrection = FLIP_DIRECTION_DOWN;
makePageToNext(mNextDataIndex);
} else if (mNextDataIndex < mDataIndex) {
mFlipDrection = FLIP_DIRECTION_UP;
makePageToPrev(mNextDataIndex);
}else{
mFlipDrection = FLIP_DIRECTION_CUR;
}
if(mPageSnapedListener != null){
mPageSnapedListener.onSnapedCompleted(mFlipDrection);
}
}
//這里調用View的scrollTo()完畢實際的滾動
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//必須調用該方法。否則不一定能看到滾動效果
postInvalidate();
}
}
private void makePageToNext(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
private void makePageToPrev(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
public int getCurrentScreen() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return i;
}
}
return mCurrentScreen;
}
public View getCurrentView() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return getChildAt(i);
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
* 重寫了父類的onInterceptTouchEvent()。主要功能是在onTouchEvent()方法之前處理
* touch事件。包含:down、up、move事件。
* 當onInterceptTouchEvent()返回true時進入onTouchEvent()。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
// 記錄y與mLastMotionY差值的絕對值。
// yDiff大于gapBetweenTopAndBottom時就覺得界面拖動了足夠大的距離,屏幕就能夠移動了。
final int yDiff = (int)(y - mLastMotionY);
boolean yMoved = Math.abs(yDiff) > gapBetweenTopAndBottom;
if (yMoved) {
if(MCOY_DEBUG) {
Log.e(TAG, "yDiff is " + yDiff);
Log.e(TAG, "mPageTop.isFlipToBottom() is " + mPageTop.isAtBottom());
Log.e(TAG, "mCurrentScreen is " + mCurrentScreen);
Log.e(TAG, "mPageBottom.isFlipToTop() is " + mPageBottom.isAtTop());
}
if(yDiff < 0 && mPageTop.isAtBottom() && mCurrentScreen == 0
|| yDiff > 0 && mPageBottom.isAtTop() && mCurrentScreen == 1){
Log.e("mcoy", "121212121212121212121212");
mTouchState = TOUCH_STATE_SCROLLING;
}
}
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
mLastMotionY = y;
Log.e("mcoy", "mScroller.isFinished() is " + mScroller.isFinished());
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Release the drag
mTouchState = TOUCH_STATE_REST;
break;
}
boolean intercept = mTouchState != TOUCH_STATE_REST;
Log.e("mcoy", "McoySnapPageLayout---onInterceptTouchEvent return " + intercept);
return intercept;
}
/*
* (non-Javadoc)
*
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
* 主要功能是處理onInterceptTouchEvent()返回值為true時傳遞過來的touch事件
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("mcoy", "onTouchEvent--" + System.currentTimeMillis());
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if(mTouchState != TOUCH_STATE_SCROLLING){
// 記錄y與mLastMotionY差值的絕對值。
// yDiff大于gapBetweenTopAndBottom時就覺得界面拖動了足夠大的距離,屏幕就能夠移動了。
final int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > gapBetweenTopAndBottom;
if (yMoved) {
mTouchState = TOUCH_STATE_SCROLLING;
}
}
// 手指拖動屏幕的處理
if ((mTouchState == TOUCH_STATE_SCROLLING)) {
// Scroll to follow the motion event
final int deltaY = (int) (mLastMotionY - y);
mLastMotionY = y;
final int scrollY = getScrollY();
if(mCurrentScreen == 0){//顯示第一頁。僅僅能上拉時使用
if(mPageTop != null && mPageTop.isAtBottom()){
scrollBy(0, Math.max(-1 * scrollY, deltaY));
}
}else{
if(mPageBottom != null && mPageBottom.isAtTop()){
scrollBy(0, deltaY);
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 彈起手指后。切換屏幕的處理
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) velocityTracker.getYVelocity();
if (Math.abs(velocityY) > SNAP_VELOCITY) {
if( velocityY > 0 && mCurrentScreen == 1 && mPageBottom.isAtTop()){
snapToScreen(mDataIndex-1);
}else if(velocityY < 0 && mCurrentScreen == 0){
snapToScreen(mDataIndex+1);
}else{
snapToScreen(mDataIndex);
}
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}else{
}
mTouchState = TOUCH_STATE_REST;
break;
default:
break;
}
return true;
}
private void clearOnTouchEvents(){
mTouchState = TOUCH_STATE_REST;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void snapToDestination() {
// 計算應該去哪個屏
final int flipHeight = getHeight() / 8;
int whichScreen = -1;
final int topEdge = getCurrentView().getTop();
if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){
//向下滑動
whichScreen = mDataIndex + 1;
}else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){
//向上滑動
whichScreen = mDataIndex - 1;
}else{
whichScreen = mDataIndex;
}
Log.e(TAG, "snapToDestination mDataIndex = " + mDataIndex);
Log.e(TAG, "snapToDestination whichScreen = " + whichScreen);
snapToScreen(whichScreen);
}
private void snapToScreen(int dataIndex) {
if (!mScroller.isFinished())
return;
final int direction = dataIndex - mDataIndex;
mNextDataIndex = dataIndex;
boolean changingScreens = dataIndex != mDataIndex;
View focusedChild = getFocusedChild();
if (focusedChild != null && changingScreens) {
focusedChild.clearFocus();
}
//在這里推斷是否已到目標位置~
int newY = 0;
switch (direction) {
case 1: //須要滑動到第二頁
Log.e(TAG, "the direction is 1");
newY = getCurrentView().getBottom(); // 終于停留的位置
break;
case -1: //須要滑動到第一頁
Log.e(TAG, "the direction is -1");
Log.e(TAG, "getCurrentView().getTop() is "
+ getCurrentView().getTop() + " getHeight() is "
+ getHeight());
newY = getCurrentView().getTop() - getHeight(); // 終于停留的位置
break;
case 0: //滑動距離不夠, 因此不造成換頁。回到滑動之前的位置
Log.e(TAG, "the direction is 0");
newY = getCurrentView().getTop(); //第一頁的top是0, 第二頁的top應該是第一頁的高度
break;
default:
break;
}
final int cy = getScrollY(); // 啟動的位置
Log.e(TAG, "the newY is " + newY + " cy is " + cy);
final int delta = newY - cy; // 滑動的距離,正值是往左滑<—。負值是往右滑—>
mScroller.startScroll(0, cy, 0, delta, Math.abs(delta));
invalidate();
}
。。。
。
}
McoySnapPage是定義在VIewGroup的一個接口, 比方說我們須要類似某東商品詳情那樣,有上下兩頁的效果。 那我就須要自定義兩個類實現這個接口。并實現接口的方法。getRootView須要返回當前頁須要顯示的布局內容;isAtTop須要返回當前頁是否已經在頂端; isAtBottom須要返回當前頁是否已經在底部
onInterceptTouchEvent和onTouchEvent決定當前的滑動狀態, 并決定是有當前VIewGroup攔截touch事件還是由子view去消費touch事件
Demo地址: http://download.csdn.net/detail/zxm317122667/8926295
PS: Mcoy是本人的英文名稱, 希望不要引起誤會^_^
浙公網安備 33010602011771號