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)的距離一起做判斷,來確定,頁面該滑動(dòng)到那個(gè)頁面,這里用到了彈性滑動(dòng)Scroller
- 難點(diǎn)來了:算法,
//即確定當(dāng)前顯示的子控件的位置,
//確定彈性滑動(dòng)滑向那個(gè)位置
if (Math.abs(velocityY) > criticalVelocityY) {//當(dāng)手指滑動(dòng)速度快時(shí),按照速度方向直接翻頁
// 重點(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 + "");
總結(jié)
當(dāng)要寫一個(gè)算法時(shí),不要著急試圖一下子寫出來,這樣往往陷入盲目,應(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)方向來判定應(yīng)該滾動(dòng)到哪個(gè)子控件的界面,并且記得調(diào)用invalidate();
if (Math.abs(velocityY) > criticalVelocityY) {//當(dāng)手指滑動(dòng)速度快時(shí),按照速度方向直接翻頁
// 重點(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 + "");
} else {//當(dāng)手指滑動(dòng)速度不夠快時(shí),按照松手時(shí),已經(jīng)滑動(dòng)的位置來決定翻頁
// 重點(diǎn)三、低速滑動(dòng)時(shí),如何根據(jù)位置來判斷當(dāng)前顯示的是第幾個(gè)控件,(這里不必再次進(jìn)行邊界判斷,因?yàn)榈谝淮蔚倪吔缗袛啵谶@里會(huì)起到作用)
if ((getScrollY() >= childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) {
// 手指向上滑動(dòng)并且,滾動(dòng)距離過了屏幕一半的距離
mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage) - getScrollY());
currentPage++;
} else if ((getScrollY() < childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) {
// 手指向上滑動(dòng)并且,滾動(dòng)距離沒有過屏幕一半的距離
mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY());
} else if
((getScrollY() <= childHeight * (currentPage - 2) + childHeight / 2
&& shouZhiXiangXiaHuaDong)) {
// 手指向下滑動(dòng)并且,滾動(dòng)距離過了屏幕一半的距離
mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY());
currentPage--;
} else if
((getScrollY() > childHeight * (currentPage - 2) + childHeight / 2
&& shouZhiXiangXiaHuaDong)) {
// 手指向下滑動(dòng)并且,滾動(dòng)距離沒有過屏幕一半的距離
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軸的距離
// 換句話說,就是你手指每次滑動(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)翻頁效果
- 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ù)庫查找與導(dǎo)出方法(圖文詳解)
這篇文章主要介紹了Android Studio3.6.3 當(dāng)前最新版本數(shù)據(jù)庫查找與導(dǎo)出方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例
這篇文章主要介紹了 Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例的相關(guān)資料,這里自定義scrollview 并實(shí)現(xiàn)反彈效果,需要的朋友可以參考下2017-07-07
Android 桌面Widget開發(fā)要點(diǎn)解析(時(shí)間日期Widget)
總的來說,widget主要功能就是顯示一些信息。我們今天編寫一個(gè)很簡(jiǎn)單的作為widget,顯示時(shí)間、日期、星期幾等信息。需要顯示時(shí)間信息,那就需要實(shí)時(shí)更新,一秒或者一分鐘更新一次2013-07-07
Android Recyclerview實(shí)現(xiàn)多選,單選,全選,反選,批量刪除的功能
本篇文章主要介紹了Android Recyclerview 實(shí)現(xiàn)多選,單選,全選,反選,批量刪除的功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼
本篇文章主要介紹了Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03
Android中RecyclerView實(shí)現(xiàn)滑動(dòng)刪除與拖拽功能
這篇文章主要使用了RecyclerView的ItemTouchHelper類實(shí)現(xiàn)了Item的拖動(dòng)和刪除功能,ItemTouchHelper是v7包下的一個(gè)類,下面來看看詳細(xì)的介紹吧,需要的朋友可以參考學(xué)習(xí)。2017-02-02

