对于电商App,商品详情无疑是很重要的一个模块,观察主流购物App的详情界面,发现大部分都是作成了上下两部分,上面展现商品规格信息,下面是H5商品详情,或者是嵌套了一个包含H5详情及评论列表的ViewPager界面,本文就是实现了一个兼容不一样需求的上下滚动黏滞View控件。GitHub连接DragScrollDetailsLayout javascript
首先看一下实现效果图java
固然,若是将Webview替换成其余的ListView之类的也是支持的。git
适用场景:底部须要添加多个界面,而且须要滑动github
适用场景:底部须要添加多个界面,可是不须要滑动web
对于这个需求的场景,很容易想到能够分红上下两部分来实现,只须要一个Vertical的LinearLayout,其他的就是处理滚动及动画的问题,首先自定义ViewGroup内部先声明两个顶层子ViewmUpstairsView、 View mDownstairsView,而且采用一个变量CurrentTargetIndex标记当前处于操做那个View,app
public class DragScrollDetailsLayout extends LinearLayout {
private View mUpstairsView;
private View mDownstairsView;
private View mCurrentTargetView;
public enum CurrentTargetIndex {
UPSTAIRS,
DOWNSTAIRS;
public static CurrentTargetIndex valueOf(int index) {
return 1 == index ? DOWNSTAIRS : UPSTAIRS;
}
}复制代码
而后集中处理滚动事件,对于滚动与动画主要有以下几个问题须要解决:ide
首先来看第一个问题,如何知道上面或者下面的View滚动到了边界,其实Android源码中有个类ViewCompat,它有个函数canScrollVertically(View view, int offSet, MotionEvent ev)就能够判断当前View是否能够向哪一个方向滚动,offset的正负值用来判断向上仍是向下,固然,仅仅靠这个函数仍是不够的,由于ViewGroup是能够相互嵌套的,也许ViewGroup自己不能滚动,可是其内部的子View却能够滚动,这时候,就须要递归遍历相关的View,好比对于ViewPager中嵌套了包含WebView或者List的Fragment。不过,并不是全部的子View都须要遍历,只有与TouchEvent相关的View才须要判断。所以还须要写个函数判断View是否在TouchEvent所在的区域,以下函数isTransformedTouchPointInView:函数
/*** * 判断MotionEvent是否处于View上面 */
protected boolean isTransformedTouchPointInView(MotionEvent ev, View view) {
float x = ev.getRawX();
float y = ev.getRawY();
int[] rect = new int[2];
view.getLocationInWindow(rect);
float localX = x - rect[0];
float localY = y - rect[1];
return localX >= 0 && localX < (view.getRight() - view.getLeft())
&& localY >= 0 && localY < (view.getBottom() - view.getTop());
}复制代码
以后咱们能够利用该函数对View进行递归遍历,判断最上层的ViewGroup是否能够上下滑动布局
private boolean canScrollVertically(View view, int offSet, MotionEvent ev) {
if (!mChildHasScrolled && !isTransformedTouchPointInView(ev, view)) {
return false;
}
if (ViewCompat.canScrollVertically(view, offSet)) {
mChildHasScrolled = true;
return true;
}
if (view instanceof ViewPager) {
return canViewPagerScrollVertically((ViewPager) view, offSet, ev);
}
if (view instanceof ViewGroup) {
ViewGroup vGroup = (ViewGroup) view;
for (int i = 0; i < vGroup.getChildCount(); i++) {
if (canScrollVertically(vGroup.getChildAt(i), offSet, ev)) {
mChildHasScrolled = true;
return true;
}
}
}
return false;
}复制代码
知道View是否能够上下滑动到边界后,拦截事件的时机就比较清晰了,那么接着看第二个问题,如何拦截滑动。post
onInterceptTouchEvent在返回True以后,就不会再执行了,咱们只须要把握准确的拦截时机,好比若是处于上面的View,就要对上拉事件比较敏感,处于底部就要对下拉事件敏感,同时还要将无效的手势归零,好比,操做上面的View时,若是先是下拉,而且是无效的下拉,那么就要将拦截点重置。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownMotionX = ev.getX();
mDownMotionY = ev.getY();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.clear();
mChildHasScrolled=false;
break;
case MotionEvent.ACTION_MOVE:
adjustValidDownPoint(ev);
return checkCanInterceptTouchEvent(ev);
default:
break;
}
return false;
}复制代码
checkCanInterceptTouchEvent主要用来判断是否须要拦截,并不是不可滚动,就须要拦截事件,不可滚动只是一个必要条件而已,
private boolean checkCanInterceptTouchEvent(MotionEvent ev) {
final float xDiff = ev.getX() - mDownMotionX;
final float yDiff = ev.getY() - mDownMotionY;
if (!canChildScrollVertically((int) yDiff,ev)) {
mInitialInterceptY = (int) ev.getY();
if (Math.abs(yDiff) > mTouchSlop && Math.abs(yDiff) >= Math.abs(xDiff)
&& !(mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS && yDiff > 0
|| mCurrentViewIndex == CurrentTargetIndex.DOWNSTAIRS && yDiff < 0)) {
return true;
}
}
return false;
} 复制代码
事件拦截以后,就是对Move事件进行处理
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
flingToFinishScroll();
recycleVelocityTracker();
break;
case MotionEvent.ACTION_MOVE:
scroll(ev);
break;
default:
break;
}
return true;
}复制代码
滚动比较简单,直接调用scrollTo就能够,同时为了收集滚动速度,还能够用VelocityTracker作一下记录:
private void scroll(MotionEvent event) {
if (mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS) {
if (getScrollY() <= 0 && event.getY() >= mInitialInterceptY) {
mInitialInterceptY = (int) event.getY();
}
int distance = mInitialInterceptY - event.getY() >= 0 ? (int) (mInitialInterceptY - event.getY()) : 0;
scrollTo(0, distance);
} else {
if (getScrollY() >= mUpstairsView.getMeasuredHeight() && event.getY() <= mInitialInterceptY) {
mInitialInterceptY = (int) event.getY();
}
int distance = event.getY() <= mInitialInterceptY ? mUpstairsView.getMeasuredHeight()
: (int) (mInitialInterceptY - event.getY() + mUpstairsView.getMeasuredHeight());
scrollTo(0, distance);
}
mVelocityTracker.addMovement(event);
} 复制代码
在Up事件以后,还要简单的处理一下一下收尾的滚动动画,好比,滚动距离不够要复原,不然,就滚动到目标视图,这里主要是根据Up事件的位置,计算须要滚动的距离,并经过Scroller来完成剩下的滚动。
private void flingToFinishScroll() {
final int pHeight = mUpstairsView.getMeasuredHeight();
final int threshold = (int) (pHeight * mPercent);
float needFlingDistance = 0;
if (CurrentTargetIndex.UPSTAIRS == mCurrentViewIndex) {
if (getScrollY() <= 0) {
needFlingDistance = 0;
} else if (getScrollY() <= threshold) {
if (needFlingToToggleView()) {
needFlingDistance = pHeight - getScrollY();
mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
} else {
needFlingDistance = -getScrollY();
}
} else {
needFlingDistance = pHeight - getScrollY();
mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
}
} else if (CurrentTargetIndex.DOWNSTAIRS == mCurrentViewIndex) {
if (pHeight <= getScrollY()) {
needFlingDistance = 0;
} else if (pHeight - getScrollY() < threshold) {
if (needFlingToToggleView()) {
needFlingDistance = -getScrollY();
mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
} else {
needFlingDistance = pHeight - getScrollY();
}
} else {
needFlingDistance = -getScrollY();
mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
}
}
mScroller.startScroll(0, getScrollY(), 0, (int) needFlingDistance, mDuration);
if (mOnSlideDetailsListener != null) {
mOnSlideDetailsListener.onStatueChanged(mCurrentViewIndex);
}
postInvalidate();
}复制代码
以上就是经常使用商品详情黏滞布局的实现。最后附上GitHub连接 欢迎 star DragScrollDetailsLayout GitHub连接