实现淘宝详情页上下两个界面弹性切换。在此做个笔记,可以直接复制使用。
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;
/**
* 这是一个viewGroup容器,实现上下两个frameLayout拖动切换
*
* @author ncj
*/
@SuppressLint("NewApi")
public class DragLayout extends ViewGroup {
/* 拖拽工具类 */
private final ViewDragHelper mDragHelper;
private GestureDetectorCompat gestureDetector;
/* 上下两个frameLayout,在Activity中注入fragment */
private View frameView1, frameView2;
private int viewHeight;
private static final int VEL_THRESHOLD = 100; // 滑动速度的阈值,超过这个绝对值认为是上下
private static final int DISTANCE_THRESHOLD = 100; // 单位是像素,当上下滑动速度不够时,通过这个 阈 值来判定是应该粘到顶部还是底部
private int downTop1; // 手指按下的时候,frameView1的getTop值
private ShowNextPageNotifier nextPageListener; // 手指松开是否加载下一页的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一样,初始化上下两个view
frameView1 = getChildAt(0);
frameView2 = getChildAt(1);
}
class YScrollDetector extends SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
float dy) {
// 垂直滑动时dy>dx,才被认定是上下拖动
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;
}
// 一个view位置改变,另一个view的位置要跟进
onViewPosChanged(childIndex, top);
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 两个子View都需要跟踪,返回true
return true;
}
@Override
public int getViewVerticalDragRange(View child) {
// 这个用来控制拖拽过程中松手后,自动滑行的速度,暂时给一个随意的数值
return 1;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 滑动松开后,需要向上或者乡下粘到特定的位置
animTopOrBottom(releasedChild, yvel);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int finalTop = top;
if (child == frameView1) {
// 拖动的时第一个view
if (top > 0) {
// 不让第一个view往下拖,因为顶部会白板
finalTop = 0;
}
} else if (child == frameView2) {
// 拖动的时第二个view
if (top < 0) {
// 不让第二个view网上拖,因为底部会白板
finalTop = 0;
}
}
// finalTop代表的是理论上应该拖动到的位置。此处计算拖动的距离除以一个参数(3),是让滑动的速度变慢。数值越大,滑动的越慢
return child.getTop() + (finalTop - child.getTop()) / 3;
}
}
/**
* 滑动时view位置改变协调处理
*
* @param viewIndex
* 滑动view的index(1或2)
* @param posTop
* 滑动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);
}
// 有的时候会默认白板,这个很恶心。后面有时间再优化
invalidate();
}
private void animTopOrBottom(View releasedChild, float yvel) {
int finalTop = 0; // 默认是粘到最顶端
if (releasedChild == frameView1) {
// 拖动第一个view松手
if (yvel < -VEL_THRESHOLD
|| (downTop1 == 0 && frameView1.getTop() < -DISTANCE_THRESHOLD)) {
// 向上的速度足够大,就滑动到顶端
// 向上滑动的距离超过某个阈值,就滑动到顶端
finalTop = -viewHeight;
// 下一页可以初始化了
if (null != nextPageListener) {
nextPageListener.onDragNext();
}
}
} else {
// 拖动第二个view松手
if (yvel > VEL_THRESHOLD
|| (downTop1 == -viewHeight && releasedChild.getTop() > DISTANCE_THRESHOLD)) {
// 保持原地不动
finalTop = viewHeight;
}
}
if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
/* touch事件的拦截与处理都交给mDraghelper来处理 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (frameView1.getBottom() > 0 && frameView1.getTop() < 0) {
// view粘到顶部或底部,正在动画中的时候,不处理touch事件
return false;
}
boolean yScroll = gestureDetector.onTouchEvent(ev);
boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// action_down时就让mDragHelper开始工作,否则有时候导致异常 他大爷的
mDragHelper.processTouchEvent(ev);
downTop1 = frameView1.getTop();
}
return shouldIntercept && yScroll;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
// 统一交给mDragHelper处理,由DragHelperCallback实现拖动效果
mDragHelper.processTouchEvent(e); // 该行代码可能会抛异常,正式发布时请将这行代码加上try catch
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 只在初始化的时候调用
// 一些参数作为全局变量保存起来
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)的操作系统,所以手动复制过来以免强制退出
*/
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();
}
}
布局中使用:
在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() {
}
}
到这里不算完事,大家可以看到淘宝页会有两个能滑动的界面,使用的布局中存在着ScrollView,这样就会有滑动手势冲突,在这里给大家自定义一个使用的ScrollView 能完美解决这个问题:
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内部消费
private static final int TOUCH_DRAG_LAYOUT = 2; // touch事件由上层的DragLayout去消费
public static final int TOP_MODEL = 1;
public static final int BOTTOM_MODE = 2;
private int model = 1;
boolean isAtBottom; // 按下的时候是否在底部
boolean isAtTop; // 按下的时候是否在顶部
private int mTouchSlop = 4; // 判定为滑动的阈值,单位是像素
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工作不按正常来
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可以直接在布局中使用,使用方式为:
//你的内容
使用的时候在第一个Fragment里面添加
@BindView(R.id.drag_scroll_view)
DragScrollView scrollView;
初始化后添加:
scrollView.setMode(DragScrollView.BOTTOM_MODE);
这样就能解决两个上下页面有滑动事件的冲突问题!