您好,登錄后才能下訂單哦!
Android 自定義布局豎向的ViewPager的實現
效果圖:
這個自定義控件涉及到的知識點:
自定義ViewGroup中onMeasure和onLayout的寫法
彈性滾動Scroller的用法
速度軌跡追蹤器VelocityTracker的用法
如何處理滑動事件沖突
dispatchTouchEvent:(外部攔截)告訴此ScrollLayout的父布局,什么時候該攔截觸摸事件,什么時候不該攔截觸摸事件
onInterceptTouchEvent:(內部攔截)ScrollLayout告訴自己什么時候要攔截內部子View的觸摸事件,什么時候不要攔截內部子View的觸摸事件
處理觸摸滑動的思路:
//即確定當前顯示的子控件的位置, //確定彈性滑動滑向那個位置 if (Math.abs(velocityY) > criticalVelocityY) {//當手指滑動速度快時,按照速度方向直接翻頁 // 重點二、快速滑動時,如何判斷當前顯示的是第幾個控件,并且再次包含邊界判斷(必須包含邊界判斷,因為前面的邊界判斷,只適用于低速滑動時) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個,還繼續滑動,注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個,還繼續滑動,注意★currentPage mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY()); currentPage++; } } Log.e("eee", currentPage + "");
總結
當要寫一個算法時,不要著急試圖一下子寫出來,這樣往往陷入盲目,應該是一步一步地推導,一步一步實現代碼,指導最后找到規律,類似于歸納總結、通項公式的方法。
代碼如下:(注釋很全)
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只適用于每個子控件為match_parent的情況,其實一般情況也都是這種情況 * 注意:此自定義viewgroup,沒有考慮padding的情況,使用者不要在ScrollerLayout里使用任何padding,否則你看到的不是你想要的, * 為了實現padding效果,你可以為ScrollerLayout的外層再套一層線性布局(或其他布局),在外層布局里使用padding值 * 此自定義viewgroup基于郭霖博客改編,想了解具體實現細節,請參照: * Android Scroller完全解析,關于Scroller你所需知道的一切 * http://blog.csdn.net/guolin_blog/article/details/48719871 */ public class VerticalViewPager extends ViewGroup { int currentPage = 1; /** * 速度軌跡追蹤器 */ private VelocityTracker mVelocityTracker; /** * 此次計算速度你想要的最大值 */ private final int mMaxVelocity; /** * 第一個觸點的id, 此時可能有多個觸點,但至少一個 */ private int mPointerId; /** * 計算出的豎向滾動速率 */ private float velocityY; /** * 手指橫向滑動的速率臨界值,大于這個值時,不考慮手指滑動的距離,直接滾動到最左邊或者最右邊 */ private int criticalVelocityY = 2500; /** * 用于完成滾動操作的實例 */ private Scroller mScroller; /** * 判定為拖動的最小移動像素數 */ private int mTouchSlop; /** * 手機按下時的屏幕坐標 */ private float mYDown; /** * 手機當時所處的屏幕坐標 */ private float mYMove; /** * 上次觸發ACTION_MOVE事件時的屏幕坐標 */ private float mYLastMove; /** * 界面可滾動的頂部邊界 */ private int topBorder; /** * 界面可滾動的底部邊界 */ private int bottomBorder; /** * 子控件的高度(這里每個子控件高度都一樣,都是match_parent) */ private int childHeight; /** * 手指是否是向下滑動 */ private boolean shouZhiXiangXiaHuaDong; private int childCount; public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); // 第一步,創建Scroller的實例 mScroller = new Scroller(context); ViewConfiguration configuration = ViewConfiguration.get(context); // 獲取TouchSlop值 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); //此次計算速度你想要的最大值 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中的每一個子控件測量大小 measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { /** * 當前子控件之前的所有子控件的總寬度 */ int preChildViewTotalHeight = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 為ScrollerLayout中的每一個子控件在豎直方向上進行布局 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的父布局,什么時候該攔截觸摸事件,什么時候不該攔截觸摸事件 public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //讓當前ScrollerLayout對應的父控件不要去攔截事件 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(); //請求父控件ViewPager攔截觸摸事件,ViewPager左右滾動時,不要觸發該布局的上下滑動 if (Math.abs(moveY - downY) < Math.abs(moveX - downX)) { getParent().requestDisallowInterceptTouchEvent(false); } else { //請求父控件ViewPager不要攔截觸摸事件,ScrollerLayout自己可以上下滑動 getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_CANCEL: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); } // ScrollLayout告訴自己什么時候要攔截內部子View的觸摸事件,什么時候不要攔截內部子View的觸摸事件 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //▲▲▲1.求第一個觸點的id, 此時可能有多個觸點,但至少一個 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; // 當手指拖動值大于TouchSlop值時,認為應該進行滾動,攔截子控件的事件 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.求偽瞬時速度 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); velocityY = mVelocityTracker.getYVelocity(mPointerId); mYMove = event.getRawY(); int scrolledY = (int) (mYLastMove - mYMove);//注意取的是負值,因為是整個布局在動,而不是控件在動 if (getScrollY() + scrolledY < topBorder) {// 如果已經在最上端了,就不讓再往上滑動了(重點一、邊界判斷,直接照著這個模板抄就行) scrollTo(0, topBorder); return true;//★★★★★★★★★★★★★★★★這里返回true或者false實踐證明都可以,但是不能什么都不返回。 } else if (getScrollY() + getHeight() + scrolledY > bottomBorder) {//如果已經在最底部了,就不讓再往底部滑動了 scrollTo(0, bottomBorder - getHeight()); return true;//★★★★★★★★★★★★★★★★★這里返回true或者false實踐證明都可以,但是不能什么都不返回。 } scrollBy(0, scrolledY);//手指move時,布局跟著滾動 if (mYDown <= mYMove) {//★★★判斷手指向上滑動,還是向下滑動,要用mYDown,而不是mYLastMove shouZhiXiangXiaHuaDong = true;//手指往下滑動 } else { shouZhiXiangXiaHuaDong = false;//手指往上滑動 } mYLastMove = mYMove; break; case MotionEvent.ACTION_UP: // 4.▲▲▲釋放VelocityTracker releaseVelocityTracker(); // 第二步,當手指抬起時,根據當前的滾動值以及滾動方向來判定應該滾動到哪個子控件的界面,并且記得調用invalidate(); if (Math.abs(velocityY) > criticalVelocityY) {//當手指滑動速度快時,按照速度方向直接翻頁 // 重點二、快速滑動時,如何判斷當前顯示的是第幾個控件,并且再次包含邊界判斷(必須包含邊界判斷,因為前面的邊界判斷,只適用于低速滑動時) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個,還繼續滑動,注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個,還繼續滑動,注意★currentPage mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY()); currentPage++; } } Log.e("eee", currentPage + ""); } else {//當手指滑動速度不夠快時,按照松手時,已經滑動的位置來決定翻頁 // 重點三、低速滑動時,如何根據位置來判斷當前顯示的是第幾個控件,(這里不必再次進行邊界判斷,因為第一次的邊界判斷,在這里會起到作用) if ((getScrollY() >= childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) { // 手指向上滑動并且,滾動距離過了屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage) - getScrollY()); currentPage++; } else if ((getScrollY() < childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) { // 手指向上滑動并且,滾動距離沒有過屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY()); } else if ((getScrollY() <= childHeight * (currentPage - 2) + childHeight / 2 && shouZhiXiangXiaHuaDong)) { // 手指向下滑動并且,滾動距離過了屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } else if ((getScrollY() > childHeight * (currentPage - 2) + childHeight / 2 && shouZhiXiangXiaHuaDong)) { // 手指向下滑動并且,滾動距離沒有過屏幕一半的距離 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY()); } /* if ((getScrollY() >= childHeight && !shouZhiXiangXiaHuaDong)//手指往左滑動,并且滑動完全顯示第二個控件時,viewgroup滑動到最右端 || ((getScrollY() >= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong))) {//手指往右滑動,并且當滑動沒有完全隱藏最后一個控件時,viewgroup滑動到最右端 // 當滾動值大于某個數字時(大于第二個控件的寬度,即完全顯示第二個控件時)并且是向左滑動,讓這個viewgroup滑動到整個Viewgroup的最右側, // 因為右側的所有控件寬度是600,而現在已經滑動的距離是getScrollX, // 那么,還應該繼續滑動的距離是600-getScrollX(),這里正值表示向右滑動 mScroller.startScroll(0,getScrollY(), 0, (totalChildHeight - firstChildHeight) - getScrollY()); } else if ((getScrollY() <= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong)//手指往右滑動,并且當滑動完全隱藏最后一個控件時,viewgroup滑動到最左端 || (getScrollY() <= childHeight && !shouZhiXiangXiaHuaDong)) {//手指往左滑動,并且滑動沒有完全顯示第二個控件時,viewgroup滑動到最左端 // 當滾動值小于某個數字時,讓這個viewgroup滑動到整個Viewgroup的最左側, // 因為滑動到最左側時,就是讓整個viewgroup的滑動量為0,而現在已經滑動的距離是getScrollX, // 那么,還應該繼續滑動的距離是0-getScrollX(),這里負值表示向左滑動 mScroller.startScroll(0,getScrollY(), 0, 0 - getScrollY()); }*/ } // 必須調用invalidate()重繪 invalidate(); break; case MotionEvent.ACTION_CANCEL: // 5.▲▲▲釋放VelocityTracker releaseVelocityTracker(); break; } return super.onTouchEvent(event); } @Override public void computeScroll() { // 第三步,重寫computeScroll()方法,并在其內部完成平滑滾動的邏輯 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調用View的scrollTo(int x, int y)或者scrollBy(int x, int y)產生的X軸的距離 // 換句話說,就是你手指每次滑動,引起的是viewgroup累計滑動的距離,右為正 // 指的是相當于控件的左上角的為原點的坐標值 Log.e("qqq","getX():"+event.getX()); // 指的是相當于屏幕的左上角的為原點的坐標值 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>
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。