Android自定義ViewGroup實(shí)現(xiàn)淘寶商品詳情頁(yè)
最近公司在新版本上有一個(gè)需要,要在首頁(yè)添加一個(gè)滑動(dòng)效果,具體就是仿照X寶的商品詳情頁(yè),拉到頁(yè)面底部時(shí)有一個(gè)粘滯效果,如下圖X東的商品詳情頁(yè),如果用戶繼續(xù)向上拉的話就進(jìn)入商品圖文描述界面:
剛開始是想拿來(lái)主義,直接從網(wǎng)上找個(gè)現(xiàn)成的demo來(lái)用, 但是網(wǎng)上無(wú)一例外的答案都特別統(tǒng)一: 幾乎全部是ScrollView中再套兩個(gè)ScrollView,或者是一個(gè)LinearLayout中套兩個(gè)ScrollView。 通過(guò)指定父view和子view的focus來(lái)切換滑動(dòng)的處理界面---即通過(guò)view的requestDisallowInterceptTouchEvent方法來(lái)決定是哪一個(gè)ScrollView來(lái)處理滑動(dòng)事件。
使用以上方法雖然可以解一時(shí)之渴, 但是存在幾點(diǎn)缺陷:
1 擴(kuò)展性不強(qiáng) : 如果后續(xù)產(chǎn)品要求不止是兩頁(yè)滑動(dòng)呢,是三頁(yè)滑動(dòng)呢, 難道要嵌3個(gè)ScrollView并通過(guò)N個(gè)判斷來(lái)實(shí)現(xiàn)嗎
2 兼容性不強(qiáng) : 如果需要在某一個(gè)子頁(yè)中需要處理左右滑動(dòng)事件或者雙指操作事件呢, 此方法就無(wú)法實(shí)現(xiàn)了
3 個(gè)人原因 : 個(gè)人喜歡自己掌握主動(dòng)性,事件的處理自己來(lái)控制更靠譜一些(PS:就如同一份感情一樣,需要細(xì)心去經(jīng)營(yíng))
總和以上原因, 自己實(shí)現(xiàn)了一個(gè)ViewGroup,實(shí)現(xiàn)文章開頭提到的效果, 廢話不多說(shuō) 直接上源碼,以下只是部分主要源碼,并對(duì)每一個(gè)方法都做了注釋,可以參照注釋理解。 文章最后對(duì)這個(gè)ViewGroup加了一點(diǎn)實(shí)現(xiàn)的細(xì)節(jié)以及如何使用此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之前, 有兩點(diǎn)一定要做到心中有數(shù) * 一個(gè)是對(duì)Scroller的使用, 另一個(gè)是對(duì)onInterceptTouchEvent和onTouchEvent要做到很熟悉 * 以下幾個(gè)網(wǎng)站可以做參考用 * 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根節(jié)點(diǎn) * * @return */ View getRootView(); /** * 是否滑動(dòng)到最頂端 * 第二頁(yè)必須自己實(shí)現(xiàn)此方法,來(lái)判斷是否已經(jīng)滑動(dòng)到第二頁(yè)的頂部 * 并決定是否要繼續(xù)滑動(dòng)到第一頁(yè) */ boolean isAtTop(); /** * 是否滑動(dòng)到最底部 * 第一頁(yè)必須自己實(shí)現(xiàn)此方法,來(lái)判斷是否已經(jīng)滑動(dòng)到第二頁(yè)的底部 * 并決定是否要繼續(xù)滑動(dòng)到第二頁(yè) */ boolean isAtBottom(); } public interface PageSnapedListener { /** * @mcoy * 當(dāng)從某一頁(yè)滑動(dòng)到另一頁(yè)完成時(shí)的回調(diào)函數(shù) */ void onSnapedCompleted(int derection); } 。。。。。。 /** * 設(shè)置上下頁(yè)面 * @param pageTop * @param pageBottom */ public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) { mPageTop = pageTop; mPageBottom = pageBottom; addPagesAndRefresh(); } private void addPagesAndRefresh() { // 設(shè)置頁(yè)面id mPageTop.getRootView().setId(0); mPageBottom.getRootView().setId(1); addView(mPageTop.getRootView()); addView(mPageBottom.getRootView()); postInvalidate(); } /** * @mcoy add * computeScroll方法會(huì)調(diào)用postInvalidate()方法, 而postInvalidate()方法中系統(tǒng) * 又會(huì)調(diào)用computeScroll方法, 因此會(huì)一直在循環(huán)互相調(diào)用, 循環(huán)的終結(jié)點(diǎn)是在computeScrollOffset() * 當(dāng)computeScrollOffset這個(gè)方法返回false時(shí),說(shuō)明已經(jīng)結(jié)束滾動(dòng)。 * * 重要:真正的實(shí)現(xiàn)此view的滾動(dòng)是調(diào)用scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); */ @Override public void computeScroll() { //先判斷mScroller滾動(dòng)是否完成 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); } } //這里調(diào)用View的scrollTo()完成實(shí)際的滾動(dòng) scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //必須調(diào)用該方法,否則不一定能看到滾動(dòng)效果 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事件。 * 當(dāng)onInterceptTouchEvent()返回true時(shí)進(jìn)入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差值的絕對(duì)值。 // yDiff大于gapBetweenTopAndBottom時(shí)就認(rèn)為界面拖動(dòng)了足夠大的距離,屏幕就可以移動(dòng)了。 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時(shí)傳遞過(guò)來(lái)的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差值的絕對(duì)值。 // yDiff大于gapBetweenTopAndBottom時(shí)就認(rèn)為界面拖動(dòng)了足夠大的距離,屏幕就可以移動(dòng)了。 final int yDiff = (int) Math.abs(y - mLastMotionY); boolean yMoved = yDiff > gapBetweenTopAndBottom; if (yMoved) { mTouchState = TOUCH_STATE_SCROLLING; } } // 手指拖動(dòng)屏幕的處理 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){//顯示第一頁(yè),只能上拉時(shí)使用 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() { // 計(jì)算應(yīng)該去哪個(gè)屏 final int flipHeight = getHeight() / 8; int whichScreen = -1; final int topEdge = getCurrentView().getTop(); if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){ //向下滑動(dòng) whichScreen = mDataIndex + 1; }else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){ //向上滑動(dòng) 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(); } //在這里判斷是否已到目標(biāo)位置~ int newY = 0; switch (direction) { case 1: //需要滑動(dòng)到第二頁(yè) Log.e(TAG, "the direction is 1"); newY = getCurrentView().getBottom(); // 最終停留的位置 break; case -1: //需要滑動(dòng)到第一頁(yè) 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: //滑動(dòng)距離不夠, 因此不造成換頁(yè),回到滑動(dòng)之前的位置 Log.e(TAG, "the direction is 0"); newY = getCurrentView().getTop(); //第一頁(yè)的top是0, 第二頁(yè)的top應(yīng)該是第一頁(yè)的高度 break; default: break; } final int cy = getScrollY(); // 啟動(dòng)的位置 Log.e(TAG, "the newY is " + newY + " cy is " + cy); final int delta = newY - cy; // 滑動(dòng)的距離,正值是往左滑<—,負(fù)值是往右滑—> mScroller.startScroll(0, cy, 0, delta, Math.abs(delta)); invalidate(); } }
McoySnapPage是定義在VIewGroup的一個(gè)接口, 比如說(shuō)我們需要類似某東商品詳情那樣,有上下兩頁(yè)的效果。 那我就需要自己定義兩個(gè)類實(shí)現(xiàn)這個(gè)接口,并實(shí)現(xiàn)接口的方法。getRootView需要返回當(dāng)前頁(yè)需要顯示的布局內(nèi)容;isAtTop需要返回當(dāng)前頁(yè)是否已經(jīng)在頂端; isAtBottom需要返回當(dāng)前頁(yè)是否已經(jīng)在底部
onInterceptTouchEvent和onTouchEvent決定當(dāng)前的滑動(dòng)狀態(tài), 并決定是有當(dāng)前VIewGroup攔截touch事件還是由子view去消費(fèi)touch事件
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android手把手教大家制作APP首頁(yè)(下拉刷新、自動(dòng)加載)
這篇文章主要為大家詳細(xì)介紹了Android手把手教大家制作APP首頁(yè),實(shí)現(xiàn)下拉刷新、自動(dòng)加載功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android SurfaceView預(yù)覽變形完美解決方法
本篇文章主要介紹了Android SurfaceView預(yù)覽變形完美解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04Android定時(shí)器和Handler用法實(shí)例分析
這篇文章主要介紹了Android定時(shí)器和Handler用法,實(shí)例分析了Android中的定時(shí)器與Handler相關(guān)使用技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04Android布局自定義Shap圓形ImageView可以單獨(dú)設(shè)置背景與圖片
這篇文章主要介紹了Android布局自定義Shap圓形ImageView可以單獨(dú)設(shè)置背景與圖片 的相關(guān)資料,需要的朋友可以參考下2016-01-01Android獲取手機(jī)SIM卡運(yùn)營(yíng)商信息的方法
這篇文章主要介紹了Android獲取手機(jī)SIM卡運(yùn)營(yíng)商信息的方法,可獲得手機(jī)的型號(hào)、運(yùn)營(yíng)商信息及系統(tǒng)版本等,需要的朋友可以參考下2014-09-09Android調(diào)用相機(jī)并將照片存儲(chǔ)到sd卡上實(shí)現(xiàn)方法
Android中實(shí)現(xiàn)拍照有兩種方法,一種是調(diào)用系統(tǒng)自帶的相機(jī),還有一種是自己用Camera類和其他相關(guān)類實(shí)現(xiàn)相機(jī)功能,這種方法定制度比較高,需要的朋友可以了解下2012-12-12Android基于BaseExpandableListAdapter實(shí)現(xiàn)的二級(jí)列表仿通話記錄功能詳解
這篇文章主要介紹了Android基于BaseExpandableListAdapter實(shí)現(xiàn)的二級(jí)列表仿通話記錄功能,結(jié)合具體實(shí)例形式分析了Android實(shí)現(xiàn)通話記錄功能的布局與功能相關(guān)操作技巧,需要的朋友可以參考下2017-07-07