Android 自定義布局豎向的ViewPager的實現(xiàn)
Android 自定義布局豎向的ViewPager的實現(xiàn)
效果圖:

這個自定義控件涉及到的知識點:
自定義ViewGroup中onMeasure和onLayout的寫法
彈性滾動Scroller的用法
速度軌跡追蹤器VelocityTracker的用法
如何處理滑動事件沖突
dispatchTouchEvent:(外部攔截)告訴此ScrollLayout的父布局,什么時候該攔截觸摸事件,什么時候不該攔截觸摸事件
onInterceptTouchEvent:(內(nèi)部攔截)ScrollLayout告訴自己什么時候要攔截內(nèi)部子View的觸摸事件,什么時候不要攔截內(nèi)部子View的觸摸事件
處理觸摸滑動的思路:
- 先實現(xiàn)布局跟著手指的滑動而滑動 scrollBy
- 處理好邊界條件(這次的處理邊界,僅適用于低速滑動情況下)
- 如果是快速滑動VelocityTracker,必須再次考慮邊界問題(上面考慮的邊界問題不適用于快速滑動情況)
- 如果是低速滑動,要根據(jù)手指滑動方向和布局滑動的距離一起做判斷,來確定,頁面該滑動到那個頁面,這里用到了彈性滑動Scroller
- 難點來了:算法,
//即確定當前顯示的子控件的位置,
//確定彈性滑動滑向那個位置
if (Math.abs(velocityY) > criticalVelocityY) {//當手指滑動速度快時,按照速度方向直接翻頁
// 重點二、快速滑動時,如何判斷當前顯示的是第幾個控件,并且再次包含邊界判斷(必須包含邊界判斷,因為前面的邊界判斷,只適用于低速滑動時)
if (shouZhiXiangXiaHuaDong) {
if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個,還繼續(xù)滑動,注意★(currentPage-2)
mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY());
currentPage--;
}
} else {
if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個,還繼續(xù)滑動,注意★currentPage
mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY());
currentPage++;
}
}
Log.e("eee", currentPage + "");
總結(jié)
當要寫一個算法時,不要著急試圖一下子寫出來,這樣往往陷入盲目,應該是一步一步地推導,一步一步實現(xiàn)代碼,指導最后找到規(guī)律,類似于歸納總結(jié)、通項公式的方法。
代碼如下:(注釋很全)
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,否則你看到的不是你想要的,
* 為了實現(xiàn)padding效果,你可以為ScrollerLayout的外層再套一層線性布局(或其他布局),在外層布局里使用padding值
* 此自定義viewgroup基于郭霖博客改編,想了解具體實現(xiàn)細節(jié),請參照:
* 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;
/**
* 此次計算速度你想要的最大值
*/
private final int mMaxVelocity;
/**
* 第一個觸點的id, 此時可能有多個觸點,但至少一個
*/
private int mPointerId;
/**
* 計算出的豎向滾動速率
*/
private float velocityY;
/**
* 手指橫向滑動的速率臨界值,大于這個值時,不考慮手指滑動的距離,直接滾動到最左邊或者最右邊
*/
private int criticalVelocityY = 2500;
/**
* 用于完成滾動操作的實例
*/
private Scroller mScroller;
/**
* 判定為拖動的最小移動像素數(shù)
*/
private int mTouchSlop;
/**
* 手機按下時的屏幕坐標
*/
private float mYDown;
/**
* 手機當時所處的屏幕坐標
*/
private float mYMove;
/**
* 上次觸發(fā)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);
// 第一步,創(chuàng)建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左右滾動時,不要觸發(fā)該布局的上下滑動
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告訴自己什么時候要攔截內(nèi)部子View的觸摸事件,什么時候不要攔截內(nèi)部子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) {// 如果已經(jīng)在最上端了,就不讓再往上滑動了(重點一、邊界判斷,直接照著這個模板抄就行)
scrollTo(0, topBorder);
return true;//★★★★★★★★★★★★★★★★這里返回true或者false實踐證明都可以,但是不能什么都不返回。
} else if (getScrollY() + getHeight() + scrolledY > bottomBorder) {//如果已經(jīng)在最底部了,就不讓再往底部滑動了
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();
// 第二步,當手指抬起時,根據(jù)當前的滾動值以及滾動方向來判定應該滾動到哪個子控件的界面,并且記得調(diào)用invalidate();
if (Math.abs(velocityY) > criticalVelocityY) {//當手指滑動速度快時,按照速度方向直接翻頁
// 重點二、快速滑動時,如何判斷當前顯示的是第幾個控件,并且再次包含邊界判斷(必須包含邊界判斷,因為前面的邊界判斷,只適用于低速滑動時)
if (shouZhiXiangXiaHuaDong) {
if (currentPage > 1) {//★★★★★★★★邊界限制,防止滑倒第一個,還繼續(xù)滑動,注意★(currentPage-2)
mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY());
currentPage--;
}
} else {
if (currentPage < childCount) {//★★★★★★★邊界限制,防止滑倒最后一個,還繼續(xù)滑動,注意★currentPage
mScroller.startScroll(0, getScrollY(), 0, childHeight * currentPage - getScrollY());
currentPage++;
}
}
Log.e("eee", currentPage + "");
} else {//當手指滑動速度不夠快時,按照松手時,已經(jīng)滑動的位置來決定翻頁
// 重點三、低速滑動時,如何根據(jù)位置來判斷當前顯示的是第幾個控件,(這里不必再次進行邊界判斷,因為第一次的邊界判斷,在這里會起到作用)
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滑動到最右端
// 當滾動值大于某個數(shù)字時(大于第二個控件的寬度,即完全顯示第二個控件時)并且是向左滑動,讓這個viewgroup滑動到整個Viewgroup的最右側(cè),
// 因為右側(cè)的所有控件寬度是600,而現(xiàn)在已經(jīng)滑動的距離是getScrollX,
// 那么,還應該繼續(xù)滑動的距離是600-getScrollX(),這里正值表示向右滑動
mScroller.startScroll(0,getScrollY(), 0, (totalChildHeight - firstChildHeight) - getScrollY());
} else if ((getScrollY() <= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong)//手指往右滑動,并且當滑動完全隱藏最后一個控件時,viewgroup滑動到最左端
|| (getScrollY() <= childHeight && !shouZhiXiangXiaHuaDong)) {//手指往左滑動,并且滑動沒有完全顯示第二個控件時,viewgroup滑動到最左端
// 當滾動值小于某個數(shù)字時,讓這個viewgroup滑動到整個Viewgroup的最左側(cè),
// 因為滑動到最左側(cè)時,就是讓整個viewgroup的滑動量為0,而現(xiàn)在已經(jīng)滑動的距離是getScrollX,
// 那么,還應該繼續(xù)滑動的距離是0-getScrollX(),這里負值表示向左滑動
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)部完成平滑滾動的邏輯
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軸的距離
// 換句話說,就是你手指每次滑動,引起的是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>
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
相關(guān)文章
Android PopupWindow 點擊外面取消實現(xiàn)代碼
這篇文章主要介紹了Android PopupWindow 點擊外面取消實現(xiàn)代碼,本文直接給出核心實現(xiàn)代碼,代碼中包含注釋,需要的朋友可以參考下2015-04-04
Android編程實現(xiàn)ListView滾動提示等待框功能示例
這篇文章主要介紹了Android編程實現(xiàn)ListView滾動提示等待框功能,結(jié)合實例形式分析了Android ListView滾動事件相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-02-02
AndroidStudio Gradle基于友盟的多渠道打包方法
這篇文章主要介紹了AndroidStudio Gradle基于友盟的多渠道打包方法,需要的朋友可以參考下2017-09-09
Android中的人臉檢測的示例代碼(靜態(tài)和動態(tài))
本篇文章主要介紹了Android中的人臉檢測的示例代碼(靜態(tài)和動態(tài)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
android應用開發(fā)之spinner控件的簡單使用
Android的控件有很多種,其中就有一個Spinner的控件,這個控件其實就是一個下拉顯示列表。本文通過腳本之家平臺給大家介紹android應用開發(fā)之spinner控件的簡單使用,感興趣的朋友可以參考下2015-11-11
基于Android實現(xiàn)跳轉(zhuǎn)到WiFi開關(guān)設置頁的詳細步驟
在Android應用開發(fā)中,有時候需要引導用戶到特定的系統(tǒng)設置頁面,例如Wi-Fi開關(guān)設置頁,可以通過隱式Intent來實現(xiàn)這一功能,以下是詳細的步驟以及相關(guān)的Kotlin代碼示例,需要的朋友可以參考下2024-09-09

