編輯:關於Android編程
終於不再是實習生,走出了窮逼的實習生生活,正式開始稍微不那麼窮逼的正職員工生活,三天的啟航培訓還是挺歡樂的,學到一句話,保持饑渴,哈哈,感覺放到哪方面都說得通。
ViewPager的嵌套使用是一個很常見的問題,然而,最近又一次遇到ViewPager的嵌套使用問題。
情景是這樣的,需求上給出了這樣的要求,需要實現內外兩個ViewPager嵌套的效果,外部ViewPager控制著4個Tab的切換滑動,內部ViewPager控制著若干個二級Tab的滑動切換(這裡可能是廣告欄,也可能是榜單等),另外,當內部ViewPager滑動到最左或者最右的時候,外部ViewPager恢復滑動。
需求剛一看的時候,確實不難,於是我立馬想出了方案一。
解決兩個ViewPager滑動沖突問題,首先我們要知道為什麼會產生滑動沖突,通過查看源碼我們大致發現,是由於ViewPager的onInteruptTouchEvent()方法攔截了傳遞給子View的觸摸事件。
那麼從這個點出發,解決方法就是,重寫內部ViewPager,調用getParent().requestDisallowInterceptTouchEvent(true)方法來禁止外部ViewPager攔截觸摸事件,使得內部ViewPager可以滑動。
代碼:
package com.zero.viewpagerdemo.way1;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* @author linzewu
* @date 16-7-12
*/
public class InnerViewPager extends ViewPager {
public InnerViewPager(Context context) {
super(context);
}
public InnerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
}
這個方案實現了兩個ViewPager嵌套時,保證了內部ViewPager能夠正常滑動,同時外部ViewPager可以滑動,但是,當手指觸摸范圍在內部ViewPager內的時候,而且內部ViewPager滑動了最左或者最右邊的時候,理想狀態下是此時外部ViewPager恢復響應,可以滑動,如果按照以上代碼,就實現不了這一步。
既然我們要監控內部ViewPager是否滑動最左或者最右再來判斷是否讓觸摸事件傳遞給內部ViewPager,顯然從內部ViewPager去實現似乎不太理想,那就從外部ViewPager著手處理,
我們去試著重寫外部ViewPager.
於是,我想了一個能夠解決問題,但不是很高級的方法.
讓外部ViewPager持有內部ViewPager的引用,這樣外部ViewPager就可以在onInterceptTouchEvent()方法裡通過判斷內部ViewPager是否滑動到了最左或者最右。
package com.zero.viewpagerdemo.way2;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.List;
/**
* 嵌套ViewPager外部ViewPager滑動沖突解決
*
* @author linzewu
* @date 16-7-12
*/
public class OuterViewPager extends ViewPager {
public OuterViewPager(Context context) {
super(context);
}
public OuterViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
private float mDownX;
private float mMoveX;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mMoveX = ev.getRawX();
InnerViewPager currentViewPager = getCurrentInnerViewPager();
if (currentViewPager != null) {
if (mMoveX - mDownX > 0 && !isViewPagerReachLeft(getCurrentInnerViewPager()
.mViewPager)) {
return false;
} else if (mMoveX - mDownX <= 0 && !isViewPagerReachRight
(getCurrentInnerViewPager().mViewPager)) {
return false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 內部ViewPager集合
*/
private List mInnerViewPagers;
public void setInnerViewPagers(List innerViewPagers) {
this.mInnerViewPagers = innerViewPagers;
}
/**
* 內部ViewPager類
*/
public static class InnerViewPager {
ViewPager mViewPager;
int mIndex; //內部viewPager在外部ViewPager的位置值
public InnerViewPager(ViewPager viewPager, int index) {
this.mViewPager = viewPager;
this.mIndex = index;
}
}
private InnerViewPager getCurrentInnerViewPager() {
if (mInnerViewPagers == null) {
return null;
}
for (InnerViewPager innerViewPager : mInnerViewPagers) {
if (innerViewPager.mIndex == getCurrentItem()) {
return innerViewPager;
}
}
return null;
}
private boolean isViewPagerReachLeft(ViewPager viewPager) {
return viewPager.getCurrentItem() == 0;
}
private boolean isViewPagerReachRight(ViewPager viewPager) {
return viewPager.getCurrentItem() >= viewPager.getChildCount() - 1
&& viewPager.getChildCount() == 2;
}
}
采用重寫外部ViewPager的方式,由於需求需要,當內部ViewPager滑動到最左或者最右的時候,外部ViewPager能夠恢復響應,缺點:需要保留內部ViewPager的引用,導致該控件拓展性不高,這是一種很不高級的重寫方式。
通過以上方案,其實已經是可以解決ViewPager的滑動問題,但是有另一個問題一直困擾著我.因為一開始使用ViewPager嵌套出現滑動沖突的情況,只是在2.3的機器上出現,而在高版本的機器例如5.0,使用ViewPager嵌套ViewPager,是不會出現滑動沖突的情況,這就相當奇怪了,既然高版本不會出現滑動沖突,為什麼在低版本上反而會出現滑動沖突呢?
帶著這些疑問,我開始去翻查資料.
我們先找到ViewPager的onInterceptTouchEvent()方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...//省略不展示
switch (action) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
// The finger has moved enough in the vertical
// direction to be counted as a drag... abort
// any attempt to drag horizontally, to work correctly
// with children that have scrolling containers.
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
...
}
從上面代碼我們可以看出,當判斷為ACTION_MOVE的時候,這裡首先會判斷dx不為0,如果為,0,同時再判斷canScroll()方法返回是否為true,如果為true,則onInterceptTouchEvent整個方法體返回false,也就是當前ViewPager不攔截處理觸摸滑動事件,那麼這些一連串的觸摸滑動事件則會傳遞給子ViewPager處理。
再來看這個關鍵方法canScroll(),從官方的注釋,我們可以知道這個方法的作用是測試某個View的可滑動性.參數checkV很重要,當checkV為ture,返回的是View本身加上子View的可滑動性,當checkV為false,返回的只是View的子View的可滑動性。
我們可以看到代碼中先判斷v是否為ViewGroup,如果為ViewGroup,則依次遞歸調用canScroll()來判斷子View的可滑動性,注意這裡canScroll()的checkV的值為true.最後一行則是判斷自己View本身的可滑動性,當然當checkV為false的時候,不會判斷自己的可滑動性。
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
* @param dx Delta scrolled in pixels
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
而在上文中,我們知道ViewPager在onInterceptTouchEvent()方法中調用的代碼是
canScroll(this, false, (int) dx, (int) x, (int) y)
checkV的值為false,因此返回的是ViewPager的子View的可滑動性。
再進一步查看ViewCompat.canScrollHorizontally()的源碼
/**
* Check if this view can be scrolled horizontally in a certain direction.
*
* @param v The View against which to invoke the method.
* @param direction Negative to check scrolling left, positive to check scrolling right.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public static boolean canScrollHorizontally(View v, int direction) {
return IMPL.canScrollHorizontally(v, direction);
}
canScrollHorizontally()用來判斷當前View的水平滑動性,也就是這個是否還可以滑動。我們發現,這個方法在不同的版本,有不同的方法體。
static final ViewCompatImpl IMPL;
static {
final int version = android.os.Build.VERSION.SDK_INT;
if (version >= 23) {
IMPL = new MarshmallowViewCompatImpl();
} else if (version >= 21) {
IMPL = new LollipopViewCompatImpl();
} else if (version >= 19) {
IMPL = new KitKatViewCompatImpl();
} else if (version >= 17) {
IMPL = new JbMr1ViewCompatImpl();
} else if (version >= 16) {
IMPL = new JBViewCompatImpl();
} else if (version >= 15) {
IMPL = new ICSMr1ViewCompatImpl();
} else if (version >= 14) {
IMPL = new ICSViewCompatImpl();
} else if (version >= 11) {
IMPL = new HCViewCompatImpl();
} else if (version >= 9) {
IMPL = new GBViewCompatImpl();
} else if (version >= 7) {
IMPL = new EclairMr1ViewCompatImpl();
} else {
IMPL = new BaseViewCompatImpl();
}
}
當API 小於7 或者 小於9 或者 小於11 或者 小於14的時候,也就是IMPL的實例為BaseViewCompatImpl,GBViewCompatImpl, EclairMr1ViewCompatImpl,HCViewCompatImpl,它們的canScrollHorizontally方法體是一樣的,因為基礎類都是BaseViewCompatImpl.
public boolean canScrollHorizontally(View v, int direction) {
return (v instanceof ScrollingView) &&
canScrollingViewScrollHorizontally((ScrollingView) v, direction);
}
只有當View實現ScrollingView這個接口的時候,才不會返回false,但是我們都知道ViewPager是繼承ViewGroup而來,也沒有實現這個接口,因此這裡肯定是返回false了。
然而當API大於等於14,IMPL的實例為ICSViewCompatImpl,這時候復寫了canScrollHorizontally()方法。
@Override
public boolean canScrollHorizontally(View v, int direction) {
return ViewCompatICS.canScrollHorizontally(v, direction);
}
我們繼續跟蹤進入,發現最後調用的是View.canScrollHorizontally()方法。
/**
* Check if this view can be scrolled horizontally in a certain direction.
*
* @param direction Negative to check scrolling left, positive to check scrolling right.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public boolean canScrollHorizontally(int direction) {
final int offset = computeHorizontalScrollOffset();
final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
這樣一來,我們可以做出總結了,當API小於14的時候,ViewPager的canScroll()方法無法獲知子View的可滑動性(因為它只會默認返回false,總是告訴你子View不可滑動),當API大於等於14的時候,ViewPager的canScroll()方法能夠最終調用到View.canScrollHorizontally(),即能夠獲知子View的可滑動性。
同時,我們也知道了,為什麼在2.3的機型上,使用ViewPager嵌套ViewPager會出現滑動沖突,而在高版本上,這種讓人郁悶的現象卻神奇地恢復了。
那麼,如果解決這個問題呢,既然canScroll()方法中,當API小於14的時候,默認返回false,我們可以試著讓它最終去調用View.canScrollHorizontally(),實現和當API大於等於14一樣的效果。
package com.zero.viewpagerdemo.way3;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* @author linzewu
* @date 16-7-12
*/
public class CompatibleViewPager extends ViewPager {
public CompatibleViewPager(Context context) {
super(context);
}
public CompatibleViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
if (checkV) {
if (v instanceof ViewPager) {
return ((ViewPager)v).canScrollHorizontally(-dx);
} else {
return ViewCompat.canScrollHorizontally(v, -dx);
}
} else {
return false;
}
}
}
最後,通過在demo的試驗,發現通過這種方式去重寫ViewPager,確實可以解決ViewPager嵌套使用的滑動沖突問題,而且暫時沒有發現一些其他的問題。
廢話不多說先貼效果圖,跟代碼,後面再詳解:效果圖如下效果圖如下由於項目公司數據保密性加了馬賽克。貼代碼代碼如下:package com.tony.linechart;im
Builder模式是一種設計模式,最初被介紹於《設計模式:可復用面向對象軟件的基礎》,目前在Java及Android中用處更是十分廣泛,因此基本的了解與學習應當掌握。一.
CircleImageView實現圓形頭像代碼分享,供大家參考,具體內容如下一、創建屬性文件(attrs.xml)具體操作:1、在項目的values文件底下創建一新的屬性
前言安卓開發中,在寫布局代碼的時候,ide可以看到布局的預覽效果。但是有些效果則必須在運行之後才能看見,比如這種情況:TextView在xml中沒有設置任何字符,而是在a