android之滑動懸浮tab&無限迴圈的viewPager
android之滑動懸浮tab&無限迴圈的viewPager
2017年01月10日 15:12:03 小鐘視野 閱讀數:2627 標籤: 真正的無效迴圈viewpager懸浮tab選中tab居中 更多
效果圖如下:
雖然listview現在已經過時,而且這種效果也滿地都是,但是因為自己專案的原因還是自己寫一個,而且也想整合都涉及的優化知識點,所以還是值得寫一寫,當作練練手,也算是一種提升吧
一:知識點
1、屬性動畫的實現view的移動,讓其懸浮在頂部
2、HorizontalScrollview計算寬度實現選中tab居中
3、Fragment避免預載入
4、viewPager實現真正的無限迴圈只需要5個fragment(思路及原理網上是有的),而不是通過設定viewPager的無限大來實現
5、Fragment中的listview和滑動時的事件衝突解決(外部攔截即父類攔截)
其中知識點3、4不進行講解
知識點3可以移步我的另一篇部落格:
知識點4:可以檢視這個作者的部落格
二、原理
原理的話一步步拆分就不是那麼的難了,一下逐一分析
1、懸浮tab
(1)懸浮的tab是一個horizontalScrollview,重寫FrameLayout為SlideRootFrameLayout作為activity的佈局中父布 局,tab自然是它的一個子view,所以我們可以在這裡搞事情,重寫這個主要是滑動事件用到
(2)計算tab到SlideRootFrameLayout的距離top,然後通過重寫滑動事件,可知其滑動的距離,當手指順著屏 幕向上滑動時,tab跟其一起滑動,其實是控制SlideRootFrameLayout滑動,
《1》若是滑動大於等於top則不再進行滑動
《2》若是小於top,則向上滑動還是遵循《1》,向下滑動則就是要恢復到原來的位置,由於滑動的時候可 知道其滑動的偏移量,所以向下滑動時,滑動距離超過這個偏移量則將偏移量置0就回到原來位置
注意:這裡所說的向上向下滑動,都是手指順著螢幕操作,即手指向上滑動或手指向下滑動
程式碼如下:
重寫父佈局SlideRootFramelayout的onTouchEvent如下
-
@Override
-
public boolean onTouchEvent(MotionEvent ev) {
-
if (mTouchInterceptionListener != null) {
-
switch (ev.getActionMasked()) {
-
case MotionEvent.ACTION_DOWN:
-
mInitialPoint = new PointF(ev.getX(), ev.getY());
-
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
-
event.setLocation(ev.getX(), ev.getY());
-
mTouchInterceptionListener.onDownMotionEvent(event);
-
break;
-
case MotionEvent.ACTION_MOVE:
-
float diffX = ev.getX() - mInitialPoint.x;
-
float diffY = ev.getY() - mInitialPoint.y;
-
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
-
break;
-
case MotionEvent.ACTION_UP:
-
break;
-
case MotionEvent.ACTION_CANCEL:
-
mBeganFromDownMotionEvent = false;
-
mTouchInterceptionListener.onUpOrCancelMotionEvent(ev,mIntercepting);
-
// Children's touches should be canceled regardless of
-
// whether or not this layout intercepted the consecutive motion events.
-
/*if (!mChildrenEventsCanceled) {
-
mChildrenEventsCanceled = true;
-
if (mDownMotionEventPended) {
-
mDownMotionEventPended = false;
-
MotionEvent event1 = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
-
event1.setLocation(ev.getX(), ev.getY());
-
duplicateTouchEventForChildren(ev, event1);
-
} else {
-
duplicateTouchEventForChildren(ev);
-
}
-
}*/
-
break;
-
}
-
return true;
-
}
-
return super.onTouchEvent(ev);
-
}
主要是在Action_Move中搞事情:這裡為了更好的擴充套件自定義一個介面
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
將移動的偏移量返回,再來看看具體實現
-
@Override
-
public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) {
-
/* ViewDragHelper.create(slideRootFrameLayout, new ViewDragHelper.Callback() {
-
@Override
-
public boolean tryCaptureView(View child, int pointerId) {
-
return false;
-
}
-
})*/
-
doMoveHeadFloatTab(diffX, diffY);
-
}
-
/**
-
* 處理當滑動時,懸浮的tab
-
*
-
* @param diffX
-
* @param diffY
-
*/
-
private void doMoveHeadFloatTab(float diffX, float diffY) {
-
//最大隻能移動的距離是 llHeadParent.getHeight()
-
float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);
-
float translationY = getNegativeMaxValue(currTranstionY + diffY, -llHeadParent.getHeight(), 0);
-
if (translationY <= 0 && translationY != currTranstionY) {//手指向上滑動,並且沒有滑動到頂部
-
ViewHelper.setTranslationY(slideRootFrameLayout, translationY);
-
//移動多上距離這個佈局就要增加多少佈局,否則會顯示不全,底部會留有一處空白
-
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) slideRootFrameLayout.getLayoutParams();
-
//一定要減去titleBar,如果沒有去掉Winow.xxx.Title,還要減去這個高度,否則會顯示不全
-
lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));
-
slideRootFrameLayout.requestLayout();//請求重繪,但是會有一閃一閃的情況
-
}
-
}
主要邏輯是在這個方法:
ViewHelper這個是一個工具包,其實裡邊就是屬性動畫的庫,直接使用就好了
-
/***
-
* 手指上移過程dy是負數
-
* 返回負數最大值:0是最大值,不可以超過
-
*
-
* @param value 移動的最終距離:上次的位置+當次移動的偏移量之和,就是本次要移動的最終的偏移量
-
* @param canMoveMaxValue 可移動的最大值
-
* @param maxValue
-
* @return
-
*/
-
public static float getNegativeMaxValue(final float value, final float canMoveMaxValue, final float maxValue) {
-
return Math.min(maxValue, Math.max(canMoveMaxValue, value));
-
}
這個方法是獲取滑動時的距離,向上滑動時dy是負數所以這裡比較最大值設定0 得到滑動的距離之後,接下來就是移動SlideRootFramelayout,其直接藉助viewHelper.setTranslationY搞事情就行,
注意:
-
//一定要減去titleBar,如果沒有去掉Winow.xxx.Title,還要減去這個高度,否則會顯示不全
-
lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));
-
slideRootFrameLayout.requestLayout();//請求重繪,但是會有一閃一閃的情況
SlideRootFramelayout佈局向上移動多少就要增加多少高度,否則會顯示不全,而且一定要重繪,否則不會更新
這樣就實現了懸浮的tab啦,是不是很簡單
2、HorizontalScrollView中的tab居中
(1)、我的思路是將螢幕寬分為三分,即只顯示3個view
(2)、當滑動viewpager或者選中當前的view時,通過獲取當前的view距離horizontalScrollview的距離,然後往左滑動一個view的寬度,選中的view就居中了
程式碼如下:
-
private void init() {
-
screenWidthOneThird = Tools.getScreenSize(context).x / 3;
-
tabTextViewList = new ArrayList<TextView>();
-
}
將螢幕分為三份
然後根據tab資料來源生成N個tabView
-
/**
-
* @description 新增tab欄:資源集合
-
* @author zhongwr
-
* @update 2015年9月1日 下午5:24:44
-
*/
-
@SuppressLint("ResourceAsColor")
-
public void addTabList(ArrayList<TabItem> allTabList) {
-
if (!Tools.isListEmpty(allTabList)) {
-
this.allTabList = allTabList;
-
llTabContainer.setVisibility(View.VISIBLE);
-
llTabContainer.removeAllViews();
-
int size = allTabList.size();
-
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
-
ViewGroup.LayoutParams.WRAP_CONTENT);
-
layoutParams.leftMargin = 30;
-
layoutParams.rightMargin = 30;
-
layoutParams.gravity = Gravity.CENTER_VERTICAL;
-
layoutParams.width = screenWidthOneThird - 60;// 左右兩邊間距
-
for (int i = 0; i < size; i++) {
-
TabItem tabItem = allTabList.get(i);
-
TextView tvTab = createTabTextView(tabItem, layoutParams);
-
tvTab.setOnClickListener(new TabOnClickListener(tabItem.tabIndex));
-
tabTextViewList.add(tvTab);
-
if (1 == tabItem.selected) {// 當前選中的
-
currTabIndex = tabItem.tabIndex;
-
tvCurrTab = tvTab;
-
tvTab.setTextColor(context.getResources().getColor(R.color.red1));
-
} else {
-
tvTab.setTextColor(context.getResources().getColor(R.color.gray2));
-
}
-
llTabContainer.addView(tvTab);
-
// 增加豎線
-
View line = new View(context);
-
line.setBackgroundColor(context.getResources().getColor(R.color.color_line_e2));
-
LinearLayout.LayoutParams layoutline = new LinearLayout.LayoutParams(1, 30);
-
line.setLayoutParams(layoutline);
-
llTabContainer.addView(line);
-
}
-
if (null != onClickTabListener) {
-
onClickTabListener.onDefualtTab(currTabIndex, allTabList.get(currTabIndex));
-
}
-
scrollToPosition(currTabIndex);
-
} else {
-
llTabContainer.setVisibility(View.GONE);
-
}
-
}
這裡是通過動態載入的tabView,llTabContainer是HorizontalScrollview的子view是tabView的父類容器
-
/**
-
* @description 設定定位到指定的位置,左右滑動都是往左滑動一個view的寬度,選中的view就居中了
-
* @author zhongwr
-
* @update 2015-11-30 下午3:53:31
-
*/
-
public void scrollToPosition(final int currTabIndex) {
-
scrollView.post(new Runnable() {
-
@Override
-
public void run() {// 選中的view居中
-
TextView textView = tabTextViewList.get(currTabIndex);
-
int left = textView.getLeft();
-
left = left - screenWidthOneThird;
-
scrollView.scrollTo(left, 0);
-
}
-
});
-
}
不管是點選左邊還是右邊的tabView,都是按照向左邊滑動一個tabView的寬度,讓選中的tabView居中。
主要程式碼就是這樣,是不是覺得難度其實也沒什麼,就是靠思路及計算
這些前期工作都已經搞完,解決滑動衝突才真正是個難點
3、解決滑動懸浮tab和viewpager中的listView的衝突
解決事件衝突的方式無非就是兩種:
(1)、外部攔截法:父類控制是否要攔截事件,
重寫攔截方法onInterceptTouchEvent() 返回true 攔截事件 false:不攔截
(2)、內部攔截法:子類通知父類是否需要攔截,
requestDisallowInterceptToucheEvent(boolean) false:攔截 true :不攔截
基於上邊兩個方法規則,這裡我選用第一種方法:外部攔截法
解決衝突還是要一步步分析,什麼時候攔截,什麼時候不攔截?
《1》當向上滑動的時:
1、剛進到頁面還沒滑動,則直接攔截
2、已滑動,但是tab還沒置頂懸浮,則直接攔截,所以1和2可以合起來,tab還沒置頂懸浮直接攔截
3、當tab已懸浮,則不再進行攔截,把事件交給子view(這裡是交給listview)
《2》當向下滑動時:
1、當tab懸浮時:
<1> listview已經滑動,則不攔截,讓listview回到初始位置:即position = 0;
<2> listview已經在初始位置(回到初始位置或者不曾滑動過)則,直接通知父類攔截事件
2、當tab未懸浮時:
<1> 剛進入,tab還是初始位置,則不攔截,將事件交給子view(listview)可以滑動
<2>已滑動,但並未置頂懸浮,只是滑動到一半,則直接攔截,讓tab回到初始位置
基本就是這樣,分析完成之後,接下來就是直接擼碼了。
SlideRootFrameLayout:在外部攔截,這都是交給自定義的介面實現
-
@Override
-
public boolean onInterceptTouchEvent(MotionEvent ev) {
-
if (mTouchInterceptionListener == null) {
-
return false;
-
}
-
switch (ev.getActionMasked()) {
-
case MotionEvent.ACTION_DOWN:
-
mInitialPoint = new PointF(ev.getX(), ev.getY());
-
mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
-
mDownMotionEventPended = true;
-
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
-
mBeganFromDownMotionEvent = mIntercepting;
-
mChildrenEventsCanceled = false;
-
return mIntercepting;
-
case MotionEvent.ACTION_MOVE:
-
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
-
if (mInitialPoint == null) {
-
mInitialPoint = new PointF(ev.getX(), ev.getY());
-
}
-
// diffX and diffY are the origin of the motion, and should be difference
-
// from the position of the ACTION_DOWN event occurred.
-
float diffX = ev.getX() - mInitialPoint.x;
-
float diffY = ev.getY() - mInitialPoint.y;
-
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
-
return mIntercepting;
-
}
-
return false;
-
}
自定義的介面實現 TouchInterceptionListener.shouldInterceptTouchEvent():
-
@Override
-
public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {
-
return doInterceptEvent(diffX, diffY);
-
}
所有的處理都交給了doInterceptEvent():
-
/**
-
* 處理攔截事件
-
*
-
* @param diffX
-
* @param diffY
-
* @return
-
*/
-
private boolean doInterceptEvent(float diffX, float diffY) {
-
float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);
-
float headHeight = -llHeadParent.getHeight();
-
if (Math.abs(diffY) > Math.abs(diffX)) {//上下滑動
-
if (diffY < 0) {//手指向上滑動
-
if (Math.abs(currTranstionY) >= Math.abs(headHeight)) {//移動到頂端(tab懸浮)
-
isUpInterception = false;
-
isTabFloat = true;
-
} else {//還沒移動到頂部所以還是要攔截
-
isUpInterception = true;
-
isTabFloat = false;
-
}
-
// return isUpInterception;
-
} else if (diffY > 0) {//手指向下滑動
-
if (isTabFloat) {//如果tab懸浮著,手指要向下滑動,要攔截將tab復原
-
if (!viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()) {//listview已經滑動了
-
isUpInterception = false;
-
} else {
-
isUpInterception = true;
-
if (Math.abs(currTranstionY) <= 0) {//向下滑動復原
-
isTabFloat = false;
-
}
-
}
-
} else {//tab未懸浮,兩種可能性:一個是可能剛進入時手指向下滑動時不攔截,一個是滑動到一半時要攔截
-
if (Math.abs(currTranstionY) <= 0) {//剛進入時,手指向下滑動,不攔截
-
isUpInterception = false;
-
} else if (Math.abs(currTranstionY) < Math.abs(headHeight)) {//滑動到一半,手指向下滑動要復原,則攔截
-
isUpInterception = true;
-
}
-
}
-
// return isUpInterception;
-
}
-
return isUpInterception;
-
} else {//左右滑動不攔截
-
return false;
-
}
-
}
以上的處理邏輯就是跟我之前分析的一樣,這裡需要還有一處地方就是,listview是否已經滑動了或者是否已經回到初始位置了,需要獲取或者釋放事件主動權要告知父類,當然也是要自定義實現的,這裡只對外部提供一個方法:
viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()
這方法就是通知外部是否需要攔截事件;
由於tab未置頂懸浮或不在初始位置時,listview是不可以滑動的,所以只有在tab置頂浮或回到初始位置時,才可以滑動,才有獲取或者釋放事件的主動權;
接下來分析在什麼情況下,listview需要掌握主動權:
(1)、當向上滑動的時,外部會在之前的規則不攔截事件,此時listview可以任意向上滑動,這種情況可以不管
(2)、當向下滑動時,要回到初始位置,既是第一個位置 position=0;因為只有到了初始位置才通知外部攔截事件,否則不可以攔截事件。
滑動的話,我們立即想到的就是ScrollListener
-
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
-
if (0 == firstVisibleItem) {
-
if (getChildCount() > 0) {
-
View firstView = getChildAt(0);
-
if (0 == firstView.getTop()) {
-
isViewIntercepted = true;
-
}else{
-
isViewIntercepted = false;
-
}
-
}
-
} else {
-
isViewIntercepted = false;
-
}
-
}
onScroll方法:因為它可以直接獲取到一個可見view的position,所以當時position=0時,可以通知攔截;但是這裡直接攔截會有bug,因為會出現firstView沒顯示全就被攔截;所以這裡拿到firstView.getTop();這個top值如果不是0則表示沒顯示全,則不攔截,顯示全則通知攔截;主要是isViewIntercepted這個標誌了,以下是對外的方法,外部通過Fragment間接呼叫的;
-
/**
-
* 當前類是否要被攔截
-
*/
-
private boolean isViewIntercepted = false;
-
/**
-
* 當前view是否被攔截
-
*/
-
public boolean isViewIntercepted() {
-
return isViewIntercepted;
-
}
這篇文章講的這裡算是結束了。
說說這裡遇到的最大的坑
這裡設計的知識點以及坑尤其是ViewPager的無限迴圈使用Fragment會有許多坑;比如迴圈使用更新資料、快取資料、listview定位的快取此外最坑的是 onSelectedPage執行時Fragment並沒有完全繫結activity,這時就要考慮什麼時間點去更新資料,因為沒繫結時可能會出現getActivity為null等等問題,所以如果不是特別大的話載入量的話,不建議使用無限迴圈的Fragment,以上的快取資料也很難管理,此外選中tabView時的定位,要對應上的頁數也需要很大的功夫,所以還是建議使用老套方法,有多少個tab就建立多少個Fragment,只要控制懶載入就好了,其它都很好管理,畢竟那麼點東西android的記憶體還是妥妥的,而且一般使用者都有自己喜歡的某個tab,使用者很少去把所有的tab都點了個遍。不使用無限迴圈可以通過這個demo去改造就好了,改起來應該比較好改。