/* * * * sufly0001@gmail.com Modify the code to enhance the ease of use. * * * * Copyright (C) 2015 Ted xiong-wei@hotmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * * limitations under the License. * * */ package com.hpw.myapp.widget.scrolllayout; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.Scroller; import com.hpw.myapp.R; import com.hpw.myapp.widget.scrolllayout.content.ContentScrollView; /** * Layout that can scroll down to a max offset and can tell the scroll progress by * OnScrollProgressListener. */ public class ScrollLayout extends FrameLayout { private static final int MAX_SCROLL_DURATION = 400; private static final int MIN_SCROLL_DURATION = 100; private static final int FLING_VELOCITY_SLOP = 80; private static final float DRAG_SPEED_MULTIPLIER = 1.2f; private static final int DRAG_SPEED_SLOP = 30; private static final int MOTION_DISTANCE_SLOP = 10; private static final float SCROLL_TO_CLOSE_OFFSET_FACTOR = 0.5f; private static final float SCROLL_TO_EXIT_OFFSET_FACTOR = 0.8f; private final GestureDetector.OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (velocityY > FLING_VELOCITY_SLOP) { if (lastFlingStatus.equals(Status.OPENED) && -getScrollY() > maxOffset) { lastFlingStatus = Status.EXIT; scrollToExit(); } else { scrollToOpen(); lastFlingStatus = Status.OPENED; } return true; } else if (velocityY < FLING_VELOCITY_SLOP && getScrollY() <= -maxOffset) { scrollToOpen(); lastFlingStatus = Status.OPENED; return true; } else if (velocityY < FLING_VELOCITY_SLOP && getScrollY() > -maxOffset) { scrollToClose(); lastFlingStatus = Status.CLOSED; return true; } return false; } }; private final AbsListView.OnScrollListener associatedListViewListener = new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { updateListViewScrollState(view); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { updateListViewScrollState(view); } }; private float lastX; private float lastY; private float lastDownX; private float lastDownY; private Status lastFlingStatus = Status.CLOSED; private Scroller scroller; private GestureDetector gestureDetector; private boolean isEnable = true; private boolean isSupportExit = false; private boolean isAllowHorizontalScroll = true; private boolean isDraggable = true; private boolean isAllowPointerIntercepted = true; private boolean isCurrentPointerIntercepted = false; private InnerStatus currentInnerStatus = InnerStatus.OPENED; private int maxOffset = 0; private int minOffset = 0; private int exitOffset = 0; private OnScrollChangedListener onScrollChangedListener; private ContentScrollView mScrollView; public ScrollLayout(Context context) { super(context); } public ScrollLayout(Context context, AttributeSet attrs) { super(context, attrs); initFromAttributes(context, attrs); } public ScrollLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initFromAttributes(context, attrs); } { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { scroller = new Scroller(getContext(), null, true); } else { scroller = new Scroller(getContext()); } gestureDetector = new GestureDetector(getContext(), gestureListener); } private void initFromAttributes(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollLayout); if (a.hasValue(R.styleable.ScrollLayout_maxOffset)) { int maxset = a.getDimensionPixelOffset(R.styleable.ScrollLayout_maxOffset, maxOffset); if (maxset != getScreenHeight()) { maxOffset = getScreenHeight() - maxset; } } if (a.hasValue(R.styleable.ScrollLayout_minOffset)) minOffset = a.getDimensionPixelOffset(R.styleable.ScrollLayout_minOffset, minOffset); if (a.hasValue(R.styleable.ScrollLayout_exitOffset)) { int exitset = a.getDimensionPixelOffset(R.styleable.ScrollLayout_exitOffset, getScreenHeight()); if (exitset != getScreenHeight()) { exitOffset = getScreenHeight() - exitset; } } if (a.hasValue(R.styleable.ScrollLayout_allowHorizontalScroll)) isAllowHorizontalScroll = a.getBoolean(R.styleable.ScrollLayout_allowHorizontalScroll, true); if (a.hasValue(R.styleable.ScrollLayout_isSupportExit)) isSupportExit = a.getBoolean(R.styleable.ScrollLayout_isSupportExit, true); if (a.hasValue(R.styleable.ScrollLayout_mode)) { int mode = a.getInteger(R.styleable.ScrollLayout_mode, 0); switch (mode) { case 0x0: setToOpen(); break; case 0x1: setToClosed(); break; case 0x2: setToExit(); break; default: setToClosed(); break; } } a.recycle(); } private ContentScrollView.OnScrollChangedListener mOnScrollChangedListener = new ContentScrollView.OnScrollChangedListener() { @Override public void onScrollChanged(int l, int t, int oldL, int oldT) { if (null == mScrollView) return; if (null != onScrollChangedListener) onScrollChangedListener.onChildScroll(oldT); if (mScrollView.getScrollY() == 0) { setDraggable(true); } else { setDraggable(false); } } }; /** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * * @param x the x position to scroll to * @param y the y position to scroll to */ @Override public void scrollTo(int x, int y) { super.scrollTo(x, y); if (maxOffset == minOffset) { return; } //only from min to max or from max to min,send progress out. not exit if (-y <= maxOffset) { float progress = (float) (-y - minOffset) / (maxOffset - minOffset); onScrollProgressChanged(progress); } else { float progress = (float) (-y - maxOffset) / (maxOffset - exitOffset); onScrollProgressChanged(progress); } if (y == -minOffset) { // closed if (currentInnerStatus != InnerStatus.CLOSED) { currentInnerStatus = InnerStatus.CLOSED; onScrollFinished(Status.CLOSED); } } else if (y == -maxOffset) { // opened if (currentInnerStatus != InnerStatus.OPENED) { currentInnerStatus = InnerStatus.OPENED; onScrollFinished(Status.OPENED); } } else if (isSupportExit && y == -exitOffset) { // exited if (currentInnerStatus != InnerStatus.EXIT) { currentInnerStatus = InnerStatus.EXIT; onScrollFinished(Status.EXIT); } } } private void onScrollFinished(Status status) { if (onScrollChangedListener != null) { onScrollChangedListener.onScrollFinished(status); } } private void onScrollProgressChanged(float progress) { if (onScrollChangedListener != null) { onScrollChangedListener.onScrollProgressChanged(progress); } } @Override public void computeScroll() { if (!scroller.isFinished() && scroller.computeScrollOffset()) { int currY = scroller.getCurrY(); scrollTo(0, currY); if (currY == -minOffset || currY == -maxOffset || (isSupportExit && currY == -exitOffset)) { scroller.abortAnimation(); } else { invalidate(); } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isEnable) { return false; } if (!isDraggable && currentInnerStatus == InnerStatus.CLOSED) { return false; } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: lastX = ev.getX(); lastY = ev.getY(); lastDownX = lastX; lastDownY = lastY; isAllowPointerIntercepted = true; isCurrentPointerIntercepted = false; if (!scroller.isFinished()) { scroller.forceFinished(true); currentInnerStatus = InnerStatus.MOVING; isCurrentPointerIntercepted = true; return true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isAllowPointerIntercepted = true; isCurrentPointerIntercepted = false; if (currentInnerStatus == InnerStatus.MOVING) { return true; } break; case MotionEvent.ACTION_MOVE: if (!isAllowPointerIntercepted) { return false; } if (isCurrentPointerIntercepted) { return true; } int deltaY = (int) (ev.getY() - lastDownY); int deltaX = (int) (ev.getX() - lastDownX); if (Math.abs(deltaY) < MOTION_DISTANCE_SLOP) { return false; } if (Math.abs(deltaY) < Math.abs(deltaX)) { // horizontal event if (isAllowHorizontalScroll) { isAllowPointerIntercepted = false; isCurrentPointerIntercepted = false; return false; } } if (currentInnerStatus == InnerStatus.CLOSED) { // when closed, only handle downwards motion event if (deltaY < 0) { // upwards return false; } } else if (currentInnerStatus == InnerStatus.OPENED && !isSupportExit) { // when opened, only handle upwards motion event if (deltaY > 0) { // downwards return false; } } isCurrentPointerIntercepted = true; return true; default: return false; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { if (!isCurrentPointerIntercepted) { return false; } gestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastY = event.getY(); return true; case MotionEvent.ACTION_MOVE: int deltaY = (int) ((event.getY() - lastY) * DRAG_SPEED_MULTIPLIER); deltaY = (int) (Math.signum(deltaY)) * Math.min(Math.abs(deltaY), DRAG_SPEED_SLOP); if (disposeEdgeValue(deltaY)) { return true; } currentInnerStatus = InnerStatus.MOVING; int toScrollY = getScrollY() - deltaY; if (toScrollY >= -minOffset) { scrollTo(0, -minOffset); } else if (toScrollY <= -maxOffset && !isSupportExit) { scrollTo(0, -maxOffset); } else { scrollTo(0, toScrollY); } lastY = event.getY(); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (currentInnerStatus == InnerStatus.MOVING) { completeMove(); return true; } break; default: return false; } return false; } private boolean disposeEdgeValue(int deltaY) { if (isSupportExit) { if (deltaY <= 0 && getScrollY() >= -minOffset) { return true; } else if (deltaY >= 0 && getScrollY() <= -exitOffset) { return true; } } else { if (deltaY <= 0 && getScrollY() >= -minOffset) { return true; } else if (deltaY >= 0 && getScrollY() <= -maxOffset) { return true; } } return false; } private void completeMove() { float closeValue = -((maxOffset - minOffset) * SCROLL_TO_CLOSE_OFFSET_FACTOR); if (getScrollY() > closeValue) { scrollToClose(); } else { if (isSupportExit) { float exitValue = -((exitOffset - maxOffset) * SCROLL_TO_EXIT_OFFSET_FACTOR + maxOffset); if (getScrollY() <= closeValue && getScrollY() > exitValue) { scrollToOpen(); } else { scrollToExit(); } } else { scrollToOpen(); } } } /** * 滚动布局打开关闭,关闭否则滚动. */ public void showOrHide() { if (currentInnerStatus == InnerStatus.OPENED) { scrollToClose(); } else if (currentInnerStatus == InnerStatus.CLOSED) { scrollToOpen(); } } /** * 滚动布局开放,maxOffset之后向下滚动. */ public void scrollToOpen() { if (currentInnerStatus == InnerStatus.OPENED) { return; } if (maxOffset == minOffset) { return; } int dy = -getScrollY() - maxOffset; if (dy == 0) { return; } currentInnerStatus = InnerStatus.SCROLLING; int duration = MIN_SCROLL_DURATION + Math.abs((MAX_SCROLL_DURATION - MIN_SCROLL_DURATION) * dy / (maxOffset - minOffset)); scroller.startScroll(0, getScrollY(), 0, dy, duration); invalidate(); } /** * 滚动的布局来关闭,滚动到minOffset. */ public void scrollToClose() { if (currentInnerStatus == InnerStatus.CLOSED) { return; } if (maxOffset == minOffset) { return; } int dy = -getScrollY() - minOffset; if (dy == 0) { return; } currentInnerStatus = InnerStatus.SCROLLING; int duration = MIN_SCROLL_DURATION + Math.abs((MAX_SCROLL_DURATION - MIN_SCROLL_DURATION) * dy / (maxOffset - minOffset)); scroller.startScroll(0, getScrollY(), 0, dy, duration); invalidate(); } /** * 滚动布局退出 */ public void scrollToExit() { if (!isSupportExit) return; if (currentInnerStatus == InnerStatus.EXIT) { return; } if (exitOffset == maxOffset) { return; } int dy = -getScrollY() - exitOffset; if (dy == 0) { return; } currentInnerStatus = InnerStatus.SCROLLING; int duration = MIN_SCROLL_DURATION + Math.abs((MAX_SCROLL_DURATION - MIN_SCROLL_DURATION) * dy / (exitOffset - maxOffset)); scroller.startScroll(0, getScrollY(), 0, dy, duration); invalidate(); } /** * 初始化布局开放,没有动画。 */ public void setToOpen() { scrollTo(0, -maxOffset); currentInnerStatus = InnerStatus.OPENED; lastFlingStatus = Status.OPENED; } /** * 初始化布局关闭,没有动画。 */ public void setToClosed() { scrollTo(0, -minOffset); currentInnerStatus = InnerStatus.CLOSED; lastFlingStatus = Status.CLOSED; } /** * 初始化布局,退出,没有动画。 */ public void setToExit() { if (!isSupportExit) return; scrollTo(0, -exitOffset); currentInnerStatus = InnerStatus.EXIT; } public void setMinOffset(int minOffset) { this.minOffset = minOffset; } public void setMaxOffset(int maxOffset) { this.maxOffset = getScreenHeight() - maxOffset; } public void setExitOffset(int exitOffset) { this.exitOffset = getScreenHeight() - exitOffset; } public void setEnable(boolean enable) { this.isEnable = enable; } public void setIsSupportExit(boolean isSupportExit) { this.isSupportExit = isSupportExit; } public boolean isSupportExit() { return isSupportExit; } public boolean isAllowHorizontalScroll() { return isAllowHorizontalScroll; } public void setAllowHorizontalScroll(boolean isAllowed) { isAllowHorizontalScroll = isAllowed; } public boolean isDraggable() { return isDraggable; } public void setDraggable(boolean draggable) { this.isDraggable = draggable; } public void setOnScrollChangedListener(OnScrollChangedListener listener) { this.onScrollChangedListener = listener; } public Status getCurrentStatus() { switch (currentInnerStatus) { case CLOSED: return Status.CLOSED; case OPENED: return Status.OPENED; case EXIT: return Status.EXIT; default: return Status.OPENED; } } /** * Set associated list view, then this layout will only be able to drag down when the list * view is scrolled to top. * * @param listView */ public void setAssociatedListView(AbsListView listView) { listView.setOnScrollListener(associatedListViewListener); updateListViewScrollState(listView); } private void updateListViewScrollState(AbsListView listView) { if (listView.getChildCount() == 0) { setDraggable(true); } else { if (listView.getFirstVisiblePosition() == 0) { View firstChild = listView.getChildAt(0); if (firstChild.getTop() == listView.getPaddingTop()) { setDraggable(true); return; } } setDraggable(false); } } public void setAssociatedScrollView(ContentScrollView scrollView) { this.mScrollView = scrollView; this.mScrollView.setScrollbarFadingEnabled(false); this.mScrollView.setOnScrollChangeListener(mOnScrollChangedListener); } private enum InnerStatus { EXIT, OPENED, CLOSED, MOVING, SCROLLING } /** * 获取屏幕内容高度 * * @return */ public int getScreenHeight() { DisplayMetrics dm = new DisplayMetrics(); ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(dm); int result = 0; int resourceId = getContext().getResources() .getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = getContext().getResources().getDimensionPixelSize(resourceId); } int screenHeight = dm.heightPixels - result; return screenHeight; } /** * 表明Scrolllayout的状态,只可以打开或关闭。 */ public enum Status { EXIT, OPENED, CLOSED } /** * 注册这个Scrolllayout可以监控其滚动 */ public interface OnScrollChangedListener { /** * 每次滚动改变值 * * @param currentProgress 0 to 1, 1 to -1, 0 means close, 1 means open, -1 means exit. */ void onScrollProgressChanged(float currentProgress); /** * 滚动状态改变时调用的方法 * * @param currentStatus the current status after change */ void onScrollFinished(Status currentStatus); /*** * 滚动子视图 * * @param top the child view scroll data */ void onChildScroll(int top); } }