效果如下:
device-2017-12-04-170020.gif
分為兩部分:1. View的創(chuàng)建哀卫。 2. 滑動事件處理
1. View的創(chuàng)建
從頁面上看澜沟,主要分為上下兩部分,上部為滾動的Webview晋控,底部為拉出來的CloseView
我這里自定義了ViewGrup汞窗。初始狀態(tài)Webview撐滿整個屏幕,CloseView不可見赡译,位于Webview底部仲吏。代碼如下
public class PullupCloseLayout extends ViewGroup {
public final static int SIZE_DEFAULT_HEIGHT = 100;
// 手勢滑動view
private View mTarget;
//底部上拉關閉view
private ViewGroup mPullUpView;
//滑動關閉頁面的最大高度
private int mPullUpViewMaxHeight;
public PullupCloseLayout(Context context) {
this(context, null);
}
public PullupCloseLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
//為底部CloseView
mPullUpView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.pull_up_close, this);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mPullUpViewMaxHeight = (int) (SIZE_DEFAULT_HEIGHT * metrics.density);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (mTarget == null) {
ensureView();
}
if (mTarget == null) {
return;
}
//WebView撐滿屏幕
mTarget.layout(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom());
//CloseView在 Webview底部
mPullUpView.layout(0, height - getPaddingBottom(), width, height - getPaddingBottom() + mPullUpView.getMeasuredHeight());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureView();
}
if (mTarget == null) {
return;
}
//設置Webview的高度撐滿全屏
mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//設置CloseView 為固定高度
mPullUpView.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mPullUpViewMaxHeight, MeasureSpec.EXACTLY));
}
//初始化內部滾動view, 參考v4 SwipRefreshLayout
private void ensureView() {
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mPullUpView)) {
mTarget = child;
break;
}
}
}
}
}
此時頁面布局完成蝌焚。接下來第二部處理滑動事件
2. 滑動事件處理
滑動事件主要處理兩個狀態(tài)裹唆, 1. 滑動到底部,可以隨手勢上滑只洒,松手可回彈许帐。 2. 可以隨慣性滑動并回彈
- 手勢上滑及回彈。
判斷當頁面滑到底部不能繼續(xù)滑動的時候由本布局攔截手勢红碑, 并消費掉舞吭。 否則不了攔截。
如何判斷滑動到底部析珊?
private boolean canChildScrollUp() {
// 參數(shù)為正則代表向上是否可滑動羡鸥,負數(shù)則為向下, 一般用1和-1代表
return ViewCompat.canScrollVertically(mTarget, 1);
}
攔截滾動事件的代碼如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (canChildScrollUp() || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
//記錄按下的位置
mInitialDownY = initialDownY;
break;
case MotionEvent.ACTION_MOVE:
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
//判斷滾動的距離
final float yDiff = mInitialDownY - y;
//如果滾動距離>自定義的閾值忠寻,則認為需要跟隨手勢滾動了惧浴,此時開始攔截。
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
}
break;
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = -1;
break;
}
return mIsBeingDragged;
}
消費手勢 如下
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (canChildScrollUp()) {
return false;
}
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex;
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
//設置滾動的阻力 0.5倍系數(shù)
final int overscrollTop = (int) ((mInitialMotionY - y) * 0.5);
if (mIsBeingDragged) {//消費滑動事件
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
Log.i(TAG, "ACTION_UP");
break;
case MotionEvent.ACTION_UP:
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
return false;
}
mIsBeingDragged = false;
mActivePointerId = -1;
finishSpinner();
Log.i(TAG, "ACTION_UP");
break;
}
return true;
}
// 手勢移動奕剃,滾動當前view衷旅,并切換底部關閉按鈕的狀態(tài)
private void moveSpinner(int overscrollTop) {
scrollBy(0, overscrollTop - getScrollY());
updatePullUpViewState();
}
//手勢抬起,開始回彈動畫并回調是否關閉頁面
private void finishSpinner() {
if (getScrollY() > 0) {
scrollBackAnimator(getScrollY());
}
//上拉回調纵朋。
if (mPullUpListener != null) {
mPullUpListener.pullUp(mCanClose);
}
}
至此已經(jīng)實現(xiàn)隨手勢上滑并回彈柿顶,效果如下
device-2017-12-04-164045.gif
- 慣性和回彈效果。
搜索了很多資料都沒有特別好用的回彈效果操软。 這里通過WebView 的 overScrollBy 的回調方法拿到Webview滾動到底部時候可繼續(xù)滾動的距離嘁锯,并在此時增加一個繼續(xù)滑動的動效,模擬慣性
首先要在PullupCloseLayout中拿到 該回調聂薪。 這里定義了一個監(jiān)聽器
public class MyWebview extends WebView {
public MyWebview(Context context) {
super(context);
}
public MyWebview(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
if (mOverscrollListener != null) {
mOverscrollListener.overScroll(deltaX, deltaY,isTouchEvent);
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
private PullUpOverScrollListerer mOverscrollListener;
public void registerOverscrollListener (PullUpOverScrollListerer listener) {
if (listener != null) {
mOverscrollListener = listener;
}
}
public void unRegisterOverscrollListener () {
mOverscrollListener = null;
}
}
這樣在PullupcloseLayout中
private void ensureView() {
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mPullUpView)) {
mTarget = child;
//判斷滾動的view為自己的實現(xiàn)了onScrollBy方法的 webview則注冊該監(jiān)聽
if (mTarget instanceof MyWebview) {
MyWebview webView = (MyWebview) mTarget;
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);//去掉滑到底部的反饋水紋
webView.registerOverscrollListener(this);
}
break;
}
}
}
}
......
@Override
public void overScroll(int deltaX, int deltaY, boolean isTouchEvent) {
if (!mIsBeingDragged && !canChildScrollUp() && deltaY > mTouchSlop && mCurrentMotionEvent != MotionEvent.ACTION_MOVE) {
//1.5 倍慣性距離, 且最大滾動距離為滑動關閉的閾值
deltaY = Math.min((int)(deltaY * 1.5), mPullUpCloseHeight);
scrollBackAnimator((int) (deltaY * 1.5));
}
}
.....
//回彈動畫
private void scrollBackAnimator(final int y) {
Log.i(TAG, "scrollBackAnimator y =" + y);
if (y == 0) {
return;
}
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
mAnimator = ValueAnimator.ofFloat(0, 1);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float f = (float) animation.getAnimatedValue();
scrollTo(0, (int) (y * (1 - f)));
}
});
//long duration = SCROLL_MAX_DURATION_MS * y / mPullUpViewMaxHeight;
mAnimator.setDuration(SCROLL_MAX_DURATION_MS);
mAnimator.start();
}
至此實現(xiàn)了慣性回彈家乘。 效果如下device-2017-12-04-165546.gif
附github地址:
PullupCloseLayout