Android 自定義布局豎向的ViewPager的實(shí)現(xiàn)
Android 自定義布局豎向的ViewPager的實(shí)現(xiàn)
效果圖:
這個(gè)自定義控件涉及到的知識(shí)點(diǎn):
自定義ViewGroup中onMeasure和onLayout的寫法
彈性滾動(dòng)Scroller的用法
速度軌跡追蹤器VelocityTracker的用法
如何處理滑動(dòng)事件沖突
dispatchTouchEvent:(外部攔截)告訴此ScrollLayout的父布局,什么時(shí)候該攔截觸摸事件,什么時(shí)候不該攔截觸摸事件
onInterceptTouchEvent:(內(nèi)部攔截)ScrollLayout告訴自己什么時(shí)候要攔截內(nèi)部子View的觸摸事件,什么時(shí)候不要攔截內(nèi)部子View的觸摸事件
處理觸摸滑動(dòng)的思路:
- 先實(shí)現(xiàn)布局跟著手指的滑動(dòng)而滑動(dòng) scrollBy
- 處理好邊界條件(這次的處理邊界,僅適用于低速滑動(dòng)情況下)
- 如果是快速滑動(dòng)VelocityTracker,必須再次考慮邊界問題(上面考慮的邊界問題不適用于快速滑動(dòng)情況)
- 如果是低速滑動(dòng),要根據(jù)手指滑動(dòng)方向和布局滑動(dòng)的距離一起做判斷,來(lái)確定,頁(yè)面該滑動(dòng)到那個(gè)頁(yè)面,這里用到了彈性滑動(dòng)Scroller
- 難點(diǎn)來(lái)了:算法,
//即確定當(dāng)前顯示的子控件的位置, //確定彈性滑動(dòng)滑向那個(gè)位置 if (Math.abs(velocityY) > criticalVelocityY) {//當(dāng)手指滑動(dòng)速度快時(shí),按照速度方向直接翻頁(yè) // 重點(diǎn)二、快速滑動(dòng)時(shí),如何判斷當(dāng)前顯示的是第幾個(gè)控件,并且再次包含邊界判斷(必須包含邊界判斷,因?yàn)榍懊娴倪吔缗袛啵贿m用于低速滑動(dòng)時(shí)) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個(gè),還繼續(xù)滑動(dòng),注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個(gè),還繼續(xù)滑動(dòng),注意★currentPage mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY()); currentPage++; } } Log.e("eee", currentPage + "");
總結(jié)
當(dāng)要寫一個(gè)算法時(shí),不要著急試圖一下子寫出來(lái),這樣往往陷入盲目,應(yīng)該是一步一步地推導(dǎo),一步一步實(shí)現(xiàn)代碼,指導(dǎo)最后找到規(guī)律,類似于歸納總結(jié)、通項(xiàng)公式的方法。
代碼如下:(注釋很全)
package beautlful.time.com.beautlfultime.view; import android.content.Context; import android.support.v4.view.ViewConfigurationCompat; 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; /** * 注意:此自定義viewgroup只適用于每個(gè)子控件為match_parent的情況,其實(shí)一般情況也都是這種情況 * 注意:此自定義viewgroup,沒有考慮padding的情況,使用者不要在ScrollerLayout里使用任何padding,否則你看到的不是你想要的, * 為了實(shí)現(xiàn)padding效果,你可以為ScrollerLayout的外層再套一層線性布局(或其他布局),在外層布局里使用padding值 * 此自定義viewgroup基于郭霖博客改編,想了解具體實(shí)現(xiàn)細(xì)節(jié),請(qǐng)參照: * Android Scroller完全解析,關(guān)于Scroller你所需知道的一切 * http://blog.csdn.net/guolin_blog/article/details/48719871 */ public class VerticalViewPager extends ViewGroup { int currentPage = 1; /** * 速度軌跡追蹤器 */ private VelocityTracker mVelocityTracker; /** * 此次計(jì)算速度你想要的最大值 */ private final int mMaxVelocity; /** * 第一個(gè)觸點(diǎn)的id, 此時(shí)可能有多個(gè)觸點(diǎn),但至少一個(gè) */ private int mPointerId; /** * 計(jì)算出的豎向滾動(dòng)速率 */ private float velocityY; /** * 手指橫向滑動(dòng)的速率臨界值,大于這個(gè)值時(shí),不考慮手指滑動(dòng)的距離,直接滾動(dòng)到最左邊或者最右邊 */ private int criticalVelocityY = 2500; /** * 用于完成滾動(dòng)操作的實(shí)例 */ private Scroller mScroller; /** * 判定為拖動(dòng)的最小移動(dòng)像素?cái)?shù) */ private int mTouchSlop; /** * 手機(jī)按下時(shí)的屏幕坐標(biāo) */ private float mYDown; /** * 手機(jī)當(dāng)時(shí)所處的屏幕坐標(biāo) */ private float mYMove; /** * 上次觸發(fā)ACTION_MOVE事件時(shí)的屏幕坐標(biāo) */ private float mYLastMove; /** * 界面可滾動(dòng)的頂部邊界 */ private int topBorder; /** * 界面可滾動(dòng)的底部邊界 */ private int bottomBorder; /** * 子控件的高度(這里每個(gè)子控件高度都一樣,都是match_parent) */ private int childHeight; /** * 手指是否是向下滑動(dòng) */ private boolean shouZhiXiangXiaHuaDong; private int childCount; public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); // 第一步,創(chuàng)建Scroller的實(shí)例 mScroller = new Scroller(context); ViewConfiguration configuration = ViewConfiguration.get(context); // 獲取TouchSlop值 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); //此次計(jì)算速度你想要的最大值 mMaxVelocity = ViewConfiguration.get(context).getMaximumFlingVelocity(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 為ScrollerLayout中的每一個(gè)子控件測(cè)量大小 measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { /** * 當(dāng)前子控件之前的所有子控件的總寬度 */ int preChildViewTotalHeight = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 為ScrollerLayout中的每一個(gè)子控件在豎直方向上進(jìn)行布局 if (i == 0) { childView.layout( 0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } else { childView.layout( 0, preChildViewTotalHeight, childView.getMeasuredWidth(), preChildViewTotalHeight + childView.getMeasuredHeight()); } preChildViewTotalHeight += childView.getMeasuredHeight(); } // 初始化上下邊界值 topBorder = getChildAt(0).getTop(); bottomBorder = getChildAt(getChildCount() - 1).getBottom(); childHeight = getChildAt(0).getMeasuredHeight(); } } private int downX; private int downY; // 告訴此ScrollLayout的父布局,什么時(shí)候該攔截觸摸事件,什么時(shí)候不該攔截觸摸事件 public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //讓當(dāng)前ScrollerLayout對(duì)應(yīng)的父控件不要去攔截事件 getParent().requestDisallowInterceptTouchEvent(true); downX = (int) ev.getX(); downY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) ev.getX(); int moveY = (int) ev.getY(); //請(qǐng)求父控件ViewPager攔截觸摸事件,ViewPager左右滾動(dòng)時(shí),不要觸發(fā)該布局的上下滑動(dòng) if (Math.abs(moveY - downY) < Math.abs(moveX - downX)) { getParent().requestDisallowInterceptTouchEvent(false); } else { //請(qǐng)求父控件ViewPager不要攔截觸摸事件,ScrollerLayout自己可以上下滑動(dòng) getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_CANCEL: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); } // ScrollLayout告訴自己什么時(shí)候要攔截內(nèi)部子View的觸摸事件,什么時(shí)候不要攔截內(nèi)部子View的觸摸事件 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //▲▲▲1.求第一個(gè)觸點(diǎn)的id, 此時(shí)可能有多個(gè)觸點(diǎn),但至少一個(gè) mPointerId = ev.getPointerId(0); mYDown = ev.getRawY(); mYLastMove = mYDown; break; case MotionEvent.ACTION_MOVE: mYMove = ev.getRawY(); float diff = Math.abs(mYMove - mYDown); mYLastMove = mYMove; // 當(dāng)手指拖動(dòng)值大于TouchSlop值時(shí),認(rèn)為應(yīng)該進(jìn)行滾動(dòng),攔截子控件的事件 if (diff > mTouchSlop) { return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //▲▲▲2.向VelocityTracker添加MotionEvent acquireVelocityTracker(event); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: //▲▲▲3.求偽瞬時(shí)速度 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); velocityY = mVelocityTracker.getYVelocity(mPointerId); mYMove = event.getRawY(); int scrolledY = (int) (mYLastMove - mYMove);//注意取的是負(fù)值,因?yàn)槭钦麄€(gè)布局在動(dòng),而不是控件在動(dòng) if (getScrollY() + scrolledY < topBorder) {// 如果已經(jīng)在最上端了,就不讓再往上滑動(dòng)了(重點(diǎn)一、邊界判斷,直接照著這個(gè)模板抄就行) scrollTo(0, topBorder); return true;//★★★★★★★★★★★★★★★★這里返回true或者false實(shí)踐證明都可以,但是不能什么都不返回。 } else if (getScrollY() + getHeight() + scrolledY > bottomBorder) {//如果已經(jīng)在最底部了,就不讓再往底部滑動(dòng)了 scrollTo(0, bottomBorder - getHeight()); return true;//★★★★★★★★★★★★★★★★★這里返回true或者false實(shí)踐證明都可以,但是不能什么都不返回。 } scrollBy(0, scrolledY);//手指move時(shí),布局跟著滾動(dòng) if (mYDown <= mYMove) {//★★★判斷手指向上滑動(dòng),還是向下滑動(dòng),要用mYDown,而不是mYLastMove shouZhiXiangXiaHuaDong = true;//手指往下滑動(dòng) } else { shouZhiXiangXiaHuaDong = false;//手指往上滑動(dòng) } mYLastMove = mYMove; break; case MotionEvent.ACTION_UP: // 4.▲▲▲釋放VelocityTracker releaseVelocityTracker(); // 第二步,當(dāng)手指抬起時(shí),根據(jù)當(dāng)前的滾動(dòng)值以及滾動(dòng)方向來(lái)判定應(yīng)該滾動(dòng)到哪個(gè)子控件的界面,并且記得調(diào)用invalidate(); if (Math.abs(velocityY) > criticalVelocityY) {//當(dāng)手指滑動(dòng)速度快時(shí),按照速度方向直接翻頁(yè) // 重點(diǎn)二、快速滑動(dòng)時(shí),如何判斷當(dāng)前顯示的是第幾個(gè)控件,并且再次包含邊界判斷(必須包含邊界判斷,因?yàn)榍懊娴倪吔缗袛?,只適用于低速滑動(dòng)時(shí)) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個(gè),還繼續(xù)滑動(dòng),注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個(gè),還繼續(xù)滑動(dòng),注意★currentPage mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY()); currentPage++; } } Log.e("eee", currentPage + ""); } else {//當(dāng)手指滑動(dòng)速度不夠快時(shí),按照松手時(shí),已經(jīng)滑動(dòng)的位置來(lái)決定翻頁(yè) // 重點(diǎn)三、低速滑動(dòng)時(shí),如何根據(jù)位置來(lái)判斷當(dāng)前顯示的是第幾個(gè)控件,(這里不必再次進(jìn)行邊界判斷,因?yàn)榈谝淮蔚倪吔缗袛?,在這里會(huì)起到作用) if ((getScrollY() >= childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) { // 手指向上滑動(dòng)并且,滾動(dòng)距離過(guò)了屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage) - getScrollY()); currentPage++; } else if ((getScrollY() < childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) { // 手指向上滑動(dòng)并且,滾動(dòng)距離沒有過(guò)屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY()); } else if ((getScrollY() <= childHeight * (currentPage - 2) + childHeight / 2 && shouZhiXiangXiaHuaDong)) { // 手指向下滑動(dòng)并且,滾動(dòng)距離過(guò)了屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } else if ((getScrollY() > childHeight * (currentPage - 2) + childHeight / 2 && shouZhiXiangXiaHuaDong)) { // 手指向下滑動(dòng)并且,滾動(dòng)距離沒有過(guò)屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY()); } /* if ((getScrollY() >= childHeight && !shouZhiXiangXiaHuaDong)//手指往左滑動(dòng),并且滑動(dòng)完全顯示第二個(gè)控件時(shí),viewgroup滑動(dòng)到最右端 || ((getScrollY() >= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong))) {//手指往右滑動(dòng),并且當(dāng)滑動(dòng)沒有完全隱藏最后一個(gè)控件時(shí),viewgroup滑動(dòng)到最右端 // 當(dāng)滾動(dòng)值大于某個(gè)數(shù)字時(shí)(大于第二個(gè)控件的寬度,即完全顯示第二個(gè)控件時(shí))并且是向左滑動(dòng),讓這個(gè)viewgroup滑動(dòng)到整個(gè)Viewgroup的最右側(cè), // 因?yàn)橛覀?cè)的所有控件寬度是600,而現(xiàn)在已經(jīng)滑動(dòng)的距離是getScrollX, // 那么,還應(yīng)該繼續(xù)滑動(dòng)的距離是600-getScrollX(),這里正值表示向右滑動(dòng) mScroller.startScroll(0,getScrollY(), 0, (totalChildHeight - firstChildHeight) - getScrollY()); } else if ((getScrollY() <= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong)//手指往右滑動(dòng),并且當(dāng)滑動(dòng)完全隱藏最后一個(gè)控件時(shí),viewgroup滑動(dòng)到最左端 || (getScrollY() <= childHeight && !shouZhiXiangXiaHuaDong)) {//手指往左滑動(dòng),并且滑動(dòng)沒有完全顯示第二個(gè)控件時(shí),viewgroup滑動(dòng)到最左端 // 當(dāng)滾動(dòng)值小于某個(gè)數(shù)字時(shí),讓這個(gè)viewgroup滑動(dòng)到整個(gè)Viewgroup的最左側(cè), // 因?yàn)榛瑒?dòng)到最左側(cè)時(shí),就是讓整個(gè)viewgroup的滑動(dòng)量為0,而現(xiàn)在已經(jīng)滑動(dòng)的距離是getScrollX, // 那么,還應(yīng)該繼續(xù)滑動(dòng)的距離是0-getScrollX(),這里負(fù)值表示向左滑動(dòng) mScroller.startScroll(0,getScrollY(), 0, 0 - getScrollY()); }*/ } // 必須調(diào)用invalidate()重繪 invalidate(); break; case MotionEvent.ACTION_CANCEL: // 5.▲▲▲釋放VelocityTracker releaseVelocityTracker(); break; } return super.onTouchEvent(event); } @Override public void computeScroll() { // 第三步,重寫computeScroll()方法,并在其內(nèi)部完成平滑滾動(dòng)的邏輯 if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } /** * @param event 向VelocityTracker添加MotionEvent * @see VelocityTracker#obtain() * @see VelocityTracker#addMovement(MotionEvent) */ private void acquireVelocityTracker(final MotionEvent event) { if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 釋放VelocityTracker * * @see VelocityTracker#clear() * @see VelocityTracker#recycle() */ private void releaseVelocityTracker() { if (null != mVelocityTracker) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } /* getScrollX()指的是由viewgroup調(diào)用View的scrollTo(int x, int y)或者scrollBy(int x, int y)產(chǎn)生的X軸的距離 // 換句話說(shuō),就是你手指每次滑動(dòng),引起的是viewgroup累計(jì)滑動(dòng)的距離,右為正 // 指的是相當(dāng)于控件的左上角的為原點(diǎn)的坐標(biāo)值 Log.e("qqq","getX():"+event.getX()); // 指的是相當(dāng)于屏幕的左上角的為原點(diǎn)的坐標(biāo)值 Log.e("qqq","getRawX():"+event.getRawX());*/ }
布局文件
<beautlful.time.com.beautlfultime.view.VerticalViewPager android:id="@+id/verticalViewPager" android:layout_width="match_parent" android:layout_height="150dp"> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_orange_dark" android:text="聊天具體的信息喲" /> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" android:text="置頂" /> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" android:text="刪除" /> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" android:text="美好" /> </beautlful.time.com.beautlfultime.view.VerticalViewPager>
感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
- Android ViewPager自定義輪播圖并解決播放沖突
- Android自定義引導(dǎo)玩轉(zhuǎn)ViewPager的方法詳解
- Android自定義ViewPager實(shí)現(xiàn)縱向滑動(dòng)翻頁(yè)效果
- Android使用自定義PageTransformer實(shí)現(xiàn)個(gè)性的ViewPager動(dòng)畫切換效果
- android自定義ViewPager水平滑動(dòng)彈性效果
- Android自定義ViewPager指示器
- Android自定義ViewPager實(shí)現(xiàn)個(gè)性化的圖片切換效果
- Android自定義ViewPager實(shí)例
- Android自定義超級(jí)炫酷的ViewPage指示器
相關(guān)文章
Android Studio3.6.3 當(dāng)前最新版本數(shù)據(jù)庫(kù)查找與導(dǎo)出方法(圖文詳解)
這篇文章主要介紹了Android Studio3.6.3 當(dāng)前最新版本數(shù)據(jù)庫(kù)查找與導(dǎo)出方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例
這篇文章主要介紹了 Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例的相關(guān)資料,這里自定義scrollview 并實(shí)現(xiàn)反彈效果,需要的朋友可以參考下2017-07-07Android 桌面Widget開發(fā)要點(diǎn)解析(時(shí)間日期Widget)
總的來(lái)說(shuō),widget主要功能就是顯示一些信息。我們今天編寫一個(gè)很簡(jiǎn)單的作為widget,顯示時(shí)間、日期、星期幾等信息。需要顯示時(shí)間信息,那就需要實(shí)時(shí)更新,一秒或者一分鐘更新一次2013-07-07Android Recyclerview實(shí)現(xiàn)多選,單選,全選,反選,批量刪除的功能
本篇文章主要介紹了Android Recyclerview 實(shí)現(xiàn)多選,單選,全選,反選,批量刪除的功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼
本篇文章主要介紹了Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03Android中RecyclerView實(shí)現(xiàn)滑動(dòng)刪除與拖拽功能
這篇文章主要使用了RecyclerView的ItemTouchHelper類實(shí)現(xiàn)了Item的拖動(dòng)和刪除功能,ItemTouchHelper是v7包下的一個(gè)類,下面來(lái)看看詳細(xì)的介紹吧,需要的朋友可以參考學(xué)習(xí)。2017-02-02