實(shí)現(xiàn)淘寶詳情頁(yè)上下兩個(gè)界面彈性切換。在此做個(gè)筆記蔗牡,可以直接復(fù)制使用颖系。
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* 這是一個(gè)viewGroup容器,實(shí)現(xiàn)上下兩個(gè)frameLayout拖動(dòng)切換
*
* @author ncj
*/
@SuppressLint("NewApi")
public class DragLayout extends ViewGroup {
/* 拖拽工具類 */
private final ViewDragHelper mDragHelper;
private GestureDetectorCompat gestureDetector;
/* 上下兩個(gè)frameLayout辩越,在Activity中注入fragment */
private View frameView1, frameView2;
private int viewHeight;
private static final int VEL_THRESHOLD = 100; // 滑動(dòng)速度的閾值嘁扼,超過(guò)這個(gè)絕對(duì)值認(rèn)為是上下
private static final int DISTANCE_THRESHOLD = 100; // 單位是像素,當(dāng)上下滑動(dòng)速度不夠時(shí)黔攒,通過(guò)這個(gè) 閾 值來(lái)判定是應(yīng)該粘到頂部還是底部
private int downTop1; // 手指按下的時(shí)候趁啸,frameView1的getTop值
private ShowNextPageNotifier nextPageListener; // 手指松開(kāi)是否加載下一頁(yè)的notifier
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDragHelper = ViewDragHelper
.create(this, 10f, new DragHelperCallback());
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
gestureDetector = new GestureDetectorCompat(context,
new YScrollDetector());
}
@Override
protected void onFinishInflate() {
// 跟findviewbyId一樣强缘,初始化上下兩個(gè)view
frameView1 = getChildAt(0);
frameView2 = getChildAt(1);
}
class YScrollDetector extends SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
float dy) {
// 垂直滑動(dòng)時(shí)dy>dx,才被認(rèn)定是上下拖動(dòng)
return Math.abs(dy) > Math.abs(dx);
}
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**
* 這是拖拽效果的主要邏輯
*/
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
int childIndex = 1;
if (changedView == frameView2) {
childIndex = 2;
}
// 一個(gè)view位置改變不傅,另一個(gè)view的位置要跟進(jìn)
onViewPosChanged(childIndex, top);
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 兩個(gè)子View都需要跟蹤欺旧,返回true
return true;
}
@Override
public int getViewVerticalDragRange(View child) {
// 這個(gè)用來(lái)控制拖拽過(guò)程中松手后,自動(dòng)滑行的速度蛤签,暫時(shí)給一個(gè)隨意的數(shù)值
return 1;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 滑動(dòng)松開(kāi)后辞友,需要向上或者鄉(xiāng)下粘到特定的位置
animTopOrBottom(releasedChild, yvel);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int finalTop = top;
if (child == frameView1) {
// 拖動(dòng)的時(shí)第一個(gè)view
if (top > 0) {
// 不讓第一個(gè)view往下拖,因?yàn)轫敳繒?huì)白板
finalTop = 0;
}
} else if (child == frameView2) {
// 拖動(dòng)的時(shí)第二個(gè)view
if (top < 0) {
// 不讓第二個(gè)view網(wǎng)上拖震肮,因?yàn)榈撞繒?huì)白板
finalTop = 0;
}
}
// finalTop代表的是理論上應(yīng)該拖動(dòng)到的位置称龙。此處計(jì)算拖動(dòng)的距離除以一個(gè)參數(shù)(3),是讓滑動(dòng)的速度變慢戳晌。數(shù)值越大鲫尊,滑動(dòng)的越慢
return child.getTop() + (finalTop - child.getTop()) / 3;
}
}
/**
* 滑動(dòng)時(shí)view位置改變協(xié)調(diào)處理
*
* @param viewIndex
* 滑動(dòng)view的index(1或2)
* @param posTop
* 滑動(dòng)View的top位置
*/
private void onViewPosChanged(int viewIndex, int posTop) {
if (viewIndex == 1) {
int offsetTopBottom = viewHeight + frameView1.getTop()
- frameView2.getTop();
frameView2.offsetTopAndBottom(offsetTopBottom);
} else if (viewIndex == 2) {
int offsetTopBottom = frameView2.getTop() - viewHeight
- frameView1.getTop();
frameView1.offsetTopAndBottom(offsetTopBottom);
}
// 有的時(shí)候會(huì)默認(rèn)白板,這個(gè)很惡心沦偎。后面有時(shí)間再優(yōu)化
invalidate();
}
private void animTopOrBottom(View releasedChild, float yvel) {
int finalTop = 0; // 默認(rèn)是粘到最頂端
if (releasedChild == frameView1) {
// 拖動(dòng)第一個(gè)view松手
if (yvel < -VEL_THRESHOLD
|| (downTop1 == 0 && frameView1.getTop() < -DISTANCE_THRESHOLD)) {
// 向上的速度足夠大疫向,就滑動(dòng)到頂端
// 向上滑動(dòng)的距離超過(guò)某個(gè)閾值,就滑動(dòng)到頂端
finalTop = -viewHeight;
// 下一頁(yè)可以初始化了
if (null != nextPageListener) {
nextPageListener.onDragNext();
}
}
} else {
// 拖動(dòng)第二個(gè)view松手
if (yvel > VEL_THRESHOLD
|| (downTop1 == -viewHeight && releasedChild.getTop() > DISTANCE_THRESHOLD)) {
// 保持原地不動(dòng)
finalTop = viewHeight;
}
}
if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
/* touch事件的攔截與處理都交給mDraghelper來(lái)處理 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (frameView1.getBottom() > 0 && frameView1.getTop() < 0) {
// view粘到頂部或底部豪嚎,正在動(dòng)畫(huà)中的時(shí)候搔驼,不處理touch事件
return false;
}
boolean yScroll = gestureDetector.onTouchEvent(ev);
boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// action_down時(shí)就讓mDragHelper開(kāi)始工作,否則有時(shí)候?qū)е庐惓?他大爺?shù)? mDragHelper.processTouchEvent(ev);
downTop1 = frameView1.getTop();
}
return shouldIntercept && yScroll;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
// 統(tǒng)一交給mDragHelper處理侈询,由DragHelperCallback實(shí)現(xiàn)拖動(dòng)效果
mDragHelper.processTouchEvent(e); // 該行代碼可能會(huì)拋異常舌涨,正式發(fā)布時(shí)請(qǐng)將這行代碼加上try catch
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 只在初始化的時(shí)候調(diào)用
// 一些參數(shù)作為全局變量保存起來(lái)
frameView1.layout(l, 0, r, b - t);
frameView2.layout(l, 0, r, b - t);
viewHeight = frameView1.getMeasuredHeight();
frameView2.offsetTopAndBottom(viewHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
/**
* 這是View的方法,該方法不支持android低版本(2.2扔字、2.3)的操作系統(tǒng)囊嘉,所以手動(dòng)復(fù)制過(guò)來(lái)以免強(qiáng)制退出
*/
public static int resolveSizeAndState(int size, int measureSpec,
int childMeasuredState) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
public void setNextPageListener(ShowNextPageNotifier nextPageListener) {
this.nextPageListener = nextPageListener;
}
public interface ShowNextPageNotifier {
public void onDragNext();
}
}
布局中使用:
<DragLayout
android:id="@+id/slideLl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/one"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/two"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</DragLayout>
在Activity中使用:
/**
* 框架
*/
public class DragActivity extends BaseActivity implements ShowNextPageNotifier {
@BindView(R.id.dl_layout)
DragLayout dragLayout;
private FragmentProductOne mOneFrament;
private FragmentProductTwo mTwoFrament;
@Override
protected int getResId() {
return R.layout.activity_product_detail;
}
@Override
protected void initView() {
mOneFrament = new FragmentProductOne();
mTwoFrament = new FragmentProductTwo();
getSupportFragmentManager().beginTransaction()
.add(R.id.fy_fragment_one, mOneFrament)
.add(R.id.fy_fragment_two, mTwoFrament).commit();
dragLayout.setNextPageListener(this);
}
@Override
public void onDragNext() {
}
}
到這里不算完事,大家可以看到淘寶頁(yè)會(huì)有兩個(gè)能滑動(dòng)的界面革为,使用的布局中存在著ScrollView扭粱,這樣就會(huì)有滑動(dòng)手勢(shì)沖突,在這里給大家自定義一個(gè)使用的ScrollView 能完美解決這個(gè)問(wèn)題:
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
/**
* Created by ncj on 2018/2/6.
*/
public class DragScrollView extends ScrollView {
private static final int TOUCH_IDLE = 0;
private static final int TOUCH_INNER_CONSIME = 1; // touch事件由ScrollView內(nèi)部消費(fèi)
private static final int TOUCH_DRAG_LAYOUT = 2; // touch事件由上層的DragLayout去消費(fèi)
public static final int TOP_MODEL = 1;
public static final int BOTTOM_MODE = 2;
private int model = 1;
boolean isAtBottom; // 按下的時(shí)候是否在底部
boolean isAtTop; // 按下的時(shí)候是否在頂部
private int mTouchSlop = 4; // 判定為滑動(dòng)的閾值震檩,單位是像素
private int scrollMode;
private float downY;
public DragScrollView(Context arg0) {
this(arg0, null);
}
public DragScrollView(Context arg0, AttributeSet arg1) {
this(arg0, arg1, 0);
}
public DragScrollView(Context arg0, AttributeSet arg1, int arg2) {
super(arg0, arg1, arg2);
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//子View一定要Clickable才行琢蛤,否則onInterceptTouchEvent工作不按正常來(lái)
getChildAt(0).setClickable(true);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downY = ev.getRawY();
isAtBottom = isAtBottom();
isAtTop = isAtTop();
scrollMode = TOUCH_IDLE;
getParent().requestDisallowInterceptTouchEvent(true);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (scrollMode == TOUCH_IDLE) {
float yOffset = downY - ev.getRawY();
float yDistance = Math.abs(yOffset);
if (yDistance > mTouchSlop) {
if (yOffset > 0 && isAtBottom && (model == BOTTOM_MODE)) {
scrollMode = TOUCH_DRAG_LAYOUT;
getParent().requestDisallowInterceptTouchEvent(false);
return true;
} else if (yOffset < 0 && isAtTop && (model == TOP_MODEL)) {
scrollMode = TOUCH_DRAG_LAYOUT;
getParent().requestDisallowInterceptTouchEvent(false);
return true;
} else {
scrollMode = TOUCH_INNER_CONSIME;
}
}
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (scrollMode == TOUCH_DRAG_LAYOUT) {
return false;
}
return super.onTouchEvent(ev);
}
private boolean isAtBottom() {
return getScrollY() + getMeasuredHeight() >= computeVerticalScrollRange() - 2;
}
private boolean isAtTop() {
return getScrollY() == 0;
}
public void setMode(int model) {
this.model = model;
}
}
DragScrollView可以直接在布局中使用,使用方式為:
<com.ncj.mvp.demo.view.DragScrollView
android:id="@+id/drag_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
//你的內(nèi)容
</LinearLayout>
</com.ncj.mvp.demo.view.DragScrollView>
使用的時(shí)候在第一個(gè)Fragment里面添加
@BindView(R.id.drag_scroll_view)
DragScrollView scrollView;
初始化后添加:
scrollView.setMode(DragScrollView.BOTTOM_MODE);
這樣就能解決兩個(gè)上下頁(yè)面有滑動(dòng)事件的沖突問(wèn)題!