效果圖如下:
雖然listview現在已經過時,而且這種效果也滿地都是,但是因爲自己項目的原因還是自己寫一個,而且也想整合都涉及的優化知識點,所以還是值得寫一寫,當作練練手,也算是一種提升吧
一:知識點
二、原理
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》當向上滑動的時:
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去改造就好了,改起來應該比較好改。