Here are the examples of the java api android.widget.OverScroller taken from open source projects. By voting up you can indicate which examples are most useful and appropriate.
119 Examples
19
Source : OverScrollLayout.java
with Apache License 2.0
from zuoweitan
with Apache License 2.0
from zuoweitan
/**
* copy from wcy10586/OverscrollLayout and thanks for this
*/
public clreplaced OverScrollLayout extends RelativeLayout {
private static final String TAG = "OverScrollLayout";
private ViewConfiguration configuration;
private View child;
private float downY;
private float oldY;
private int dealtY;
private Scroller mScroller;
private float downX;
private float oldX;
private int dealtX;
private boolean isVerticalMove;
private boolean isHorizontallyMove;
private boolean isOverScrollTop;
private boolean isOverScrollBottom;
private boolean isOverScrollLeft;
private boolean isOverScrollRight;
private boolean checkScrollDirectionFinish;
private boolean canOverScrollHorizontally;
private boolean canOverScrollVertical;
private float baseOverScrollLength;
private boolean topOverScrollEnable = true;
private boolean bottomOverScrollEnable = true;
private boolean leftOverScrollEnable = true;
private boolean rightOverScrollEnable = true;
private OnOverScrollListener onOverScrollListener;
private OverScrollCheckListener checkListener;
public static int SCROLL_VERTICAL = LinearLayout.VERTICAL;
public static int SCROLL_HORIZONTAL = LinearLayout.HORIZONTAL;
private float fraction = 0.5f;
private boolean finishOverScroll;
private boolean abortScroller;
private boolean shouldSetScrollerStart;
private boolean disallowIntercept;
private GestureDetector detector;
private FlingRunnable flingRunnable;
private OverScroller flingScroller;
private OverScrollRunnable overScrollRunnable;
public OverScrollLayout(Context context) {
super(context);
init();
}
public OverScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@SuppressWarnings("NewApi")
public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
configuration = ViewConfiguration.get(getContext());
mScroller = new Scroller(getContext(), new OvershootInterpolator(0.75f));
flingRunnable = new FlingRunnable();
overScrollRunnable = new OverScrollRunnable();
flingScroller = new OverScroller(getContext());
detector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isOverScrollTop || isOverScrollBottom || isOverScrollLeft || isOverScrollRight) {
return false;
}
//
flingRunnable.start(velocityX, velocityY);
return false;
}
});
}
@Override
protected void onFinishInflate() {
int childCount = getChildCount();
if (childCount > 1) {
throw new IllegalStateException("OverScrollLayout only can host 1 element");
} else if (childCount == 1) {
child = getChildAt(0);
child.setOverScrollMode(OVER_SCROLL_NEVER);
}
super.onFinishInflate();
}
public void setDisallowInterceptTouchEvent(boolean disallowIntercept) {
this.disallowIntercept = disallowIntercept;
}
public boolean isTopOverScrollEnable() {
return topOverScrollEnable;
}
/**
* @param topOverScrollEnable true can over scroll top false otherwise
*/
public void setTopOverScrollEnable(boolean topOverScrollEnable) {
this.topOverScrollEnable = topOverScrollEnable;
}
public boolean isBottomOverScrollEnable() {
return bottomOverScrollEnable;
}
/**
* @param bottomOverScrollEnable true can over scroll bottom false otherwise
*/
public void setBottomOverScrollEnable(boolean bottomOverScrollEnable) {
this.bottomOverScrollEnable = bottomOverScrollEnable;
}
public boolean isLeftOverScrollEnable() {
return leftOverScrollEnable;
}
/**
* @param leftOverScrollEnable true can over scroll left false otherwise
*/
public void setLeftOverScrollEnable(boolean leftOverScrollEnable) {
this.leftOverScrollEnable = leftOverScrollEnable;
}
public boolean isRightOverScrollEnable() {
return rightOverScrollEnable;
}
/**
* @param rightOverScrollEnable true can over scroll right false otherwise
*/
public void setRightOverScrollEnable(boolean rightOverScrollEnable) {
this.rightOverScrollEnable = rightOverScrollEnable;
}
public OnOverScrollListener getOnOverScrollListener() {
return onOverScrollListener;
}
/**
* @param onOverScrollListener
*/
public void setOnOverScrollListener(OnOverScrollListener onOverScrollListener) {
this.onOverScrollListener = onOverScrollListener;
}
public OverScrollCheckListener getOverScrollCheckListener() {
return checkListener;
}
/**
* @param checkListener for custom view check over scroll
*/
public void setOverScrollCheckListener(OverScrollCheckListener checkListener) {
this.checkListener = checkListener;
}
public float getFraction() {
return fraction;
}
/**
* @param fraction the fraction for over scroll.it is num[0f,1f],
*/
public void setFraction(float fraction) {
if (fraction < 0 || fraction > 1) {
return;
}
this.fraction = fraction;
}
private void checkCanOverScrollDirection() {
if (checkScrollDirectionFinish) {
return;
}
if (checkListener != null) {
int mOrientation = checkListener.getContentViewScrollDirection();
canOverScrollHorizontally = RecyclerView.HORIZONTAL == mOrientation;
canOverScrollVertical = RecyclerView.VERTICAL == mOrientation;
} else if (child instanceof AbsListView || child instanceof ScrollView || child instanceof WebView) {
canOverScrollHorizontally = false;
canOverScrollVertical = true;
} else if (child instanceof RecyclerView) {
RecyclerView recyclerView = (RecyclerView) child;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int mOrientation = -1;
if (layoutManager instanceof StaggeredGridLayoutManager) {
mOrientation = ((StaggeredGridLayoutManager) layoutManager).getOrientation();
} else if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
mOrientation = manager.getOrientation();
}
canOverScrollHorizontally = RecyclerView.HORIZONTAL == mOrientation;
canOverScrollVertical = RecyclerView.VERTICAL == mOrientation;
} else if (child instanceof HorizontalScrollView) {
canOverScrollHorizontally = true;
canOverScrollVertical = false;
} else if (child instanceof ViewPager) {
// forbid ViewPager over scroll
canOverScrollHorizontally = false;
canOverScrollVertical = false;
} else {
canOverScrollHorizontally = false;
canOverScrollVertical = true;
}
checkScrollDirectionFinish = true;
if (canOverScrollVertical) {
baseOverScrollLength = getHeight();
} else {
baseOverScrollLength = getWidth();
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int scrollerY = mScroller.getCurrY();
scrollTo(mScroller.getCurrX(), scrollerY);
postInvalidate();
} else {
if (abortScroller) {
abortScroller = false;
return;
}
if (finishOverScroll) {
isOverScrollTop = false;
isOverScrollBottom = false;
isOverScrollLeft = false;
isOverScrollRight = false;
finishOverScroll = false;
}
}
}
protected void mSmoothScrollTo(int fx, int fy) {
int dx = fx - mScroller.getFinalX();
int dy = fy - mScroller.getFinalY();
mSmoothScrollBy(dx, dy);
}
protected void mSmoothScrollBy(int dx, int dy) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
invalidate();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (disallowIntercept) {
return super.dispatchTouchEvent(ev);
}
detector.onTouchEvent(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch(action) {
case MotionEvent.ACTION_POINTER_DOWN:
oldY = 0;
oldX = 0;
break;
case MotionEvent.ACTION_DOWN:
flingRunnable.abort();
downY = ev.getY();
oldY = 0;
dealtY = mScroller.getCurrY();
if (dealtY == 0) {
isVerticalMove = false;
} else {
shouldSetScrollerStart = true;
abortScroller = true;
mScroller.abortAnimation();
}
downX = ev.getX();
oldX = 0;
dealtX = mScroller.getCurrX();
if (dealtX == 0) {
isHorizontallyMove = false;
} else {
shouldSetScrollerStart = true;
abortScroller = true;
mScroller.abortAnimation();
}
if (isOverScrollTop || isOverScrollBottom || isOverScrollLeft || isOverScrollRight) {
return true;
}
checkCanOverScrollDirection();
break;
case MotionEvent.ACTION_MOVE:
if (!canOverScroll()) {
return super.dispatchTouchEvent(ev);
}
if (canOverScrollVertical) {
if (isOverScrollTop || isOverScrollBottom) {
if (onOverScrollListener != null) {
if (isOverScrollTop) {
onOverScrollListener.onTopOverScroll();
}
if (isOverScrollBottom) {
onOverScrollListener.onBottomOverScroll();
}
}
if (shouldSetScrollerStart) {
shouldSetScrollerStart = false;
mScroller.startScroll(dealtX, dealtY, 0, 0);
}
if (oldY == 0) {
oldY = ev.getY();
return true;
}
dealtY += getDealt(oldY - ev.getY(), dealtY);
oldY = ev.getY();
if (isOverScrollTop && dealtY > 0) {
dealtY = 0;
}
if (isOverScrollBottom && dealtY < 0) {
dealtY = 0;
}
overScroll(dealtX, dealtY);
if ((isOverScrollTop && dealtY == 0 && !isOverScrollBottom) || (isOverScrollBottom && dealtY == 0 && !isOverScrollTop)) {
oldY = 0;
isOverScrollTop = false;
isOverScrollBottom = false;
if (!isChildCanScrollVertical()) {
return true;
}
return super.dispatchTouchEvent(resetVertical(ev));
}
return true;
} else {
checkMoveDirection(ev.getX(), ev.getY());
if (oldY == 0) {
oldY = ev.getY();
return true;
}
boolean tempOverScrollTop = isTopOverScroll(ev.getY());
if (!isOverScrollTop && tempOverScrollTop) {
oldY = ev.getY();
isOverScrollTop = tempOverScrollTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
return true;
}
isOverScrollTop = tempOverScrollTop;
boolean tempOverScrollBottom = isBottomOverScroll(ev.getY());
if (!isOverScrollBottom && tempOverScrollBottom) {
oldY = ev.getY();
isOverScrollBottom = tempOverScrollBottom;
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
return true;
}
isOverScrollBottom = tempOverScrollBottom;
oldY = ev.getY();
}
} else if (canOverScrollHorizontally) {
if (isOverScrollLeft || isOverScrollRight) {
if (onOverScrollListener != null) {
if (isOverScrollLeft) {
onOverScrollListener.onLeftOverScroll();
}
if (isOverScrollRight) {
onOverScrollListener.onRightOverScroll();
}
}
if (shouldSetScrollerStart) {
shouldSetScrollerStart = false;
mScroller.startScroll(dealtX, dealtY, 0, 0);
}
if (oldX == 0) {
oldX = ev.getX();
return true;
}
dealtX += getDealt(oldX - ev.getX(), dealtX);
oldX = ev.getX();
if (isOverScrollLeft && dealtX > 0) {
dealtX = 0;
}
if (isOverScrollRight && dealtX < 0) {
dealtX = 0;
}
overScroll(dealtX, dealtY);
if ((isOverScrollLeft && dealtX == 0 && !isOverScrollRight) || (isOverScrollRight && dealtX == 0 && !isOverScrollLeft)) {
oldX = 0;
isOverScrollRight = false;
isOverScrollLeft = false;
if (!isChildCanScrollHorizontally()) {
return true;
}
return super.dispatchTouchEvent(resetHorizontally(ev));
}
return true;
} else {
checkMoveDirection(ev.getX(), ev.getY());
if (oldX == 0) {
oldX = ev.getX();
return true;
}
boolean tempOverScrollLeft = isLeftOverScroll(ev.getX());
if (!isOverScrollLeft && tempOverScrollLeft) {
oldX = ev.getX();
isOverScrollLeft = tempOverScrollLeft;
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
return true;
}
isOverScrollLeft = tempOverScrollLeft;
boolean tempOverScrollRight = isRightOverScroll(ev.getX());
if (!isOverScrollRight && tempOverScrollRight) {
oldX = ev.getX();
isOverScrollRight = tempOverScrollRight;
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
return true;
}
isOverScrollRight = tempOverScrollRight;
oldX = ev.getX();
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
oldY = 0;
oldX = 0;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
finishOverScroll = true;
mSmoothScrollTo(0, 0);
break;
}
return super.dispatchTouchEvent(ev);
}
private float getDealt(float dealt, float distance) {
if (dealt * distance < 0)
return dealt;
float x = (float) Math.min(Math.max(Math.abs(distance), 0.1) / Math.abs(baseOverScrollLength), 1);
float y = Math.min(new AccelerateInterpolator(0.15f).getInterpolation(x), 1);
return dealt * (1 - y);
}
private MotionEvent resetVertical(MotionEvent event) {
oldY = 0;
dealtY = 0;
event.setAction(MotionEvent.ACTION_DOWN);
super.dispatchTouchEvent(event);
event.setAction(MotionEvent.ACTION_MOVE);
return event;
}
private MotionEvent resetHorizontally(MotionEvent event) {
oldX = 0;
dealtX = 0;
event.setAction(MotionEvent.ACTION_DOWN);
super.dispatchTouchEvent(event);
event.setAction(MotionEvent.ACTION_MOVE);
return event;
}
private boolean canOverScroll() {
return child != null;
}
private void overScroll(int dealtX, int dealtY) {
mSmoothScrollTo(dealtX, dealtY);
}
private boolean isTopOverScroll(float currentY) {
if (isOverScrollTop) {
return true;
}
if (!topOverScrollEnable || !isVerticalMove) {
return false;
}
float dealtY = oldY - currentY;
return dealtY < 0 && !canChildScrollUp();
}
private boolean isBottomOverScroll(float currentY) {
if (isOverScrollBottom) {
return true;
}
if (!bottomOverScrollEnable || !isVerticalMove) {
return false;
}
float dealtY = oldY - currentY;
return dealtY > 0 && !canChildScrollDown();
}
private boolean isLeftOverScroll(float currentX) {
if (isOverScrollLeft) {
return true;
}
if (!leftOverScrollEnable || !isHorizontallyMove) {
return false;
}
float dealtX = oldX - currentX;
return dealtX < 0 && !canChildScrollLeft();
}
private boolean isRightOverScroll(float currentX) {
if (!rightOverScrollEnable || !isHorizontallyMove) {
return false;
}
float dealtX = oldX - currentX;
return dealtX > 0 && !canChildScrollRight();
}
private boolean isChildCanScrollVertical() {
return canChildScrollDown() || canChildScrollUp();
}
private boolean isChildCanScrollHorizontally() {
return canChildScrollLeft() || canChildScrollRight();
}
private void checkMoveDirection(float currentX, float currentY) {
if (isVerticalMove || isHorizontallyMove) {
return;
}
if (canOverScrollVertical) {
float dealtY = currentY - downY;
isVerticalMove = Math.abs(dealtY) >= configuration.getScaledTouchSlop();
} else if (canOverScrollHorizontally) {
float dealtX = currentX - downX;
isHorizontallyMove = Math.abs(dealtX) >= configuration.getScaledTouchSlop();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
/**
* 是否能下拉
*
* @return
*/
private boolean canChildScrollUp() {
if (checkListener != null) {
return checkListener.canScrollUp();
}
if (android.os.Build.VERSION.SDK_INT < 14) {
if (child instanceof AbsListView) {
final AbsListView absListView = (AbsListView) child;
return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
}
}
return ViewCompat.canScrollVertically(child, -1);
}
/**
* 是否能上拉
*
* @return
*/
private boolean canChildScrollDown() {
if (checkListener != null) {
return checkListener.canScrollDown();
}
if (android.os.Build.VERSION.SDK_INT < 14) {
if (child instanceof AbsListView) {
final AbsListView absListView = (AbsListView) child;
return absListView.getChildCount() > 0 && (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1 || absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getHeight() - absListView.getPaddingBottom());
}
}
return ViewCompat.canScrollVertically(child, 1);
}
/**
* 是否能左拉
*
* @return
*/
private boolean canChildScrollLeft() {
if (checkListener != null) {
return checkListener.canScrollLeft();
}
return ViewCompat.canScrollHorizontally(child, -1);
}
/**
* 是否能右拉
*
* @return
*/
private boolean canChildScrollRight() {
if (checkListener != null) {
return checkListener.canScrollRight();
}
return ViewCompat.canScrollHorizontally(child, 1);
}
private void startOverScrollAim(float currVelocity) {
float speed = currVelocity / configuration.getScaledMaximumFlingVelocity();
if (canOverScrollVertical) {
if (!canChildScrollUp()) {
overScrollRunnable.start(0, -speed);
} else {
overScrollRunnable.start(0, speed);
}
} else {
if (canChildScrollRight()) {
overScrollRunnable.start(-speed, 0);
} else {
overScrollRunnable.start(speed, 0);
}
}
}
private clreplaced OverScrollRunnable implements Runnable {
private static final long DELAY_TIME = 20;
private long duration = 160;
private float speedX, speedY;
private long timePreplaced;
private long startTime;
private int distanceX, distanceY;
public void start(float speedX, float speedY) {
this.speedX = speedX;
this.speedY = speedY;
startTime = System.currentTimeMillis();
run();
}
@Override
public void run() {
timePreplaced = System.currentTimeMillis() - startTime;
if (timePreplaced < duration) {
distanceY = (int) (DELAY_TIME * speedY);
distanceX = (int) (DELAY_TIME * speedX);
mSmoothScrollBy(distanceX, distanceY);
postDelayed(this, DELAY_TIME);
} else if (timePreplaced > duration) {
mSmoothScrollTo(0, 0);
}
}
}
private clreplaced FlingRunnable implements Runnable {
private static final long DELAY_TIME = 40;
private boolean abort;
private int mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
public void start(float velocityX, float velocityY) {
abort = false;
float velocity = canOverScrollVertical ? velocityY : velocityX;
flingScroller.fling(0, 0, 0, (int) velocity, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
postDelayed(this, 40);
}
@Override
public void run() {
if (!abort && flingScroller.computeScrollOffset()) {
boolean scrollEnd = false;
if (canOverScrollVertical) {
scrollEnd = !canChildScrollDown() || !canChildScrollUp();
} else {
scrollEnd = !canChildScrollLeft() || !canChildScrollRight();
}
float currVelocity = flingScroller.getCurrVelocity();
if (scrollEnd) {
if (currVelocity > mMinimumFlingVelocity) {
startOverScrollAim(currVelocity);
}
} else {
if (currVelocity > mMinimumFlingVelocity) {
postDelayed(this, DELAY_TIME);
}
}
}
}
public void abort() {
abort = true;
}
}
}
19
Source : SpringView.java
with Apache License 2.0
from zuoweitan
with Apache License 2.0
from zuoweitan
/**
* Created by liaoinstan on 2016/3/11.
*/
public clreplaced SpringView extends ViewGroup {
private Context context;
private LayoutInflater inflater;
private OverScroller mScroller;
// 监听回调
private OnFreshListener listener;
// 用于判断是否在下拉时到达临界点
private boolean isCallDown = false;
// 用于判断是否在上拉时到达临界点
private boolean isCallUp = false;
// 用于判断是否是拖动动作的第一次move
private boolean isFirst = true;
// 是否需要改变样式
private boolean needChange = false;
// 是否需要弹回的动画
private boolean needResetAnim = false;
// 是否超过一屏时才允许上拉,为false则不满一屏也可以上拉,注意样式为isOverlap时,无论如何也不允许在不满一屏时上拉
private boolean isFullEnable = false;
// 当前是否正在拖动
private boolean isMoveNow = false;
private long lastMoveTime;
// 是否禁用(默认可用)
private boolean enable = true;
private int MOVE_TIME = 400;
private int MOVE_TIME_OVER = 200;
// 是否需要回调接口:TOP 只回调刷新、BOTTOM 只回调加载更多、BOTH 都需要、NONE 都不
public enum Give {
BOTH, TOP, BOTTOM, NONE
}
public enum Type {
OVERLAP, FOLLOW
}
private Give give = Give.BOTH;
private Type type = Type.OVERLAP;
private Type _type;
// private boolean i1sOverlap = true; //默认是重叠的样式
// private boolean _i1sOverlap; //保存用户动态设置样式时传入的参数
// 移动参数:计算手指移动量的时候会用到这个值,值越大,移动量越小,若值为1则手指移动多少就滑动多少px
private final double MOVE_PARA = 2;
// 最大拉动距离,拉动距离越靠近这个值拉动就越缓慢
private int MAX_HEADER_PULL_HEIGHT = 600;
private int MAX_FOOTER_PULL_HEIGHT = 600;
// 拉动多少距离被认定为刷新(加载)动作
private int HEADER_LIMIT_HEIGHT;
private int FOOTER_LIMIT_HEIGHT;
private int HEADER_SPRING_HEIGHT;
private int FOOTER_SPRING_HEIGHT;
// 储存上次的Y坐标
private float mLastY;
private float mLastX;
// 储存第一次的Y坐标
private float mfirstY;
// 储存手指拉动的总距离
private float dsY;
// 滑动事件目前是否在本控件的控制中
private boolean isInControl = false;
// 存储拉动前的位置
private Rect mRect = new Rect();
// 头尾内容布局
private View header;
private View footer;
private View contentView;
public SpringView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
inflater = LayoutInflater.from(context);
mScroller = new OverScroller(context);
// 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SpringView);
if (ta.hasValue(R.styleable.SpringView_type)) {
int type_int = ta.getInt(R.styleable.SpringView_type, 0);
type = Type.values()[type_int];
}
if (ta.hasValue(R.styleable.SpringView_give)) {
int give_int = ta.getInt(R.styleable.SpringView_give, 0);
give = Give.values()[give_int];
}
if (ta.hasValue(R.styleable.SpringView_header)) {
headerResoureId = ta.getResourceId(R.styleable.SpringView_header, 0);
}
if (ta.hasValue(R.styleable.SpringView_footer)) {
footerResoureId = ta.getResourceId(R.styleable.SpringView_footer, 0);
}
ta.recycle();
}
private int headerResoureId;
private int footerResoureId;
@Override
protected void onFinishInflate() {
contentView = getChildAt(0);
if (contentView == null) {
return;
}
setPadding(0, 0, 0, 0);
contentView.setPadding(0, 0, 0, 0);
if (headerResoureId != 0) {
inflater.inflate(headerResoureId, this, true);
header = getChildAt(getChildCount() - 1);
}
if (footerResoureId != 0) {
inflater.inflate(footerResoureId, this, true);
footer = getChildAt(getChildCount() - 1);
footer.setVisibility(INVISIBLE);
}
// 把内容放在最前端
contentView.bringToFront();
super.onFinishInflate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
// 如果是动态设置的头部,则使用动态设置的参数
if (headerHander != null) {
// 设置下拉最大高度,只有在>0时才生效,否则使用默认值
int xh = headerHander.getDragMaxHeight(header);
if (xh > 0)
MAX_HEADER_PULL_HEIGHT = xh;
// 设置下拉临界高度,只有在>0时才生效,否则默认为header的高度
int h = headerHander.getDragLimitHeight(header);
HEADER_LIMIT_HEIGHT = h > 0 ? h : header.getMeasuredHeight();
// 设置下拉弹动高度,只有在>0时才生效,否则默认和临界高度一致
int sh = headerHander.getDragSpringHeight(header);
HEADER_SPRING_HEIGHT = sh > 0 ? sh : HEADER_LIMIT_HEIGHT;
} else {
// 不是动态设置的头部,设置默认值
if (header != null)
HEADER_LIMIT_HEIGHT = header.getMeasuredHeight();
HEADER_SPRING_HEIGHT = HEADER_LIMIT_HEIGHT;
}
// 设置尾部参数,和上面一样
if (footerHander != null) {
int xh = footerHander.getDragMaxHeight(footer);
if (xh > 0)
MAX_FOOTER_PULL_HEIGHT = xh;
int h = footerHander.getDragLimitHeight(footer);
FOOTER_LIMIT_HEIGHT = h > 0 ? h : footer.getMeasuredHeight();
int sh = footerHander.getDragSpringHeight(footer);
FOOTER_SPRING_HEIGHT = sh > 0 ? sh : FOOTER_LIMIT_HEIGHT;
} else {
if (footer != null)
FOOTER_LIMIT_HEIGHT = footer.getMeasuredHeight();
FOOTER_SPRING_HEIGHT = FOOTER_LIMIT_HEIGHT;
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (contentView != null) {
if (type == Type.OVERLAP) {
if (header != null) {
header.layout(0, 0, getWidth(), header.getMeasuredHeight());
}
if (footer != null) {
footer.layout(0, getHeight() - footer.getMeasuredHeight(), getWidth(), getHeight());
}
} else if (type == Type.FOLLOW) {
if (header != null) {
header.layout(0, -header.getMeasuredHeight(), getWidth(), 0);
}
if (footer != null) {
footer.layout(0, getHeight(), getWidth(), getHeight() + footer.getMeasuredHeight());
}
}
contentView.layout(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
}
}
private float dy;
private float dx;
private boolean isNeedMyMove;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
dealMulTouchEvent(event);
int action = event.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
hasCallFull = false;
hasCallRefresh = false;
mfirstY = event.getY();
boolean isTop = isChildScrollToTop();
boolean isBottom = isChildScrollToBottomFull(isFullEnable);
if (isTop || isBottom)
isNeedMyMove = false;
break;
case MotionEvent.ACTION_MOVE:
dsY += dy;
isMoveNow = true;
isNeedMyMove = isNeedMyMove();
if (isNeedMyMove && !isInControl) {
// 把内部控件的事件转发给本控件处理
isInControl = true;
event.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(event);
dispatchTouchEvent(event);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
break;
case MotionEvent.ACTION_UP:
isMoveNow = false;
lastMoveTime = System.currentTimeMillis();
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return isNeedMyMove && enable;
// int action = event.getAction();
// switch (action){
// case MotionEvent.ACTION_MOVE:
// return isNeedMyMove;
// }
// return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (contentView == null) {
return false;
}
int action = event.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
isFirst = true;
// if (!mScroller.isFinished()) mScroller.abortAnimation();//不需要处理
break;
case MotionEvent.ACTION_MOVE:
if (isNeedMyMove) {
// 按下的时候关闭回弹
needResetAnim = false;
// 执行位移操作
doMove();
// 下拉的时候显示header并隐藏footer,上拉的时候相反
if (isTop()) {
if (header != null && header.getVisibility() != View.VISIBLE)
header.setVisibility(View.VISIBLE);
if (footer != null && footer.getVisibility() != View.INVISIBLE)
footer.setVisibility(View.INVISIBLE);
} else if (isBottom()) {
if (header != null && header.getVisibility() != View.INVISIBLE)
header.setVisibility(View.INVISIBLE);
if (footer != null && footer.getVisibility() != View.VISIBLE)
footer.setVisibility(View.VISIBLE);
}
// 回调onDropAnim接口
callOnDropAnim();
// 回调callOnPreDrag接口
callOnPreDrag();
// 回调onLimitDes接口
callOnLimitDes();
isFirst = false;
} else {
// 手指在产生移动的时候(dy!=0)才重置位置
if (dy != 0 && isFlow()) {
resetPosition();
// 把滚动事件交给内部控件处理
event.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(event);
isInControl = false;
}
}
break;
case MotionEvent.ACTION_UP:
last_top = 0;
// 松开的时候打开回弹
needResetAnim = true;
isFirst = true;
_firstDrag = true;
restSmartPosition();
dsY = 0;
dy = 0;
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
/**
* 处理多点触控的情况,准确地计算Y坐标和移动距离dy
* 同时兼容单点触控的情况
*/
private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
public void dealMulTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
mLastX = x;
mLastY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE:
{
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
dx = x - mLastX;
dy = y - mLastY;
mLastY = y;
mLastX = x;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_DOWN:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId != mActivePointerId) {
mLastX = MotionEventCompat.getX(ev, pointerIndex);
mLastY = MotionEventCompat.getY(ev, pointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastX = MotionEventCompat.getX(ev, newPointerIndex);
mLastY = MotionEventCompat.getY(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
break;
}
}
}
private int last_top;
private void doMove() {
if (type == Type.OVERLAP) {
// 记录移动前的位置
if (mRect.isEmpty()) {
mRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
}
// 根据下拉高度计算位移距离,(越拉越慢)
int movedy;
if (dy > 0) {
movedy = (int) ((MAX_HEADER_PULL_HEIGHT - contentView.getTop()) / (float) MAX_HEADER_PULL_HEIGHT * dy / MOVE_PARA);
} else {
movedy = (int) ((MAX_FOOTER_PULL_HEIGHT - (getHeight() - contentView.getBottom())) / (float) MAX_FOOTER_PULL_HEIGHT * dy / MOVE_PARA);
}
int top = contentView.getTop() + movedy;
contentView.layout(contentView.getLeft(), top, contentView.getRight(), top + contentView.getMeasuredHeight());
} else if (type == Type.FOLLOW) {
// 根据下拉高度计算位移距离,(越拉越慢)
int movedx;
if (dy > 0) {
movedx = (int) ((MAX_HEADER_PULL_HEIGHT + getScrollY()) / (float) MAX_HEADER_PULL_HEIGHT * dy / MOVE_PARA);
} else {
movedx = (int) ((MAX_FOOTER_PULL_HEIGHT - getScrollY()) / (float) MAX_FOOTER_PULL_HEIGHT * dy / MOVE_PARA);
}
scrollBy(0, -movedx);
}
}
private void callOnDropAnim() {
if (type == Type.OVERLAP) {
if (contentView.getTop() > 0)
if (headerHander != null)
headerHander.onDropAnim(header, contentView.getTop());
if (contentView.getTop() < 0)
if (footerHander != null)
footerHander.onDropAnim(footer, contentView.getTop());
} else if (type == Type.FOLLOW) {
if (getScrollY() < 0)
if (headerHander != null)
headerHander.onDropAnim(header, -getScrollY());
if (getScrollY() > 0)
if (footerHander != null)
footerHander.onDropAnim(footer, -getScrollY());
}
}
private boolean _firstDrag = true;
private void callOnPreDrag() {
if (_firstDrag) {
if (isTop()) {
if (headerHander != null)
headerHander.onPreDrag(header);
_firstDrag = false;
} else if (isBottom()) {
if (footerHander != null)
footerHander.onPreDrag(footer);
_firstDrag = false;
}
}
}
private void callOnLimitDes() {
boolean topORbottom = false;
if (type == Type.OVERLAP) {
topORbottom = contentView.getTop() >= 0 && isChildScrollToTop();
} else if (type == Type.FOLLOW) {
topORbottom = getScrollY() <= 0 && isChildScrollToTop();
}
if (isFirst) {
if (topORbottom) {
isCallUp = true;
isCallDown = false;
} else {
isCallUp = false;
isCallDown = true;
}
}
if (dy == 0)
return;
boolean upORdown = dy < 0;
if (topORbottom) {
if (!upORdown) {
if ((isTopOverFarm()) && !isCallDown) {
isCallDown = true;
if (headerHander != null)
headerHander.onLimitDes(header, upORdown);
isCallUp = false;
}
} else {
if (!isTopOverFarm() && !isCallUp) {
isCallUp = true;
if (headerHander != null)
headerHander.onLimitDes(header, upORdown);
isCallDown = false;
}
}
} else {
if (upORdown) {
if (isBottomOverFarm() && !isCallUp) {
isCallUp = true;
if (footerHander != null)
footerHander.onLimitDes(footer, upORdown);
isCallDown = false;
}
} else {
if (!isBottomOverFarm() && !isCallDown) {
isCallDown = true;
if (footerHander != null)
footerHander.onLimitDes(footer, upORdown);
isCallUp = false;
}
}
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
// 在滚动动画完全结束后回调接口
// 滚动回调过程中mScroller.isFinished会多次返回true,导致判断条件被多次进入,设置标志位保证只调用一次
if (!isMoveNow && type == Type.FOLLOW && mScroller.isFinished()) {
if (isFullAnim) {
if (!hasCallFull) {
hasCallFull = true;
callOnAfterFullAnim();
}
} else {
if (!hasCallRefresh) {
hasCallRefresh = true;
callOnAfterRefreshAnim();
}
}
}
if (mScroller.isFinished() && mScroller.getCurrY() == 0 && type == Type.FOLLOW) {
callOnPositionReset();
}
}
private void callOnPositionReset() {
if ((type == Type.FOLLOW && getScrollY() == 0) || (type == Type.OVERLAP && contentView.getTop() == 0)) {
if (headerHander != null) {
headerHander.onPositionReset();
}
}
}
private int callFreshORload = 0;
private boolean isFullAnim;
private boolean hasCallFull = false;
private boolean hasCallRefresh = false;
/**
* 判断是否需要由该控件来控制滑动事件
*/
private boolean isNeedMyMove() {
if (contentView == null) {
return false;
}
if (Math.abs(dy) < Math.abs(dx)) {
return false;
}
boolean isTop = isChildScrollToTop();
// false不满一屏也算在底部,true不满一屏不算在底部
boolean isBottom = isChildScrollToBottomFull(isFullEnable);
if (type == Type.OVERLAP) {
if (header != null) {
if (isTop && dy > 0 || contentView.getTop() > 0 + 20) {
return true;
}
}
if (footer != null) {
if (isBottom && dy < 0 || contentView.getBottom() < mRect.bottom - 20) {
// if (isFullScrean()&&!isFullEnable)
// return true;
// else
// return false;
return true;
}
}
} else if (type == Type.FOLLOW) {
if (header != null) {
// 其中的20是一个防止触摸误差的偏移量
if (isTop && dy > 0 || getScrollY() < 0 - 20) {
return true;
}
}
if (footer != null) {
if (isBottom && dy < 0 || getScrollY() > 0 + 20) {
return true;
}
}
}
return false;
}
private void callOnAfterFullAnim() {
if (callFreshORload != 0) {
callOnFinishAnim();
}
if (needChangeHeader) {
needChangeHeader = false;
setHeaderIn(_headerHander);
}
if (needChangeFooter) {
needChangeFooter = false;
setFooterIn(_footerHander);
}
// 动画完成后检查是否需要切换type,是则切换
if (needChange) {
changeType(_type);
}
}
private void callOnAfterRefreshAnim() {
if (type == Type.FOLLOW) {
if (isTop()) {
listener.onRefresh();
} else if (isBottom()) {
listener.onLoadmore();
}
} else if (type == Type.OVERLAP) {
if (!isMoveNow) {
long nowtime = System.currentTimeMillis();
if (nowtime - lastMoveTime >= MOVE_TIME_OVER) {
if (callFreshORload == 1)
listener.onRefresh();
if (callFreshORload == 2)
listener.onLoadmore();
} else {
onFinishFreshAndLoad();
}
}
}
}
/**
* 重置控件位置到初始状态
*/
private void resetPosition() {
isFullAnim = true;
// 重置位置的时候,滑动事件已经不在控件的控制中了
isInControl = false;
if (type == Type.OVERLAP) {
if (mRect.bottom == 0 || mRect.right == 0)
return;
// 根据下拉高度计算弹回时间,时间最小100,最大400
int time = 0;
if (contentView.getHeight() > 0) {
time = Math.abs(400 * contentView.getTop() / contentView.getHeight());
}
if (time < 100)
time = 100;
TranslateAnimation animation = new HTranslateAnimation(0, 0, contentView.getTop(), mRect.top);
animation.setDuration(time);
animation.setFillAfter(true);
animation.setAnimationListener(new HTranslateAnimation.OnTranslateListener() {
@Override
public void onTranslateChanged(float dx, float dy) {
contentView.layout(mRect.left, (int) dy, mRect.right, mRect.bottom);
callOnDropAnim();
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
contentView.clearAnimation();
callOnAfterFullAnim();
callOnPositionReset();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
contentView.startAnimation(animation);
} else if (type == Type.FOLLOW) {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), MOVE_TIME);
invalidate();
}
// mRect.setEmpty();
}
private void callOnFinishAnim() {
if (callFreshORload != 0) {
if (callFreshORload == 1) {
if (headerHander != null)
headerHander.onFinishAnim();
if (give == Give.BOTTOM || give == Give.NONE) {
listener.onRefresh();
}
} else if (callFreshORload == 2) {
if (footerHander != null)
footerHander.onFinishAnim();
if (give == Give.TOP || give == Give.NONE) {
listener.onLoadmore();
}
}
callFreshORload = 0;
}
}
/**
* 重置控件位置到刷新状态(或加载状态)
*/
private void resetRefreshPosition() {
isFullAnim = false;
// 重置位置的时候,滑动事件已经不在控件的控制中了
isInControl = false;
if (type == Type.OVERLAP) {
if (mRect.bottom == 0 || mRect.right == 0)
return;
if (contentView.getTop() > mRect.top) {
// 下拉
Animation animation = new TranslateAnimation(0, 0, contentView.getTop() - HEADER_SPRING_HEIGHT, mRect.top);
animation.setDuration(MOVE_TIME_OVER);
animation.setFillAfter(true);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
callOnAfterRefreshAnim();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
contentView.startAnimation(animation);
contentView.layout(mRect.left, mRect.top + HEADER_SPRING_HEIGHT, mRect.right, mRect.bottom + HEADER_SPRING_HEIGHT);
} else {
// 上拉
Animation animation = new TranslateAnimation(0, 0, contentView.getTop() + FOOTER_SPRING_HEIGHT, mRect.top);
animation.setDuration(MOVE_TIME_OVER);
animation.setFillAfter(true);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
callOnAfterRefreshAnim();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
contentView.startAnimation(animation);
contentView.layout(mRect.left, mRect.top - FOOTER_SPRING_HEIGHT, mRect.right, mRect.bottom - FOOTER_SPRING_HEIGHT);
}
} else if (type == Type.FOLLOW) {
if (getScrollY() < 0) {
// 下拉
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - HEADER_SPRING_HEIGHT, MOVE_TIME);
invalidate();
} else {
// 上拉
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + FOOTER_SPRING_HEIGHT, MOVE_TIME);
invalidate();
}
}
}
public void callFresh() {
header.setVisibility(VISIBLE);
if (type == Type.OVERLAP) {
if (mRect.isEmpty()) {
mRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
}
Animation animation = new TranslateAnimation(0, 0, contentView.getTop() - HEADER_SPRING_HEIGHT, mRect.top);
animation.setDuration(MOVE_TIME_OVER);
animation.setFillAfter(true);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
if (headerHander != null)
headerHander.onStartAnim();
}
@Override
public void onAnimationEnd(Animation animation) {
callFreshORload = 1;
needResetAnim = true;
listener.onRefresh();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
contentView.startAnimation(animation);
contentView.layout(mRect.left, mRect.top + HEADER_SPRING_HEIGHT, mRect.right, mRect.bottom + HEADER_SPRING_HEIGHT);
} else if (type == Type.FOLLOW) {
isFullAnim = false;
hasCallRefresh = false;
callFreshORload = 1;
needResetAnim = true;
if (headerHander != null)
headerHander.onStartAnim();
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - HEADER_SPRING_HEIGHT, MOVE_TIME);
invalidate();
}
}
/**
* 智能判断是重置控件位置到初始状态还是到刷新/加载状态
*/
private void restSmartPosition() {
if (listener == null) {
resetPosition();
} else {
if (isTopOverFarm()) {
callFreshORload();
if (give == Give.BOTH || give == Give.TOP)
resetRefreshPosition();
else
resetPosition();
} else if (isBottomOverFarm()) {
callFreshORload();
if (give == Give.BOTH || give == Give.BOTTOM)
resetRefreshPosition();
else
resetPosition();
} else {
resetPosition();
}
}
}
private void callFreshORload() {
if (isTop()) {
// 下拉
callFreshORload = 1;
if (type == Type.OVERLAP) {
if (dsY > 200 || HEADER_LIMIT_HEIGHT >= HEADER_SPRING_HEIGHT) {
if (headerHander != null)
headerHander.onStartAnim();
}
} else if (type == Type.FOLLOW) {
if (headerHander != null)
headerHander.onStartAnim();
}
} else if (isBottom()) {
callFreshORload = 2;
if (type == Type.OVERLAP) {
if (dsY < -200 || FOOTER_LIMIT_HEIGHT >= FOOTER_SPRING_HEIGHT) {
if (footerHander != null)
footerHander.onStartAnim();
}
} else if (type == Type.FOLLOW) {
if (footerHander != null)
footerHander.onStartAnim();
}
}
}
/**
* 判断目标View是否滑动到顶部-还能否继续滑动
*
* @return
*/
private boolean isChildScrollToTop() {
// if (android.os.Build.VERSION.SDK_INT < 14) {
// if (contentView instanceof AbsListView) {
// final AbsListView absListView = (AbsListView) contentView;
// return !(absListView.getChildCount() > 0 && (absListView
// .getFirstVisiblePosition() > 0 || absListView
// .getChildAt(0).getTop() < absListView.getPaddingTop()));
// } else {
// return !(contentView.getScrollY() > 0);
// }
// } else {
// return !ViewCompat.canScrollVertically(contentView, -1);
// }
return !ViewCompat.canScrollVertically(contentView, -1);
}
/**
* 是否滑动到底部
* @return
*/
private boolean isChildScrollToBottomFull(boolean isFull) {
// if (isFull){
// if (isChildScrollToTop()) {
// return false;
// }
// }
// if (contentView instanceof RecyclerView) {
// RecyclerView recyclerView = (RecyclerView) contentView;
// RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// int count = recyclerView.getAdapter().gereplacedemCount();
// if (layoutManager instanceof LinearLayoutManager && count > 0) {
// LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
// if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == count - 1) {
// return true;
// }
// } else if (layoutManager instanceof StaggeredGridLayoutManager) {
// StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
// int[] lasreplacedems = new int[2];
// staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(lasreplacedems);
// int lasreplacedem = Math.max(lasreplacedems[0], lasreplacedems[1]);
// if (lasreplacedem == count - 1) {
// return true;
// }
// }
// return false;
// } else if (contentView instanceof AbsListView) {
// final AbsListView absListView = (AbsListView) contentView;
// final Adapter adapter = absListView.getAdapter();
// if (null == adapter || adapter.isEmpty()) {
// return true;
// }
// final int lasreplacedemPosition = adapter.getCount() - 1;
// final int lastVisiblePosition = absListView.getLastVisiblePosition();
// if (lastVisiblePosition >= lasreplacedemPosition - 1) {
// final int childIndex = lastVisiblePosition - absListView.getFirstVisiblePosition();
// final int childCount = absListView.getChildCount();
// final int index = Math.max(childIndex, childCount - 1);
// final View lastVisibleChild = absListView.getChildAt(index);
// if (lastVisibleChild != null) {
// return lastVisibleChild.getBottom() <= absListView.getBottom()-absListView.getTop();
// }
// }
// return false;
// } else if (contentView instanceof ScrollView) {
// ScrollView scrollView = (ScrollView) contentView;
// View view = scrollView.getChildAt(scrollView.getChildCount() - 1);
// if (view != null) {
// int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()));
// if (diff == 0) {
// return true;
// }
// if(!isFull) {
// //如果scrollView中内容不满一屏,也算在底部
// if (view.getMeasuredHeight() <= scrollView.getMeasuredHeight()) {
// return true;
// }
// }
// }
// }
// return false;
return !ViewCompat.canScrollVertically(contentView, 1);
}
private boolean isChildScrollToBottom() {
return isChildScrollToBottomFull(true);
}
private boolean isFullScrean() {
boolean isBottom = isChildScrollToBottomFull(false);
if (isBottom) {
return isChildScrollToBottomFull(true);
}
return true;
}
/**
* 判断顶部拉动是否超过临界值
*/
private boolean isTopOverFarm() {
if (type == Type.OVERLAP) {
return contentView.getTop() > HEADER_LIMIT_HEIGHT;
} else if (type == Type.FOLLOW) {
return -getScrollY() > HEADER_LIMIT_HEIGHT;
} else
return false;
}
/**
* 判断底部拉动是否超过临界值
*/
private boolean isBottomOverFarm() {
if (type == Type.OVERLAP) {
return getHeight() - contentView.getBottom() > FOOTER_LIMIT_HEIGHT;
} else if (type == Type.FOLLOW) {
return getScrollY() > FOOTER_LIMIT_HEIGHT;
} else
return false;
}
/**
* 判断当前状态是否拉动顶部
*/
private boolean isTop() {
if (type == Type.OVERLAP) {
return contentView.getTop() > 0;
} else if (type == Type.FOLLOW) {
return getScrollY() < 0;
} else
return false;
}
private boolean isBottom() {
if (type == Type.OVERLAP) {
return contentView.getTop() < 0;
} else if (type == Type.FOLLOW) {
return getScrollY() > 0;
} else
return false;
}
private boolean isFlow() {
if (type == Type.OVERLAP) {
return contentView.getTop() < 30 && contentView.getTop() > -30;
} else if (type == Type.FOLLOW) {
return getScrollY() > -30 && getScrollY() < 30;
} else
return false;
}
/**
* 切换Type的方法,之所以不暴露在外部,是防止用户在拖动过程中调用造成布局错乱
* 所以在外部方法中设置标志,然后在拖动完毕后判断是否需要调用,是则调用
*/
private void changeType(Type type) {
this.type = type;
if (header != null && header.getVisibility() != INVISIBLE)
header.setVisibility(INVISIBLE);
if (footer != null && footer.getVisibility() != INVISIBLE)
footer.setVisibility(INVISIBLE);
requestLayout();
needChange = false;
}
// #############################################
// ## 对外暴露的方法 ##
// #############################################
/**
* 重置控件位置,暴露给外部的方法,用于在刷新或者加载完成后调用
*/
public void onFinishFreshAndLoad() {
if (!isMoveNow && needResetAnim) {
boolean needTop = isTop() && (give == Give.TOP || give == Give.BOTH);
boolean needBottom = isBottom() && (give == Give.BOTTOM || give == Give.BOTH);
if (needTop || needBottom) {
if (contentView instanceof ListView) {
// ((ListView) contentView).smoothScrollByOffset(1);
// 刷新后调用,才能正确显示刷新的item,如果调用上面的方法,listview会被固定在底部
// ((ListView) contentView).smoothScrollBy(-1,0);
}
resetPosition();
}
}
}
public void setMoveTime(int time) {
this.MOVE_TIME = time;
}
public void setMoveTimeOver(int time) {
this.MOVE_TIME_OVER = time;
}
/**
* 是否禁用SpringView
*/
public void setEnable(boolean enable) {
this.enable = enable;
}
public boolean isEnable() {
return enable;
}
/**
* 设置监听
*/
public void setListener(OnFreshListener listener) {
this.listener = listener;
}
/**
* 动态设置弹性模式
*/
public void setGive(Give give) {
this.give = give;
}
/**
* 改变样式的对外接口
*/
public void setType(Type type) {
if (isTop() || isBottom()) {
// 如果当前用户正在拖动,直接调用changeType()会造成布局错乱
// 设置needChange标志,在执行完拖动后再调用changeType()
needChange = true;
// 把参数保持起来
_type = type;
} else {
changeType(type);
}
}
/**
* 获取当前样式
*/
public Type getType() {
return type;
}
/**
* 回调接口
*/
public interface OnFreshListener {
/**
* 下拉刷新,回调接口
*/
void onRefresh();
/**
* 上拉加载,回调接口
*/
void onLoadmore();
}
public View getHeaderView() {
return header;
}
public View getFooterView() {
return footer;
}
private boolean needChangeHeader = false;
private boolean needChangeFooter = false;
private DragHander _headerHander;
private DragHander _footerHander;
private DragHander headerHander;
private DragHander footerHander;
public DragHander getHeader() {
return headerHander;
}
public DragHander getFooter() {
return footerHander;
}
public void setHeader(DragHander headerHander) {
if (this.headerHander != null && isTop()) {
needChangeHeader = true;
_headerHander = headerHander;
resetPosition();
} else {
setHeaderIn(headerHander);
}
}
private void setHeaderIn(DragHander headerHander) {
this.headerHander = headerHander;
if (header != null) {
removeView(this.header);
}
headerHander.getView(inflater, this);
this.header = getChildAt(getChildCount() - 1);
// 把内容放在最前端
contentView.bringToFront();
requestLayout();
}
public void setFooter(DragHander footerHander) {
if (this.footerHander != null && isBottom()) {
needChangeFooter = true;
_footerHander = footerHander;
resetPosition();
} else {
setFooterIn(footerHander);
}
}
private void setFooterIn(DragHander footerHander) {
this.footerHander = footerHander;
if (footer != null) {
removeView(footer);
}
footerHander.getView(inflater, this);
this.footer = getChildAt(getChildCount() - 1);
// 把内容放在最前端
contentView.bringToFront();
requestLayout();
}
public interface DragHander {
View getView(LayoutInflater inflater, ViewGroup viewGroup);
void onPositionReset();
int getDragLimitHeight(View rootView);
int getDragMaxHeight(View rootView);
int getDragSpringHeight(View rootView);
void onPreDrag(View rootView);
/**
* 手指拖动控件过程中的回调,用户可以根据拖动的距离添加拖动过程动画
* @param dy 拖动距离,下拉为+,上拉为-
*/
void onDropAnim(View rootView, int dy);
/**
* 手指拖动控件过程中每次抵达临界点时的回调,用户可以根据手指方向设置临界动画
* @param upORdown 是上拉还是下拉
*/
void onLimitDes(View rootView, boolean upORdown);
/**
* 拉动超过临界点后松开时回调
*/
void onStartAnim();
/**
* 头(尾)已经全部弹回时回调
*/
void onFinishAnim();
}
}
19
Source : ScrollableImageView.java
with MIT License
from zhangyangjing
with MIT License
from zhangyangjing
/**
* An extension to the standard Android {@link ImageView}, which makes it
* respond to Scroll and Fling events. Uses a {@link GestureDetectorCompat} and
* a {@link OverScroller} to provide scrolling functionality.
*
* @author EgorAnd
*/
public clreplaced ScrollableImageView extends ImageView {
private GestureDetectorCompat gestureDetector;
private OverScroller overScroller;
private final int screenW;
private final int screenH;
private int positionX = 0;
private int positionY = 0;
public ScrollableImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
DisplayMetrics dm = getResources().getDisplayMetrics();
screenW = dm.widthPixels;
screenH = dm.heightPixels;
gestureDetector = new GestureDetectorCompat(context, gestureListener);
overScroller = new OverScroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
gestureDetector.onTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
// computeScrollOffset() returns true only when the scrolling isn't
// already finished
if (overScroller.computeScrollOffset()) {
positionX = overScroller.getCurrX();
positionY = overScroller.getCurrY();
scrollTo(positionX, positionY);
} else {
// when scrolling is over, we will want to "spring back" if the
// image is overscrolled
overScroller.springBack(positionX, positionY, 0, getMaxHorizontal(), 0, getMaxVertical());
}
}
private int getMaxHorizontal() {
if (null == getDrawable())
return 0;
return (Math.abs(getDrawable().getBounds().width() - screenW));
}
private int getMaxVertical() {
if (null == getDrawable())
return 0;
return (Math.abs(getDrawable().getBounds().height() - screenH));
}
private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
overScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
overScroller.forceFinished(true);
overScroller.fling(positionX, positionY, (int) -velocityX, (int) -velocityY, 0, getMaxHorizontal(), 0, getMaxVertical());
ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
overScroller.forceFinished(true);
// normalize scrolling distances to not overscroll the image
int dx = (int) distanceX;
int dy = (int) distanceY;
int newPositionX = positionX + dx;
int newPositionY = positionY + dy;
if (newPositionX < 0) {
dx -= newPositionX;
} else if (newPositionX > getMaxHorizontal()) {
dx -= (newPositionX - getMaxHorizontal());
}
if (newPositionY < 0) {
dy -= newPositionY;
} else if (newPositionY > getMaxVertical()) {
dy -= (newPositionY - getMaxVertical());
}
overScroller.startScroll(positionX, positionY, dx, dy, 0);
ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
return true;
}
};
}
19
Source : ScrollLayout.java
with Apache License 2.0
from Z-bm
with Apache License 2.0
from Z-bm
/**
* Created by zbm阿铭 on 2018/2/10.
*/
public clreplaced ScrollLayout extends RelativeLayout {
private View mTop;
private int mTopViewHeight;
private OverScroller mScroller;
public ScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new OverScroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 这个id必须能找到
mTop = findViewById(R.id.toolbar);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTop.getMeasuredHeight();
}
// 这里留出状态栏的高度
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
}
19
Source : GalleryThumbnailView.java
with Apache License 2.0
from yuchuangu85
with Apache License 2.0
from yuchuangu85
public clreplaced GalleryThumbnailView extends ViewGroup {
public interface GalleryThumbnailAdapter extends ListAdapter {
/**
* @param position Position to get the intrinsic aspect ratio for
* @return width / height
*/
float getIntrinsicAspectRatio(int position);
}
private static final String TAG = "GalleryThumbnailView";
private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);
private static final int LAND_UNITS = 2;
private static final int PORT_UNITS = 3;
private GalleryThumbnailAdapter mAdapter;
private final RecycleBin mRecycler = new RecycleBin();
private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
private boolean mDataChanged;
private int mOldItemCount;
private int mItemCount;
private boolean mHreplacedtableIds;
private int mFirstPosition;
private boolean mPopulating;
private boolean mInLayout;
private int mTouchSlop;
private int mMaximumVelocity;
private int mFlingVelocity;
private float mLastTouchX;
private float mTouchRemainderX;
private int mActivePointerId;
private static final int TOUCH_MODE_IDLE = 0;
private static final int TOUCH_MODE_DRAGGING = 1;
private static final int TOUCH_MODE_FLINGING = 2;
private int mTouchMode;
private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
private final OverScroller mScroller;
private final EdgeEffectCompat mLeftEdge;
private final EdgeEffectCompat mRightEdge;
private int mLargeColumnWidth;
private int mSmallColumnWidth;
private int mLargeColumnUnitCount = 8;
private int mSmallColumnUnitCount = 10;
public GalleryThumbnailView(Context context) {
this(context, null);
}
public GalleryThumbnailView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
mFlingVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = new OverScroller(context);
mLeftEdge = new EdgeEffectCompat(context);
mRightEdge = new EdgeEffectCompat(context);
setWillNotDraw(false);
setClipToPadding(false);
}
@Override
public void requestLayout() {
if (!mPopulating) {
super.requestLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + "Using fallback spec of EXACTLY " + widthSize);
}
if (heightMode != MeasureSpec.EXACTLY) {
Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + "Using fallback spec of EXACTLY " + heightSize);
}
setMeasuredDimension(widthSize, heightSize);
float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
float height = getMeasuredHeight() / portSpaces;
mLargeColumnWidth = (int) (height / ASPECT_RATIO);
portSpaces++;
height = getMeasuredHeight() / portSpaces;
mSmallColumnWidth = (int) (height / ASPECT_RATIO);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
populate();
mInLayout = false;
final int width = r - l;
final int height = b - t;
mLeftEdge.setSize(width, height);
mRightEdge.setSize(width, height);
}
private void populate() {
if (getWidth() == 0 || getHeight() == 0) {
return;
}
// TODO: Handle size changing
// final int colCount = mColCount;
// if (mItemTops == null || mItemTops.length != colCount) {
// mItemTops = new int[colCount];
// mItemBottoms = new int[colCount];
// final int top = getPaddingTop();
// final int offset = top + Math.min(mRestoreOffset, 0);
// Arrays.fill(mItemTops, offset);
// Arrays.fill(mItemBottoms, offset);
// mLayoutRecords.clear();
// if (mInLayout) {
// removeAllViewsInLayout();
// } else {
// removeAllViews();
// }
// mRestoreOffset = 0;
// }
mPopulating = true;
layoutChildren(mDataChanged);
fillRight(mFirstPosition + getChildCount(), 0);
fillLeft(mFirstPosition - 1, 0);
mPopulating = false;
mDataChanged = false;
}
final void layoutChildren(boolean queryAdapter) {
// TODO
// final int childCount = getChildCount();
// for (int i = 0; i < childCount; i++) {
// View child = getChildAt(i);
//
// if (child.isLayoutRequested()) {
// final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
// final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
// child.measure(widthSpec, heightSpec);
// child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
// }
//
// int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
// mItemBottoms[col] + mItemMargin : child.getTop();
// if (span > 1) {
// int lowest = childTop;
// for (int j = col + 1; j < col + span; j++) {
// final int bottom = mItemBottoms[j] + mItemMargin;
// if (bottom > lowest) {
// lowest = bottom;
// }
// }
// childTop = lowest;
// }
// final int childHeight = child.getMeasuredHeight();
// final int childBottom = childTop + childHeight;
// final int childLeft = paddingLeft + col * (colWidth + itemMargin);
// final int childRight = childLeft + child.getMeasuredWidth();
// child.layout(childLeft, childTop, childRight, childBottom);
// }
}
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param startPosition Logical position in the list to start from
* @param x Left or right edge of the view to add
* @param forward If true, align left edge to x and increase position.
* If false, align right edge to x and decrease position.
* @return Number of views added
*/
private int makeAndAddColumn(int startPosition, int x, boolean forward) {
int columnWidth = mLargeColumnWidth;
int addViews = 0;
for (int remaining = mLargeColumnUnitCount, i = 0; remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount; i += forward ? 1 : -1, addViews++) {
if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
// landscape
remaining -= LAND_UNITS;
} else {
// portrait
remaining -= PORT_UNITS;
if (remaining < 0) {
remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
columnWidth = mSmallColumnWidth;
}
}
}
int nextTop = 0;
for (int i = 0; i < addViews; i++) {
int position = startPosition + (forward ? i : -i);
View child = obtainView(position, null);
if (child.getParent() != this) {
if (mInLayout) {
addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
} else {
addView(child, forward ? -1 : 0);
}
}
int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f ? columnWidth / ASPECT_RATIO : columnWidth * ASPECT_RATIO));
int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
int childLeft = forward ? x : x - columnWidth;
child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
nextTop += heightSize;
}
return addViews;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mVelocityTracker.addMovement(ev);
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
switch(action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.clear();
mScroller.abortAnimation();
mLastTouchX = ev.getX();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mTouchRemainderX = 0;
if (mTouchMode == TOUCH_MODE_FLINGING) {
// Catch!
mTouchMode = TOUCH_MODE_DRAGGING;
return true;
}
break;
case MotionEvent.ACTION_MOVE:
{
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (index < 0) {
Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + "event stream?");
return false;
}
final float x = MotionEventCompat.getX(ev, index);
final float dx = x - mLastTouchX + mTouchRemainderX;
final int deltaY = (int) dx;
mTouchRemainderX = dx - deltaY;
if (Math.abs(dx) > mTouchSlop) {
mTouchMode = TOUCH_MODE_DRAGGING;
return true;
}
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mVelocityTracker.addMovement(ev);
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
switch(action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.clear();
mScroller.abortAnimation();
mLastTouchX = ev.getX();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mTouchRemainderX = 0;
break;
case MotionEvent.ACTION_MOVE:
{
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (index < 0) {
Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + "event stream?");
return false;
}
final float x = MotionEventCompat.getX(ev, index);
final float dx = x - mLastTouchX + mTouchRemainderX;
final int deltaX = (int) dx;
mTouchRemainderX = dx - deltaX;
if (Math.abs(dx) > mTouchSlop) {
mTouchMode = TOUCH_MODE_DRAGGING;
}
if (mTouchMode == TOUCH_MODE_DRAGGING) {
mLastTouchX = x;
if (!trackMotionScroll(deltaX, true)) {
// Break fling velocity if we impacted an edge.
mVelocityTracker.clear();
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
mTouchMode = TOUCH_MODE_IDLE;
break;
case MotionEvent.ACTION_UP:
{
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId);
if (Math.abs(velocity) > mFlingVelocity) {
// TODO
mTouchMode = TOUCH_MODE_FLINGING;
mScroller.fling(0, 0, (int) velocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
mLastTouchX = 0;
ViewCompat.postInvalidateOnAnimation(this);
} else {
mTouchMode = TOUCH_MODE_IDLE;
}
}
break;
}
return true;
}
/**
* @param deltaX Pixels that content should move by
* @return true if the movement completed, false if it was stopped prematurely.
*/
private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
final boolean contentFits = contentFits();
final int allowOverhang = Math.abs(deltaX);
final int overScrolledBy;
final int movedBy;
if (!contentFits) {
final int overhang;
final boolean up;
mPopulating = true;
if (deltaX > 0) {
overhang = fillLeft(mFirstPosition - 1, allowOverhang);
up = true;
} else {
overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
up = false;
}
movedBy = Math.min(overhang, allowOverhang);
offsetChildren(up ? movedBy : -movedBy);
recycleOffscreenViews();
mPopulating = false;
overScrolledBy = allowOverhang - overhang;
} else {
overScrolledBy = allowOverhang;
movedBy = 0;
}
if (allowOverScroll) {
final int overScrollMode = ViewCompat.getOverScrollMode(this);
if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
if (overScrolledBy > 0) {
EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge;
edge.onPull((float) Math.abs(deltaX) / getWidth());
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
return deltaX == 0 || movedBy != 0;
}
/**
* Important: this method will leave offscreen views attached if they
* are required to maintain the invariant that child view with index i
* is always the view corresponding to position mFirstPosition + i.
*/
private void recycleOffscreenViews() {
final int height = getHeight();
final int clearAbove = 0;
final int clearBelow = height;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= clearBelow) {
// There may be other offscreen views, but we need to maintain
// the invariant doreplacedented above.
break;
}
if (mInLayout) {
removeViewsInLayout(i, 1);
} else {
removeViewAt(i);
}
mRecycler.addScrap(child);
}
while (getChildCount() > 0) {
final View child = getChildAt(0);
if (child.getBottom() >= clearAbove) {
// There may be other offscreen views, but we need to maintain
// the invariant doreplacedented above.
break;
}
if (mInLayout) {
removeViewsInLayout(0, 1);
} else {
removeViewAt(0);
}
mRecycler.addScrap(child);
mFirstPosition++;
}
}
final void offsetChildren(int offset) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
child.layout(child.getLeft() + offset, child.getTop(), child.getRight() + offset, child.getBottom());
}
}
private boolean contentFits() {
final int childCount = getChildCount();
if (childCount == 0)
return true;
if (childCount != mItemCount)
return false;
return getChildAt(0).getLeft() >= getPaddingLeft() && getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
}
private void recycleAllViews() {
for (int i = 0; i < getChildCount(); i++) {
mRecycler.addScrap(getChildAt(i));
}
if (mInLayout) {
removeAllViewsInLayout();
} else {
removeAllViews();
}
}
private int fillRight(int pos, int overhang) {
int end = (getRight() - getLeft()) + overhang;
int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
while (nextLeft < end && pos < mItemCount) {
pos += makeAndAddColumn(pos, nextLeft, true);
nextLeft = getChildAt(getChildCount() - 1).getRight();
}
final int gridRight = getWidth() - getPaddingRight();
return getChildAt(getChildCount() - 1).getRight() - gridRight;
}
private int fillLeft(int pos, int overhang) {
int end = getPaddingLeft() - overhang;
int nextRight = getChildAt(0).getLeft();
while (nextRight > end && pos >= 0) {
pos -= makeAndAddColumn(pos, nextRight, false);
nextRight = getChildAt(0).getLeft();
}
mFirstPosition = pos + 1;
return getPaddingLeft() - getChildAt(0).getLeft();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int x = mScroller.getCurrX();
final int dx = (int) (x - mLastTouchX);
mLastTouchX = x;
final boolean stopped = !trackMotionScroll(dx, false);
if (!stopped && !mScroller.isFinished()) {
ViewCompat.postInvalidateOnAnimation(this);
} else {
if (stopped) {
final int overScrollMode = ViewCompat.getOverScrollMode(this);
if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
final EdgeEffectCompat edge;
if (dx > 0) {
edge = mLeftEdge;
} else {
edge = mRightEdge;
}
edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
ViewCompat.postInvalidateOnAnimation(this);
}
mScroller.abortAnimation();
}
mTouchMode = TOUCH_MODE_IDLE;
}
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (!mLeftEdge.isFinished()) {
final int restoreCount = canvas.save();
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
canvas.rotate(270);
canvas.translate(-height + getPaddingTop(), 0);
mLeftEdge.setSize(height, getWidth());
if (mLeftEdge.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mRightEdge.isFinished()) {
final int restoreCount = canvas.save();
final int width = getWidth();
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
canvas.rotate(90);
canvas.translate(-getPaddingTop(), width);
mRightEdge.setSize(height, width);
if (mRightEdge.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
/**
* Obtain a populated view from the adapter. If optScrap is non-null and is not
* reused it will be placed in the recycle bin.
*
* @param position position to get view for
* @param optScrap Optional scrap view; will be reused if possible
* @return A new view, a recycled view from mRecycler, or optScrap
*/
private final View obtainView(int position, View optScrap) {
View view = mRecycler.getTransientStateView(position);
if (view != null) {
return view;
}
// Reuse optScrap if it's of the right type (and not null)
final int optType = optScrap != null ? ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
final int positionViewType = mAdapter.gereplacedemViewType(position);
final View scrap = optType == positionViewType ? optScrap : mRecycler.getScrapView(positionViewType);
view = mAdapter.getView(position, scrap, this);
if (view != scrap && scrap != null) {
// The adapter didn't use it; put it back.
mRecycler.addScrap(scrap);
}
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (view.getParent() != this) {
if (lp == null) {
lp = generateDefaultLayoutParams();
} else if (!checkLayoutParams(lp)) {
lp = generateLayoutParams(lp);
}
view.setLayoutParams(lp);
}
final LayoutParams sglp = (LayoutParams) lp;
sglp.position = position;
sglp.viewType = positionViewType;
return view;
}
public GalleryThumbnailAdapter getAdapter() {
return mAdapter;
}
public void setAdapter(GalleryThumbnailAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mObserver);
}
// TODO: If the new adapter says that there are stable IDs, remove certain layout records
// and onscreen views if they have changed instead of removing all of the state here.
clearAllState();
mAdapter = adapter;
mDataChanged = true;
mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
if (adapter != null) {
adapter.registerDataSetObserver(mObserver);
mRecycler.setViewTypeCount(adapter.getViewTypeCount());
mHreplacedtableIds = adapter.hreplacedtableIds();
} else {
mHreplacedtableIds = false;
}
populate();
}
/**
* Clear all state because the grid will be used for a completely different set of data.
*/
private void clearAllState() {
// Clear all layout records and views
removeAllViews();
// Reset to the top of the grid
mFirstPosition = 0;
// Clear recycler because there could be different view types now
mRecycler.clear();
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new LayoutParams(lp);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
return lp instanceof LayoutParams;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public static clreplaced LayoutParams extends ViewGroup.LayoutParams {
private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_span };
private static final int SPAN_INDEX = 0;
/**
* The number of columns this item should span
*/
public int span = 1;
/**
* Item position this view represents
*/
int position;
/**
* Type of this view as reported by the adapter
*/
int viewType;
/**
* The column this view is occupying
*/
int column;
/**
* The stable ID of the item this view displays
*/
long id = -1;
public LayoutParams(int height) {
super(MATCH_PARENT, height);
if (this.height == MATCH_PARENT) {
Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
this.height = WRAP_CONTENT;
}
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
if (this.width != MATCH_PARENT) {
Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + " - must be MATCH_PARENT");
this.width = MATCH_PARENT;
}
if (this.height == MATCH_PARENT) {
Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
this.height = WRAP_CONTENT;
}
TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
span = a.getInteger(SPAN_INDEX, 1);
a.recycle();
}
public LayoutParams(ViewGroup.LayoutParams other) {
super(other);
if (this.width != MATCH_PARENT) {
Log.w(TAG, "Constructing LayoutParams with width " + this.width + " - must be MATCH_PARENT");
this.width = MATCH_PARENT;
}
if (this.height == MATCH_PARENT) {
Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
this.height = WRAP_CONTENT;
}
}
}
private clreplaced RecycleBin {
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
private int mMaxScrap;
private SparseArray<View> mTransientStateViews;
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Must have at least one view type (" + viewTypeCount + " types reported)");
}
if (viewTypeCount == mViewTypeCount) {
return;
}
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mScrapViews = scrapViews;
}
public void clear() {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
mScrapViews[i].clear();
}
if (mTransientStateViews != null) {
mTransientStateViews.clear();
}
}
public void clearTransientViews() {
if (mTransientStateViews != null) {
mTransientStateViews.clear();
}
}
public void addScrap(View v) {
final LayoutParams lp = (LayoutParams) v.getLayoutParams();
if (ViewCompat.hasTransientState(v)) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<View>();
}
mTransientStateViews.put(lp.position, v);
return;
}
final int childCount = getChildCount();
if (childCount > mMaxScrap) {
mMaxScrap = childCount;
}
ArrayList<View> scrap = mScrapViews[lp.viewType];
if (scrap.size() < mMaxScrap) {
scrap.add(v);
}
}
public View getTransientStateView(int position) {
if (mTransientStateViews == null) {
return null;
}
final View result = mTransientStateViews.get(position);
if (result != null) {
mTransientStateViews.remove(position);
}
return result;
}
public View getScrapView(int type) {
ArrayList<View> scrap = mScrapViews[type];
if (scrap.isEmpty()) {
return null;
}
final int index = scrap.size() - 1;
final View result = scrap.get(index);
scrap.remove(index);
return result;
}
}
private clreplaced AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
// TODO: Consider matching these back up if we have stable IDs.
mRecycler.clearTransientViews();
if (!mHreplacedtableIds) {
recycleAllViews();
}
// TODO: consider repopulating in a deferred runnable instead
// (so that successive changes may still be batched)
requestLayout();
}
@Override
public void onInvalidated() {
}
}
}
19
Source : SwipeMenuLayout.java
with Apache License 2.0
from yanzhenjie
with Apache License 2.0
from yanzhenjie
/**
* Created by Yan Zhenjie on 2016/7/27.
*/
public clreplaced SwipeMenuLayout extends FrameLayout implements Controller {
public static final int DEFAULT_SCROLLER_DURATION = 200;
private int mLeftViewId = 0;
private int mContentViewId = 0;
private int mRightViewId = 0;
private float mOpenPercent = 0.5f;
private int mScrollerDuration = DEFAULT_SCROLLER_DURATION;
private int mScaledTouchSlop;
private int mLastX;
private int mLastY;
private int mDownX;
private int mDownY;
private View mContentView;
private LeftHorizontal mSwipeLeftHorizontal;
private RightHorizontal mSwipeRightHorizontal;
private Horizontal mSwipeCurrentHorizontal;
private boolean shouldResetSwipe;
private boolean mDragging;
private boolean swipeEnable = true;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mScaledMinimumFlingVelocity;
private int mScaledMaximumFlingVelocity;
public SwipeMenuLayout(Context context) {
this(context, null);
}
public SwipeMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setClickable(true);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout);
mLeftViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_leftViewId, mLeftViewId);
mContentViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_contentViewId, mContentViewId);
mRightViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_rightViewId, mRightViewId);
typedArray.recycle();
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mScaledTouchSlop = configuration.getScaledTouchSlop();
mScaledMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mScaledMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
mScroller = new OverScroller(getContext());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mLeftViewId != 0 && mSwipeLeftHorizontal == null) {
View view = findViewById(mLeftViewId);
mSwipeLeftHorizontal = new LeftHorizontal(view);
}
if (mRightViewId != 0 && mSwipeRightHorizontal == null) {
View view = findViewById(mRightViewId);
mSwipeRightHorizontal = new RightHorizontal(view);
}
if (mContentViewId != 0 && mContentView == null) {
mContentView = findViewById(mContentViewId);
} else {
TextView errorView = new TextView(getContext());
errorView.setClickable(true);
errorView.setGravity(Gravity.CENTER);
errorView.setTextSize(16);
errorView.setText("You may not have set the ContentView.");
mContentView = errorView;
addView(mContentView);
}
}
/**
* Set whether open swipe. Default is true.
*
* @param swipeEnable true open, otherwise false.
*/
public void setSwipeEnable(boolean swipeEnable) {
this.swipeEnable = swipeEnable;
}
/**
* Open the swipe function of the Item?
*
* @return open is true, otherwise is false.
*/
public boolean isSwipeEnable() {
return swipeEnable;
}
/**
* Set open percentage.
*
* @param openPercent such as 0.5F.
*/
public void setOpenPercent(float openPercent) {
this.mOpenPercent = openPercent;
}
/**
* Get open percentage.
*
* @return such as 0.5F.
*/
public float getOpenPercent() {
return mOpenPercent;
}
/**
* The duration of the set.
*
* @param scrollerDuration such as 500.
*/
public void setScrollerDuration(int scrollerDuration) {
this.mScrollerDuration = scrollerDuration;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercepted = super.onInterceptTouchEvent(ev);
if (!isSwipeEnable()) {
return isIntercepted;
}
int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
{
mDownX = mLastX = (int) ev.getX();
mDownY = (int) ev.getY();
return false;
}
case MotionEvent.ACTION_MOVE:
{
int disX = (int) (ev.getX() - mDownX);
int disY = (int) (ev.getY() - mDownY);
return Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);
}
case MotionEvent.ACTION_UP:
{
boolean isClick = mSwipeCurrentHorizontal != null && mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());
if (isMenuOpen() && isClick) {
smoothCloseMenu();
return true;
}
return false;
}
case MotionEvent.ACTION_CANCEL:
{
if (!mScroller.isFinished())
mScroller.abortAnimation();
return false;
}
}
return isIntercepted;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isSwipeEnable()) {
return super.onTouchEvent(ev);
}
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
int dx;
int dy;
int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
{
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
break;
}
case MotionEvent.ACTION_MOVE:
{
int disX = (int) (mLastX - ev.getX());
int disY = (int) (mLastY - ev.getY());
if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {
mDragging = true;
}
if (mDragging) {
if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {
if (disX < 0) {
if (mSwipeLeftHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
} else {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
}
} else {
if (mSwipeRightHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
} else {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
}
}
}
scrollBy(disX, 0);
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
shouldResetSwipe = false;
}
break;
}
case MotionEvent.ACTION_UP:
{
dx = (int) (mDownX - ev.getX());
dy = (int) (mDownY - ev.getY());
mDragging = false;
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
int velocityX = (int) mVelocityTracker.getXVelocity();
int velocity = Math.abs(velocityX);
if (velocity > mScaledMinimumFlingVelocity) {
if (mSwipeCurrentHorizontal != null) {
int duration = getSwipeDuration(ev, velocity);
if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
if (velocityX < 0) {
smoothOpenMenu(duration);
} else {
smoothCloseMenu(duration);
}
} else {
if (velocityX > 0) {
smoothOpenMenu(duration);
} else {
smoothCloseMenu(duration);
}
}
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
judgeOpenClose(dx, dy);
}
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop || Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
{
mDragging = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
} else {
dx = (int) (mDownX - ev.getX());
dy = (int) (mDownY - ev.getY());
judgeOpenClose(dx, dy);
}
break;
}
}
return super.onTouchEvent(ev);
}
/**
* compute finish duration.
*
* @param ev up event.
* @param velocity velocity x.
*
* @return finish duration.
*/
private int getSwipeDuration(MotionEvent ev, int velocity) {
int sx = getScrollX();
int dx = (int) (ev.getX() - sx);
final int width = mSwipeCurrentHorizontal.getMenuWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
int duration;
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageDelta = (float) Math.abs(dx) / width;
duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, mScrollerDuration);
return duration;
}
float distanceInfluenceForSnapDuration(float f) {
// center the values about 0.
f -= 0.5f;
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
private void judgeOpenClose(int dx, int dy) {
if (mSwipeCurrentHorizontal != null) {
if (Math.abs(getScrollX()) >= (mSwipeCurrentHorizontal.getMenuView().getWidth() * mOpenPercent)) {
// auto open
if (Math.abs(dx) > mScaledTouchSlop || Math.abs(dy) > mScaledTouchSlop) {
// swipe up
if (isMenuOpenNotEqual()) {
smoothCloseMenu();
} else {
smoothOpenMenu();
}
} else {
// normal up
if (isMenuOpen()) {
smoothCloseMenu();
} else {
smoothOpenMenu();
}
}
} else {
// auto closeMenu
smoothCloseMenu();
}
}
}
@Override
public void scrollTo(int x, int y) {
if (mSwipeCurrentHorizontal == null) {
super.scrollTo(x, y);
} else {
Horizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);
shouldResetSwipe = checker.shouldResetSwipe;
if (checker.x != getScrollX()) {
super.scrollTo(checker.x, checker.y);
}
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset() && mSwipeCurrentHorizontal != null) {
if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
scrollTo(Math.abs(mScroller.getCurrX()), 0);
invalidate();
} else {
scrollTo(-Math.abs(mScroller.getCurrX()), 0);
invalidate();
}
}
}
public boolean hasLeftMenu() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();
}
public boolean hasRightMenu() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();
}
@Override
public boolean isMenuOpen() {
return isLeftMenuOpen() || isRightMenuOpen();
}
@Override
public boolean isLeftMenuOpen() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());
}
@Override
public boolean isRightMenuOpen() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());
}
@Override
public boolean isCompleteOpen() {
return isLeftCompleteOpen() || isRightMenuOpen();
}
@Override
public boolean isLeftCompleteOpen() {
return mSwipeLeftHorizontal != null && !mSwipeLeftHorizontal.isCompleteClose(getScrollX());
}
@Override
public boolean isRightCompleteOpen() {
return mSwipeRightHorizontal != null && !mSwipeRightHorizontal.isCompleteClose(getScrollX());
}
@Override
public boolean isMenuOpenNotEqual() {
return isLeftMenuOpenNotEqual() || isRightMenuOpenNotEqual();
}
@Override
public boolean isLeftMenuOpenNotEqual() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpenNotEqual(getScrollX());
}
@Override
public boolean isRightMenuOpenNotEqual() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpenNotEqual(getScrollX());
}
@Override
public void smoothOpenMenu() {
smoothOpenMenu(mScrollerDuration);
}
@Override
public void smoothOpenLeftMenu() {
smoothOpenLeftMenu(mScrollerDuration);
}
@Override
public void smoothOpenRightMenu() {
smoothOpenRightMenu(mScrollerDuration);
}
@Override
public void smoothOpenLeftMenu(int duration) {
if (mSwipeLeftHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
smoothOpenMenu(duration);
}
}
@Override
public void smoothOpenRightMenu(int duration) {
if (mSwipeRightHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
smoothOpenMenu(duration);
}
}
private void smoothOpenMenu(int duration) {
if (mSwipeCurrentHorizontal != null) {
mSwipeCurrentHorizontal.autoOpenMenu(mScroller, getScrollX(), duration);
invalidate();
}
}
@Override
public void smoothCloseMenu() {
smoothCloseMenu(mScrollerDuration);
}
@Override
public void smoothCloseLeftMenu() {
if (mSwipeLeftHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
smoothCloseMenu();
}
}
@Override
public void smoothCloseRightMenu() {
if (mSwipeRightHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
smoothCloseMenu();
}
}
@Override
public void smoothCloseMenu(int duration) {
if (mSwipeCurrentHorizontal != null) {
mSwipeCurrentHorizontal.autoCloseMenu(mScroller, getScrollX(), duration);
invalidate();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int contentViewHeight;
if (mContentView != null) {
int contentViewWidth = mContentView.getMeasuredWidthAndState();
contentViewHeight = mContentView.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) mContentView.getLayoutParams();
int start = getPaddingLeft();
int top = getPaddingTop() + lp.topMargin;
mContentView.layout(start, top, start + contentViewWidth, top + contentViewHeight);
}
if (mSwipeLeftHorizontal != null) {
View leftMenu = mSwipeLeftHorizontal.getMenuView();
int menuViewWidth = leftMenu.getMeasuredWidthAndState();
int menuViewHeight = leftMenu.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) leftMenu.getLayoutParams();
int top = getPaddingTop() + lp.topMargin;
leftMenu.layout(-menuViewWidth, top, 0, top + menuViewHeight);
}
if (mSwipeRightHorizontal != null) {
View rightMenu = mSwipeRightHorizontal.getMenuView();
int menuViewWidth = rightMenu.getMeasuredWidthAndState();
int menuViewHeight = rightMenu.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) rightMenu.getLayoutParams();
int top = getPaddingTop() + lp.topMargin;
int parentViewWidth = getMeasuredWidthAndState();
rightMenu.layout(parentViewWidth, top, parentViewWidth + menuViewWidth, top + menuViewHeight);
}
}
}
19
Source : RightHorizontal.java
with Apache License 2.0
from yanzhenjie
with Apache License 2.0
from yanzhenjie
@Override
public void autoOpenMenu(OverScroller scroller, int scrollX, int duration) {
scroller.startScroll(Math.abs(scrollX), 0, getMenuView().getWidth() - Math.abs(scrollX), 0, duration);
}
19
Source : RightHorizontal.java
with Apache License 2.0
from yanzhenjie
with Apache License 2.0
from yanzhenjie
@Override
public void autoCloseMenu(OverScroller scroller, int scrollX, int duration) {
scroller.startScroll(-Math.abs(scrollX), 0, Math.abs(scrollX), 0, duration);
}
19
Source : GingerScroller.java
with Apache License 2.0
from yanzhenjie
with Apache License 2.0
from yanzhenjie
@TargetApi(9)
public clreplaced GingerScroller extends ScrollerProxy {
protected final OverScroller mScroller;
public GingerScroller(Context context) {
mScroller = new OverScroller(context);
}
@Override
public boolean computeScrollOffset() {
return mScroller.computeScrollOffset();
}
@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
}
@Override
public void forceFinished(boolean finished) {
mScroller.forceFinished(finished);
}
@Override
public boolean isFinished() {
return mScroller.isFinished();
}
@Override
public int getCurrX() {
return mScroller.getCurrX();
}
@Override
public int getCurrY() {
return mScroller.getCurrY();
}
}
19
Source : AppBarLayoutBehavior.java
with Apache License 2.0
from yangchong211
with Apache License 2.0
from yangchong211
/**
* 停止appbarLayout的fling事件
* @param appBarLayout
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
// 通过反射拿到HeaderBehavior中的flingRunnable变量
try {
Field flingRunnableField = getFlingRunnableField();
Field scrollerField = getScrollerField();
if (flingRunnableField != null) {
flingRunnableField.setAccessible(true);
}
if (scrollerField != null) {
scrollerField.setAccessible(true);
}
Runnable flingRunnable = null;
if (flingRunnableField != null) {
flingRunnable = (Runnable) flingRunnableField.get(this);
}
OverScroller overScroller = null;
if (scrollerField != null) {
overScroller = (OverScroller) scrollerField.get(this);
}
// 下面是关键点
if (flingRunnable != null) {
LogUtil.d(TAG, "存在flingRunnable");
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
if (overScroller != null && !overScroller.isFinished()) {
overScroller.abortAnimation();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
19
Source : SmartDragLayout.java
with Apache License 2.0
from y1xian
with Apache License 2.0
from y1xian
/**
* Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
*/
public clreplaced SmartDragLayout extends FrameLayout implements NestedScrollingParent {
private static final String TAG = "SmartDragLayout";
private View child;
OverScroller scroller;
VelocityTracker tracker;
ShadowBgAnimator bgAnimator = new ShadowBgAnimator();
// 是否启用手势拖拽
boolean enableDrag = true;
boolean dismissOnTouchOutside = true;
boolean hreplacedhadowBg = true;
boolean isUserClose = false;
// 是否开启三段拖拽
boolean isThreeDrag = false;
LayoutStatus status = LayoutStatus.Close;
public SmartDragLayout(Context context) {
this(context, null);
}
public SmartDragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (enableDrag) {
scroller = new OverScroller(context);
}
}
int maxY;
int minY;
@Override
public void onViewAdded(View c) {
super.onViewAdded(c);
child = c;
}
int lastHeight;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
maxY = child.getMeasuredHeight();
minY = 0;
int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
if (enableDrag) {
// horizontal center
child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
if (status == LayoutStatus.Open) {
if (isThreeDrag) {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
} else {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
}
}
} else {
// like bottom gravity
child.layout(l, getMeasuredHeight() - child.getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight());
}
lastHeight = maxY;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
isUserClose = true;
return super.dispatchTouchEvent(ev);
}
float touchX, touchY;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (scroller.computeScrollOffset()) {
touchX = 0;
touchY = 0;
return false;
}
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (enableDrag) {
tracker = VelocityTracker.obtain();
}
touchX = event.getX();
touchY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (enableDrag) {
tracker.addMovement(event);
tracker.computeCurrentVelocity(1000);
int dy = (int) (event.getY() - touchY);
scrollTo(getScrollX(), getScrollY() - dy);
touchY = event.getY();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// click in child rect
Rect rect = new Rect();
child.getGlobalVisibleRect(rect);
if (!PopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
performClick();
}
} else {
}
if (enableDrag) {
float yVelocity = tracker.getYVelocity();
if (yVelocity > 1500 && !isThreeDrag) {
close();
} else {
finishScroll();
}
tracker.clear();
tracker.recycle();
}
break;
default:
break;
}
return true;
}
private void finishScroll() {
if (enableDrag) {
int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
if (isThreeDrag) {
int per = maxY / 3;
if (getScrollY() > per * 2.5f) {
dy = maxY - getScrollY();
} else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
dy = per * 2 - getScrollY();
} else if (getScrollY() > per) {
dy = per - getScrollY();
} else {
dy = minY - getScrollY();
}
}
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, PopupManager.getAnimationDuration());
ViewCompat.postInvalidateOnAnimation(this);
}
}
boolean isScrollUp;
@Override
public void scrollTo(int x, int y) {
if (y > maxY) {
y = maxY;
}
if (y < minY) {
y = minY;
}
float fraction = (y - minY) * 1f / (maxY - minY);
isScrollUp = y > getScrollY();
if (hreplacedhadowBg) {
setBackgroundColor(bgAnimator.calculateBgColor(fraction));
}
if (listener != null) {
if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
status = LayoutStatus.Close;
listener.onClose();
} else if (fraction == 1f && status != LayoutStatus.Open) {
status = LayoutStatus.Open;
listener.onOpen();
}
}
super.scrollTo(x, y);
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isScrollUp = false;
isUserClose = false;
setTranslationY(0);
}
public void open() {
status = LayoutStatus.Opening;
post(new Runnable() {
@Override
public void run() {
int dy = maxY - getScrollY();
smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
}
});
}
public void close() {
isUserClose = true;
status = LayoutStatus.Closing;
post(new Runnable() {
@Override
public void run() {
smoothScroll(minY - getScrollY(), false);
}
});
}
public void smoothScroll(final int dy, final boolean isOpen) {
post(new Runnable() {
@Override
public void run() {
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? PopupManager.getAnimationDuration() : PopupManager.getAnimationDuration() * 0.8f));
ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
}
});
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// 必须要取消,否则会导致滑动初次延迟
scroller.abortAnimation();
}
@Override
public void onStopNestedScroll(View target) {
finishScroll();
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0) {
// scroll up
int newY = getScrollY() + dy;
if (newY < maxY) {
// dy不一定能消费完
consumed[1] = dy;
}
scrollTo(getScrollX(), newY);
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
if (isDragging && velocityY < -1500 && !isThreeDrag) {
close();
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return ViewCompat.SCROLL_AXIS_VERTICAL;
}
public void isThreeDrag(boolean isThreeDrag) {
this.isThreeDrag = isThreeDrag;
}
public void enableDrag(boolean enableDrag) {
this.enableDrag = enableDrag;
}
public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
this.dismissOnTouchOutside = dismissOnTouchOutside;
}
public void hreplacedhadowBg(boolean hreplacedhadowBg) {
this.hreplacedhadowBg = hreplacedhadowBg;
}
private OnCloseListener listener;
public void setOnCloseListener(OnCloseListener listener) {
this.listener = listener;
}
public interface OnCloseListener {
void onClose();
void onOpen();
}
}
19
Source : FixAppBarLayoutBehavior.java
with Apache License 2.0
from y1xian
with Apache License 2.0
from y1xian
/**
* 停止appbarLayout的fling事件
* @param appBarLayout
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
// 通过反射拿到HeaderBehavior中的flingRunnable变量
try {
Field flingRunnableField = getFlingRunnableField();
Runnable flingRunnable;
if (flingRunnableField != null) {
flingRunnableField.setAccessible(true);
flingRunnable = (Runnable) flingRunnableField.get(this);
if (flingRunnable != null) {
Log.d(TAG, "存在flingRunnable");
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
}
Field scrollerField = getScrollerField();
if (scrollerField != null) {
scrollerField.setAccessible(true);
OverScroller overScroller = (OverScroller) scrollerField.get(this);
if (overScroller != null && !overScroller.isFinished()) {
overScroller.abortAnimation();
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
19
Source : GingerScroller.java
with Apache License 2.0
from xuexiangjys
with Apache License 2.0
from xuexiangjys
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public clreplaced GingerScroller extends ScrollerProxy {
protected final OverScroller mScroller;
public GingerScroller(Context context) {
mScroller = new OverScroller(context);
}
@Override
public boolean computeScrollOffset() {
return mScroller.computeScrollOffset();
}
@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
}
@Override
public void forceFinished(boolean finished) {
mScroller.forceFinished(finished);
}
@Override
public boolean isFinished() {
return mScroller.isFinished();
}
@Override
public int getCurrX() {
return mScroller.getCurrX();
}
@Override
public int getCurrY() {
return mScroller.getCurrY();
}
}
19
Source : AlipayScrollView.java
with Apache License 2.0
from xmuSistone
with Apache License 2.0
from xmuSistone
/**
* Created by xmuSistone on 2018/12/25.
*/
public clreplaced AlipayScrollView extends ScrollView {
private AlipayContainerLayout parentView;
private int[] SLOW_DOWN_STEP = new int[7];
private float lastProcessY;
private int downTouchOffset = 0;
private Spring marginSpring, scrollSpring;
private View topLayout;
private int progressColor;
private ProgressImageView progressImageView;
private int progressHeight, progressCenterOffset;
private View firstChildView;
private int firstViewPosition = 0;
private boolean refreshing = false;
private OnRefreshListener onRefreshListener;
private ScrollChangeListener scrollChangeListener;
private boolean flinging = false;
private OverScroller overScroller;
public AlipayScrollView(Context context) {
this(context, null);
}
public AlipayScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AlipayScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1. xml配置信息获取
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.alipay);
int defaultProgressHeight = dp2px(55);
this.progressHeight = typedArray.getDimensionPixelSize(R.styleable.alipay_progressHeight, defaultProgressHeight);
this.progressCenterOffset = typedArray.getDimensionPixelSize(R.styleable.alipay_progressCenterOffset, 0);
this.progressColor = typedArray.getColor(R.styleable.alipay_progressColor, Color.BLACK);
typedArray.recycle();
// 2. 初始化越界拖拽阻力参数
int step = dp2px(20);
int initSlowDownThreshold = progressHeight;
int index = 0;
for (int i = SLOW_DOWN_STEP.length - 1; i >= 0; i--) {
SLOW_DOWN_STEP[i] = initSlowDownThreshold + step * index;
index++;
}
// 3. 松手时的动画,用margin来做
SpringConfig springConfig = SpringConfig.fromOrigamiTensionAndFriction(3, 2);
SpringSystem mSpringSystem = SpringSystem.create();
marginSpring = mSpringSystem.createSpring().setSpringConfig(springConfig);
marginSpring.setOvershootClampingEnabled(true);
marginSpring.addListener(new SimpleSpringListener() {
@Override
public void onSpringUpdate(Spring spring) {
int yPos = (int) spring.getCurrentValue();
setFirstViewPosition(yPos);
onPositionChanged();
}
@Override
public void onSpringAtRest(Spring spring) {
super.onSpringAtRest(spring);
if (firstViewPosition < progressHeight / 2) {
refreshing = false;
progressImageView.stopProgress();
}
}
});
// 4. snap停靠,属性动画用腻了,还是用spring吧
scrollSpring = mSpringSystem.createSpring().setSpringConfig(springConfig);
scrollSpring.setOvershootClampingEnabled(true);
scrollSpring.addListener(new SimpleSpringListener() {
@Override
public void onSpringUpdate(Spring spring) {
int scrollY = (int) spring.getCurrentValue();
setScrollY(scrollY);
}
});
// 5. 反射获取scroller,这个是用来在computeScroll中判定ScrollView是否fling停止了,格外注意proguard不能混淆ScrollView
try {
Field scrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
scrollerField.setAccessible(true);
this.overScroller = (OverScroller) scrollerField.get(this);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public void computeScroll() {
super.computeScroll();
// 判定是否fling停止,用来判定是否需要snap的逻辑入口
if (flinging && overScroller.isFinished()) {
flinging = false;
if (null != scrollChangeListener) {
scrollChangeListener.onFlingStop();
}
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
parentView = (AlipayContainerLayout) getParent();
topLayout = parentView.getTopLayout();
progressImageView = parentView.getProgressImageView();
progressImageView.setProgressColor(progressColor);
firstChildView = ((ViewGroup) getChildAt(0)).getChildAt(0);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
adjustTopLayoutPos();
if (null != scrollChangeListener) {
scrollChangeListener.onScrollChange(t);
}
}
/**
* ScrollView上下滑动时,topLayout需要跟随滑动
*/
private void adjustTopLayoutPos() {
int topLayoutTop = -getScrollY();
if (topLayoutTop < -topLayout.getHeight()) {
topLayoutTop = -topLayout.getHeight();
} else if (topLayoutTop > 0) {
topLayoutTop = 0;
}
final int destLayoutTop = topLayoutTop;
topLayout.offsetTopAndBottom(destLayoutTop - topLayout.getTop());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
flinging = false;
lastProcessY = ev.getRawY();
downTouchOffset = refreshing ? firstViewPosition - progressHeight : firstViewPosition;
// 手指按下,动画结束
scrollSpring.setAtRest();
} else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
onDragRelease();
super.onInterceptTouchEvent(ev);
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
super.onTouchEvent(ev);
} else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
flinging = true;
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
onDragRelease();
super.onTouchEvent(ev);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
onTouchMove(ev);
}
return true;
}
/**
* 专门抽出一个函数用来处理touch拖动
*/
private void onTouchMove(MotionEvent ev) {
int distance = (int) (ev.getRawY() - lastProcessY);
if (getScrollY() == 0) {
if (firstViewPosition == 0) {
if (distance < 0) {
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
super.onTouchEvent(ev);
} else {
this.setFirstViewPosition(firstViewPosition + distance);
onPositionChanged();
}
} else {
// 1. scroll在最顶部,继续向下拉时添加阻力。此段代码就是让移动的distance缩小,越偏离顶部,阻力越大
int originDistance = distance;
distance = shrinkDragDistance(distance);
int newPosition = firstViewPosition + distance;
if (progressImageView.isRunning()) {
// 正在动画
if (firstViewPosition == progressHeight) {
if (originDistance > 0) {
// 向下拉,直接改变firstView的位置
setFirstViewPosition(newPosition);
} else {
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
super.onTouchEvent(ev);
}
} else {
if (newPosition < progressHeight) {
newPosition = progressHeight;
}
setFirstViewPosition(newPosition);
}
} else {
// 动画停止
if (newPosition < 0) {
newPosition = 0;
}
setFirstViewPosition(newPosition);
onPositionChanged();
}
}
lastProcessY = ev.getRawY();
} else {
lastProcessY = ev.getRawY();
if (parentView.getTouchingView() != this) {
ev.offsetLocation(0, downTouchOffset);
}
super.onTouchEvent(ev);
}
}
/**
* 拖动越界时,让距离缩水,以增加阻力;越界越多,阻力越大
*/
private int shrinkDragDistance(int distance) {
if (distance > 0) {
int tempPosition = firstViewPosition + distance;
if (tempPosition > SLOW_DOWN_STEP[0]) {
distance = distance / 128;
} else if (tempPosition > SLOW_DOWN_STEP[1]) {
distance = distance / 64;
} else if (tempPosition > SLOW_DOWN_STEP[2]) {
distance = distance / 32;
} else if (tempPosition > SLOW_DOWN_STEP[3]) {
distance = distance / 16;
} else if (tempPosition > SLOW_DOWN_STEP[4]) {
distance = distance / 8;
} else if (tempPosition > SLOW_DOWN_STEP[5]) {
distance = distance / 4;
} else if (tempPosition > SLOW_DOWN_STEP[6]) {
distance = distance / 2;
}
}
return distance;
}
private void onPositionChanged() {
// 1. 进度
float progress = (float) firstViewPosition / progressHeight;
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
if (!progressImageView.isRunning()) {
progressImageView.setStartEndTrim(progress * progress, progress * progress * 2);
}
// 2. 缩放
progressImageView.setPivotY(progressImageView.getHeight());
progressImageView.setScaleX(progress);
progressImageView.setScaleY(progress);
// 3. 位移
int originProgressPos = (progressHeight - progressImageView.getHeight()) / 2 + progressCenterOffset;
int progressDestPosition = (int) (originProgressPos + (1 - progress) * progressImageView.getHeight() / 5);
progressImageView.offsetTopAndBottom(progressDestPosition - progressImageView.getTop());
// 4. 透明度
float alpha = progress * 2;
if (alpha > 1) {
alpha = 1.0f;
}
progressImageView.setAlpha(alpha);
}
private void onDragRelease() {
int top = firstViewPosition;
if (top >= progressHeight) {
progressImageView.startProgress();
marginSpring.setAtRest();
marginSpring.setCurrentValue(top);
marginSpring.setEndValue(progressHeight);
if (!refreshing) {
refreshing = true;
if (null != onRefreshListener) {
onRefreshListener.onRefresh();
}
}
} else {
refreshing = false;
marginSpring.setAtRest();
marginSpring.setCurrentValue(top);
marginSpring.setEndValue(0);
}
}
/**
* 用margin的形式完成下拉位移
*/
public void setFirstViewPosition(int firstViewPosition) {
this.firstViewPosition = firstViewPosition;
ViewGroup.LayoutParams lp = firstChildView.getLayoutParams();
if (lp instanceof LinearLayout.LayoutParams) {
LinearLayout.LayoutParams castLp = (LinearLayout.LayoutParams) lp;
castLp.topMargin = firstViewPosition;
firstChildView.setLayoutParams(castLp);
} else if (lp instanceof RelativeLayout.LayoutParams) {
RelativeLayout.LayoutParams castLp = (RelativeLayout.LayoutParams) lp;
if (castLp.topMargin != firstViewPosition) {
castLp.topMargin = firstViewPosition;
firstChildView.setLayoutParams(castLp);
}
} else if (lp instanceof LayoutParams) {
LayoutParams castLp = (LayoutParams) lp;
if (castLp.topMargin != firstViewPosition) {
castLp.topMargin = firstViewPosition;
firstChildView.setLayoutParams(castLp);
}
}
}
public boolean isRefreshing() {
return refreshing;
}
/**
* 手动更新刷新状态
*/
public void setRefreshing(boolean refreshing) {
this.refreshing = refreshing;
this.marginSpring.setCurrentValue(firstViewPosition);
if (refreshing) {
progressImageView.startProgress();
marginSpring.setEndValue(progressHeight);
} else {
marginSpring.setEndValue(0);
}
}
/**
* dp和像素转换
*/
public int dp2px(float dipValue) {
float density = getContext().getResources().getDisplayMetrics().density;
return (int) (dipValue * density + 0.5f);
}
public int getProgressHeight() {
return progressHeight;
}
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
}
public void setScrollChangeListener(ScrollChangeListener scrollChangeListener) {
this.scrollChangeListener = scrollChangeListener;
}
public void updateProcessY(float rawY) {
this.lastProcessY = rawY;
}
public void snapTo(int scrollY) {
if (scrollY != getScrollY()) {
scrollSpring.setCurrentValue(getScrollY());
scrollSpring.setEndValue(scrollY);
}
}
public interface OnRefreshListener {
void onRefresh();
}
public interface ScrollChangeListener {
/**
* 滑动
*/
void onScrollChange(int scrollY);
/**
* 滑动结束监听
*/
void onFlingStop();
}
}
19
Source : StickyNavLayout.java
with Apache License 2.0
from xiaohaibin
with Apache License 2.0
from xiaohaibin
/**
* 顾修忠[email protected]/[email protected]
* Created by guxiuzhong on 2015/12/29.
* 上滑悬停控件,底部内容区域支持 ScrollView ,ListView,RecyclerView,GridViewWithHeaderAndFooter
*/
public clreplaced StickyNavLayout extends LinearLayout {
private static final String TAG = "StickyNavLayout";
private View mTop;
private View mNav;
private ViewPager mViewPager;
private int mTopViewHeight;
private ViewGroup mInnerScrollView;
private boolean isTopHidden = false;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private float mLastY;
private boolean mDragging;
private boolean isStickNav;
private boolean isInControl = false;
private int stickOffset;
private int mViewPagerMaxHeight;
private int mTopViewMaxHeight;
public StickyNavLayout(Context context) {
this(context, null);
}
public StickyNavLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StickyNavLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(LinearLayout.VERTICAL);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavLayout);
isStickNav = a.getBoolean(R.styleable.StickyNavLayout_isStickNav, false);
stickOffset = a.getDimensionPixelSize(R.styleable.StickyNavLayout_stickOffset, 0);
a.recycle();
mScroller = new OverScroller(context);
mVelocityTracker = VelocityTracker.obtain();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
}
public void setIsStickNav(boolean isStickNav) {
this.isStickNav = isStickNav;
}
/**
* 设置悬浮,并自动滚动到悬浮位置(即把top区域滚动上去)
*/
public void setStickNavAndScrollToNav() {
this.isStickNav = true;
scrollTo(0, mTopViewHeight);
}
/**
* *
* 设置顶部区域的高度
*
* @param height height
*/
public void setTopViewHeight(int height) {
mTopViewHeight = height;
mTopViewHeight -= stickOffset;
if (isStickNav) {
scrollTo(0, stickOffset);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
mNav = findViewById(R.id.id_stickynavlayout_indicator);
View view = findViewById(R.id.id_stickynavlayout_viewpager);
if (!(view instanceof ViewPager)) {
throw new RuntimeException("id_stickynavlayout_viewpager show used by ViewPager !");
} else if (mTop instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) mTop;
if (viewGroup.getChildCount() >= 2) {
throw new RuntimeException("if the TopView(android:id=\"R.id.id_stickynavlayout_topview\") is a ViewGroup(ScrollView,LinearLayout,FrameLayout, ....) ,the children count should be one !");
}
}
mViewPager = (ViewPager) view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "onMeasure---->>>>>>>>");
ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
// 修复键盘弹出后键盘关闭布局高度不对问题
int height = getMeasuredHeight() - mNav.getMeasuredHeight();
mViewPagerMaxHeight = (height >= mViewPagerMaxHeight ? height : mViewPagerMaxHeight);
params.height = /*mViewPagerMaxHeight - stickOffset*/
height - stickOffset;
mViewPager.setLayoutParams(params);
// 修复键盘弹出后Top高度不对问题
int topHeight = mTop instanceof ViewGroup ? ((ViewGroup) mTop).getChildAt(0).getMeasuredHeight() : mTop.getMeasuredHeight();
ViewGroup.LayoutParams topParams = mTop.getLayoutParams();
Log.d(TAG, "topHeight---->>>>>>>>" + topHeight);
mTopViewMaxHeight = (topHeight >= mTopViewMaxHeight ? topHeight : mTopViewMaxHeight);
topParams.height = /*mTopViewMaxHeight*/
topHeight;
mTop.setLayoutParams(topParams);
// 设置mTopViewHeight
mTopViewHeight = topParams.height - stickOffset;
Log.d(TAG, "onMeasure--mTopViewHeight:" + mTopViewHeight);
isTopHidden = getScrollY() == mTopViewHeight;
}
/**
* 更新top区域的视图,如果是处于悬浮状态,隐藏top区域的控件是不起作用的!!
*/
public void updateTopViews() {
if (isTopHidden) {
return;
}
final ViewGroup.LayoutParams params = mTop.getLayoutParams();
mTop.post(new Runnable() {
@Override
public void run() {
if (mTop instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) mTop;
int height = viewGroup.getChildAt(0).getHeight();
mTopViewHeight = height - stickOffset;
params.height = height;
mTop.setLayoutParams(params);
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
mTopViewHeight = mTop.getMeasuredHeight() - stickOffset;
}
}
});
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
final ViewGroup.LayoutParams params = mTop.getLayoutParams();
Log.d(TAG, "onSizeChanged-mTopViewHeight:" + mTopViewHeight);
mTop.post(new Runnable() {
@Override
public void run() {
if (mTop instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) mTop;
int height = viewGroup.getChildAt(0).getHeight();
mTopViewHeight = height - stickOffset;
params.height = height;
mTop.setLayoutParams(params);
mTop.requestLayout();
} else {
mTopViewHeight = mTop.getMeasuredHeight() - stickOffset;
}
Log.d(TAG, "mTopViewHeight:" + mTopViewHeight);
if (null != mInnerScrollView) {
Log.d(TAG, "mInnerScrollViewHeight:" + mInnerScrollView.getMeasuredHeight());
}
if (isStickNav) {
scrollTo(0, mTopViewHeight);
}
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
float y = ev.getY();
switch(action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
getCurrentScrollView();
if (mInnerScrollView instanceof ScrollView) {
if (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0 && !isInControl) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
isSticky = true;
return dispatchTouchEvent(ev2);
}
} else if (mInnerScrollView instanceof ListView) {
ListView lv = (ListView) mInnerScrollView;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
if (!isInControl && c != null && c.getTop() == 0 && isTopHidden && dy > 0) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
isSticky = true;
return dispatchTouchEvent(ev2);
}
} else if (mInnerScrollView instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) mInnerScrollView;
if (rv.getLayoutManager() == null) {
throw new IllegalStateException("RecyclerView does not have LayoutManager instance.");
}
View c = rv.getChildAt(0);
if (!isInControl && c != null && c.getTop() == 0 && /*android.support.v4.view.ViewCompat.canScrollVertically(rv, -1)*/
isTopHidden && dy > 0) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
isSticky = true;
return dispatchTouchEvent(ev2);
}
}
break;
case MotionEvent.ACTION_CANCEL:
case // 处理悬停后立刻抬起的处理
MotionEvent.ACTION_UP:
float distance = y - mLastY;
if (isSticky && /*distance==0.0f*/
Math.abs(distance) <= mTouchSlop) {
isSticky = false;
return true;
} else {
isSticky = false;
return super.dispatchTouchEvent(ev);
}
default:
break;
}
return super.dispatchTouchEvent(ev);
}
// mNav-view 是否悬停的标志
private boolean isSticky;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
float y = ev.getY();
switch(action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
getCurrentScrollView();
if (Math.abs(dy) > mTouchSlop) {
mDragging = true;
if (mInnerScrollView instanceof ScrollView) {
// 如果topView没有隐藏
// 或sc的scrollY = 0 && topView隐藏 && 下拉,则拦截
if (!isTopHidden || (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
} else if (mInnerScrollView instanceof ListView) {
ListView lv = (ListView) mInnerScrollView;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
if (!isTopHidden || (c != null && c.getTop() == 0 && isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
} else {
if (lv.getAdapter() != null && lv.getAdapter().getCount() == 0) {
// 当ListView或ScrollView 没有数据为空时
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
}
} else if (mInnerScrollView instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) mInnerScrollView;
if (rv.getLayoutManager() == null) {
throw new IllegalStateException("RecyclerView does not have LayoutManager instance.");
}
View c = rv.getChildAt(0);
if (!isTopHidden || (c != null && c.getTop() == 0 && /*!android.support.v4.view.ViewCompat.canScrollVertically(rv, -1)*/
isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
} else {
if (rv.getAdapter() != null && rv.getAdapter().gereplacedemCount() == 0) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mDragging = false;
recycleVelocityTracker();
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
private void getCurrentScrollView() {
int currenreplacedem = mViewPager.getCurrenreplacedem();
PagerAdapter a = mViewPager.getAdapter();
if (a instanceof FragmentPagerAdapter) {
FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
Fragment item = fadapter.gereplacedem(currenreplacedem);
View v = item.getView();
if (v != null) {
mInnerScrollView = (ViewGroup) (v.findViewById(R.id.id_stickynavlayout_innerscrollview));
}
} else if (a instanceof FragmentStatePagerAdapter) {
FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
Fragment item = fsAdapter.gereplacedem(currenreplacedem);
View v = item.getView();
if (v != null) {
mInnerScrollView = (ViewGroup) (v.findViewById(R.id.id_stickynavlayout_innerscrollview));
}
} else {
throw new RuntimeException("mViewPager should be used FragmentPagerAdapter or FragmentStatePagerAdapter !");
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(event);
int action = event.getAction();
float y = event.getY();
switch(action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastY = y;
return true;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
if (!mDragging && Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (mDragging) {
scrollBy(0, (int) -dy);
// 如果topView隐藏,且上滑动时,则改变当前事件为ACTION_DOWN
if (getScrollY() == mTopViewHeight && dy < 0) {
event.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(event);
isInControl = false;
isSticky = true;
} else {
isSticky = false;
}
}
mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
recycleVelocityTracker();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
mDragging = false;
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) mVelocityTracker.getYVelocity();
if (Math.abs(velocityY) > mMinimumVelocity) {
fling(-velocityY);
}
recycleVelocityTracker();
break;
default:
break;
}
return super.onTouchEvent(event);
}
public void fling(int velocityY) {
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
isTopHidden = getScrollY() == mTopViewHeight;
// set listener 设置悬浮监听回调
if (listener != null) {
// if(lastIsTopHidden!=isTopHidden){
// lastIsTopHidden=isTopHidden;
listener.isStick(isTopHidden);
// }
listener.scrollPercent((float) getScrollY() / (float) mTopViewHeight);
}
}
// private boolean lastIsTopHidden;//记录上次是否悬浮
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
public int getStickOffset() {
return stickOffset;
}
public void setStickOffset(int stickOffset) {
this.stickOffset = stickOffset;
}
private onStickStateChangeListener listener;
/**
* 悬浮状态回调
*/
public interface onStickStateChangeListener {
/**
* 是否悬浮的回调
*
* @param isStick true 悬浮 ,false 没有悬浮
*/
void isStick(boolean isStick);
/**
* 距离悬浮的距离的百分比
*
* @param percent 0~1(向上) or 1~0(向下) 的浮点数
*/
void scrollPercent(float percent);
}
public void setOnStickStateChangeListener(onStickStateChangeListener listener) {
this.listener = listener;
}
}
19
Source : PullToRefreshView.java
with Apache License 2.0
from wuchao226
with Apache License 2.0
from wuchao226
/**
* Created by Anthony on 2016/7/18.
* 实现对子view 的上拉和下拉的监听实现,提供下拉和下拉的视图和接口
*/
public clreplaced PullToRefreshView extends ViewGroup {
private LayoutInflater mInflater;
private OverScroller mScroller;
private OnRefreshListener mListener;
/**
* 头部View
*/
private View header;
private BaseIndicator mHeaderIndicator;
private String mHeaderIndicatorClreplacedName;
/**
* 尾部View
*/
private View footer;
private BaseIndicator mFooterIndicator;
private String mFooterIndicatorClreplacedName;
/**
* 内容View
*/
private View contentView;
private int mHeaderActionPosition;
private int mFooterActionPosition;
private int mHeaderHoldingPosition;
private int mFooterHoldingPosition;
private boolean isPullDownEnable = true;
private boolean isPullUpEnable = true;
private float mLastX;
private float mLastY;
private float deltaX = 0;
private float deltaY = 0;
private int IDLE = 0;
private int PULL_DOWN = 1;
private int PULL_UP = 2;
// 自动刷新时的状态
private int AUTO_SCROLL_PULL_DOWN = 3;
private int mStatus = IDLE;
private boolean isLoading = false;
private long mStartLoadingTime;
public PullToRefreshView(Context context) {
super(context);
}
public PullToRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PullToRefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
mInflater = LayoutInflater.from(context);
mScroller = new OverScroller(context);
// 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
if (ta.hasValue(R.styleable.PullToRefresh_header_indicator)) {
mHeaderIndicatorClreplacedName = ta.getString(R.styleable.PullToRefresh_header_indicator);
}
if (ta.hasValue(R.styleable.PullToRefresh_footer_indicator)) {
mFooterIndicatorClreplacedName = ta.getString(R.styleable.PullToRefresh_footer_indicator);
}
ta.recycle();
}
@Override
protected void onFinishInflate() {
if (getChildCount() != 1) {
throw new RuntimeException("The child of VIPullToRefresh should be only one!!!");
}
contentView = getChildAt(0);
setPadding(0, 0, 0, 0);
contentView.setPadding(0, 0, 0, 0);
mHeaderIndicator = getIndicator(mHeaderIndicatorClreplacedName);
if (mHeaderIndicator == null) {
mHeaderIndicator = new DefaultHeader();
}
header = mHeaderIndicator.createView(mInflater, this);
mFooterIndicator = getIndicator(mFooterIndicatorClreplacedName);
if (mFooterIndicator == null) {
mFooterIndicator = new DefaultFooter();
}
footer = mFooterIndicator.createView(mInflater, this);
contentView.bringToFront();
super.onFinishInflate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
mHeaderActionPosition = header.getMeasuredHeight() / 3 + header.getMeasuredHeight();
mFooterActionPosition = footer.getMeasuredHeight() / 3 + footer.getMeasuredHeight();
mHeaderHoldingPosition = header.getMeasuredHeight();
mFooterHoldingPosition = footer.getMeasuredHeight();
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (contentView != null) {
if (header != null) {
header.layout(0, -header.getMeasuredHeight(), getWidth(), 0);
}
if (footer != null) {
footer.layout(0, getHeight(), getWidth(), getHeight() + footer.getMeasuredHeight());
}
// if (header != null) {
// header.layout(0, 0, getWidth(), header.getMeasuredHeight());
// }
// if (footer != null) {
// footer.layout(0, getHeight() - footer.getMeasuredHeight(), getWidth(), getHeight());
// }
contentView.layout(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
}
}
private boolean isInControl = false;
private boolean isNeedIntercept;
private boolean isNeedIntercept() {
if (deltaY > 0 && isContentScrollToTop() || getScrollY() < 0 - 10) {
mStatus = PULL_DOWN;
return true;
}
if (deltaY < 0 && isContentScrollToBottom() || getScrollY() > 0 + 10) {
mStatus = PULL_UP;
return true;
}
return false;
}
/**
* dispatchTouchEvent主要用于记录触摸事件的初始状态
* 因为这里是所有触摸事件的入口函数所有事件都会经过这里
* <p>
* 因此如果你需要观测整个事件序列从开始到最后,无论它是否被本ViewGroup拦截,那么请在这个函数中进行
* <p>
* 为什么不在onInterceptTouchEvent中观测??
* 因为在onInterceptTouchEvent中进行记录可能漏掉一些事件
* 例如:当布局内部控件在onTouchEvent函数中对DOWN返回true,那么后续的MOVE事件和UP事件就可能不会传入onInterceptTouchEvent函数中
* 再比如:当本ViewGroup自身对第一个MOVE事件进行拦截即onInterceptTouchEvent返回true时后续的MOVE和UP事件都不会传入onInterceptTouchEvent函数中
* 而是直接进入onTouchEvent中去
*/
private VelocityTracker mVelocityTracker;
private float yVelocity;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
dealMulreplacedouch(ev);
final int action = MotionEventCompat.getActionMasked(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(ev);
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(500);
yVelocity = mVelocityTracker.getYVelocity();
isNeedIntercept = isNeedIntercept();
if (isNeedIntercept && !isInControl) {
// 把内部控件的事件转发给本控件处理
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
break;
case MotionEvent.ACTION_UP:
/**
* 为什么将本ViewGroup自动返回初始位置的触发函数放在dispatchTouchEvent中?
* 由于下面onTouchEvent代码中ACTION_MOVE时有一段对本ViewGroup当前事件控制权转移给内部控件的代码
* 因此这会使得最后的Up event不会到本ViewGroup中的onTouchEvent中去
* 所以只能将autoBackToOriginalPosition()前移到dispatchTouchEvent()中来
*/
autoBackToPosition();
isNeedIntercept = false;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isNeedIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
if (!isPullDownEnable && deltaY > 0 && getScrollY() <= 0) {
break;
}
if (!isPullUpEnable && deltaY < 0 && getScrollY() >= 0) {
break;
}
if (isNeedIntercept) {
if (mStatus == PULL_DOWN && getScrollY() > 0) {
break;
}
if (mStatus == PULL_UP && getScrollY() < 0) {
break;
}
scrollBy(0, (int) (-getMoveFloat(yVelocity, deltaY)));
updateIndicator();
} else {
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
isInControl = false;
}
break;
case MotionEvent.ACTION_UP:
autoBackToPosition();
return true;
}
return true;
}
/**
* 处理多点触控的情况
* 记录手指按下和移动时的各种相关数值
* mActivePointerId为有效手指的ID,后续所有移动数值均来自这个手指
* 当前active的手指只有一个且为后按下的那个
*/
private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
public void dealMulreplacedouch(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
mLastX = x;
mLastY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE:
{
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
// 下拉时deltaY为正,上拉时deltaY为负
deltaX = x - mLastX;
deltaY = y - mLastY;
mLastY = y;
mLastX = x;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_DOWN:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId != mActivePointerId) {
mLastX = MotionEventCompat.getX(ev, pointerIndex);
mLastY = MotionEventCompat.getY(ev, pointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
{
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastX = MotionEventCompat.getX(ev, newPointerIndex);
mLastY = MotionEventCompat.getY(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
break;
}
}
}
@Override
public void computeScroll() {
// 先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
// 这里调用View的scrollTo()完成实际的滚动
scrollTo(0, mScroller.getCurrY());
// 立即重绘View实现滚动效果
invalidate();
}
}
/**
* 速度控制函数
* 当手指移动速度越接近10000px每500毫秒时获得的控件移动距离越小
* 1.5f为权重,越大速度控制越明显,表现为用户越不容易拉动本控件,即拉动越吃力
* <p>
* 若不进行速度控制,将可能导致一系列问题,其中包括
* 用户下拉一段距离,突然很快加速上拉控件,这时footer将被拖出,但此时内部控件并未到达它的底部
* 这样显示不符合上拉加载的逻辑
*/
private float getMoveFloat(float velocity, float org) {
return ((10000f - Math.abs(velocity)) / 10000f * org) / 1.5f;
}
/**
* 判断该回到初始状态还是Loading状态
*/
private void autoBackToPosition() {
if (mStatus == PULL_DOWN && Math.abs(getScrollY()) < mHeaderActionPosition) {
autoBackToOriginalPosition();
} else if (mStatus == PULL_DOWN && Math.abs(getScrollY()) > mHeaderActionPosition) {
autoBackToLoadingPosition();
} else if (mStatus == PULL_UP && Math.abs(getScrollY()) < mFooterActionPosition) {
autoBackToOriginalPosition();
} else if (mStatus == PULL_UP && Math.abs(getScrollY()) > mFooterActionPosition) {
autoBackToLoadingPosition();
}
}
/**
* 回到Loading状态
*/
private void autoBackToLoadingPosition() {
mStartLoadingTime = System.currentTimeMillis();
if (mStatus == PULL_DOWN) {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - mHeaderHoldingPosition, 400);
invalidate();
if (!isLoading) {
isLoading = true;
if (mListener != null)
mListener.onRefresh();
}
}
if (mStatus == PULL_UP) {
mScroller.startScroll(0, getScrollY(), 0, mFooterHoldingPosition - getScrollY(), 400);
invalidate();
if (!isLoading) {
isLoading = true;
if (mListener != null)
mListener.onLoadMore();
}
}
loadingIndicator();
}
/**
* 回到初始状态
*/
private void autoBackToOriginalPosition() {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 400);
invalidate();
this.postDelayed(new Runnable() {
@Override
public void run() {
restoreIndicator();
mStatus = IDLE;
}
}, 500);
}
private boolean isContentScrollToTop() {
return !ViewCompat.canScrollVertically(contentView, -1);
}
private boolean isContentScrollToBottom() {
return !ViewCompat.canScrollVertically(contentView, 1);
}
/**
* 在拖动过程中调用Indicator(Header或Footer)的接口函数完成相应的指示性变化
* 例如:下拉刷新、放开刷新的变化
*/
private void updateIndicator() {
if (mStatus == PULL_DOWN && deltaY > 0) {
if (Math.abs(getScrollY()) > mHeaderActionPosition) {
mHeaderIndicator.onAction();
}
} else if (mStatus == PULL_DOWN && deltaY < 0) {
if (Math.abs(getScrollY()) < mHeaderActionPosition) {
mHeaderIndicator.onUnaction();
}
} else if (mStatus == PULL_UP && deltaY < 0) {
if (Math.abs(getScrollY()) > mFooterActionPosition) {
mFooterIndicator.onAction();
}
} else if (mStatus == PULL_UP && deltaY > 0) {
if (Math.abs(getScrollY()) < mFooterActionPosition) {
mFooterIndicator.onUnaction();
}
}
}
/**
* 本控件自动返回初始位置后恢复Indicator到初始状态
*/
private void restoreIndicator() {
mHeaderIndicator.onRestore();
mFooterIndicator.onRestore();
}
/**
* 本控件自动返回Loading位置后设置Indicator为Loading状态
*/
private void loadingIndicator() {
if (mStatus == PULL_DOWN) {
mHeaderIndicator.onLoading();
}
if (mStatus == PULL_UP) {
mFooterIndicator.onLoading();
}
}
public void onFinishLoading() {
long delta = System.currentTimeMillis() - mStartLoadingTime;
if (delta > 2000) {
isLoading = false;
autoBackToOriginalPosition();
} else {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
isLoading = false;
autoBackToOriginalPosition();
}
}, 1000);
}
}
public void onAutoRefresh() {
mStartLoadingTime = System.currentTimeMillis();
mStatus = PULL_DOWN;
mScroller.startScroll(0, getScrollY(), 0, -mHeaderHoldingPosition, 400);
invalidate();
if (!isLoading) {
isLoading = true;
if (mListener != null)
mListener.onRefresh();
}
loadingIndicator();
}
/**
* Interface
*/
public interface OnRefreshListener {
void onRefresh();
void onLoadMore();
}
private BaseIndicator getIndicator(String clreplacedName) {
if (!TextUtils.isEmpty(clreplacedName)) {
try {
Clreplaced clazz = Clreplaced.forName(clreplacedName);
return (BaseIndicator) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* Getter and Setter
*/
public void setListener(OnRefreshListener mListener) {
this.mListener = mListener;
}
public boolean isPullDownEnable() {
return isPullDownEnable;
}
public void setPullDownEnable(boolean pullDownEnable) {
isPullDownEnable = pullDownEnable;
}
public boolean isPullUpEnable() {
return isPullUpEnable;
}
public void setPullUpEnable(boolean pullUpEnable) {
isPullUpEnable = pullUpEnable;
}
}
19
Source : TvGridLayout.java
with Apache License 2.0
from woshidasusu
with Apache License 2.0
from woshidasusu
/**
* Created by dasu on 2018/4/23.
* 微信公众号:dasuAndroidTv
* blog:https://www.jianshu.com/u/bb52a2918096
*
* 网格容器
*/
public clreplaced TvGridLayout extends FrameLayout {
private static final String TAG = "TvGridLayout";
// 滑动的时长
private static final int ANIMATED_SCROLL_GAP = 500;
private static Interpolator sInterpolator = new AccelerateDecelerateInterpolator();
private static int[] sTwoInt = new int[2];
private Context mContext;
private OverScroller mScroller;
private long mLastScroll;
private int mCurPageIndex = 0;
private int mRightEdge;
private boolean mIsOnScrolling;
private Adapter mAdapter;
private int mWidth;
private int mHeight;
private int mItemSpace;
private boolean mIsConsumeKeyEvent;
private SparseArray<View> mFirstChildOfPage = new SparseArray<>();
private SparseIntArray mWidthOfPage = new SparseIntArray();
private OnBorderListener mBorderListener;
private OnScrollListener mScrollListener;
public TvGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
setHorizontalScrollBarEnabled(false);
mScroller = new OverScroller(context, sInterpolator);
setClipChildren(false);
setClipToPadding(false);
}
public TvGridLayout(Context context) {
super(context);
init(context);
}
public void sereplacedemSpace(int itemSpace) {
mItemSpace = itemSpace;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.mWidth = w;
this.mHeight = h;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int x = mScroller.getCurrX();
int finalX = mScroller.getFinalX();
if (oldX != x) {
scrollTo(x, 0);
}
if (x == finalX) {
if (mIsOnScrolling) {
mIsOnScrolling = false;
if (mScrollListener != null) {
mScrollListener.onScrollEnd();
}
}
}
} else {
if (mIsOnScrolling) {
mIsOnScrolling = false;
if (mScrollListener != null) {
mScrollListener.onScrollEnd();
}
}
}
}
public Adapter getAdapter() {
return mAdapter;
}
public void setAdapter(Adapter adapter) {
if (mAdapter == adapter) {
return;
}
if (mAdapter != null) {
mAdapter.onSwitchAdapter(adapter, mAdapter);
}
mAdapter = adapter;
if (mAdapter != null) {
post(new Runnable() {
@Override
public void run() {
removeAllViews();
layoutChildren();
}
});
}
}
private void layoutChildren() {
mFirstChildOfPage.clear();
mWidthOfPage.clear();
mRightEdge = 0;
mCurPageIndex = 0;
// layoutChildrenOfPages(0, mAdapter.getGroupCount());
if (this.getLocalVisibleRect(new Rect())) {
layoutChildrenOfPages(0, 1);
post(new Runnable() {
@Override
public void run() {
layoutChildrenOfPages(1, mAdapter.getPageCount());
}
});
} else {
post(new Runnable() {
@Override
public void run() {
layoutChildrenOfPages(0, mAdapter.getPageCount());
}
});
}
}
private void layoutChildrenOfPages(int fromPage, int toPage) {
int contentWidth = mWidth - getPaddingLeft() - getPaddingRight();
int contentHeight = mHeight - getPaddingTop() - getPaddingBottom();
for (int j = fromPage; j < toPage; j++) {
// 列数
int column = mAdapter.getPageColumn(j);
// 行数
int row = mAdapter.getPageRow(j);
// 每个item宽度
float itemWidth = (contentWidth) * 1.0f / column;
// 每个item高度
float itemHeight = (contentHeight) * 1.0f / row;
int pageWidth = 0;
// 遍历每个item
for (int i = 0; i < mAdapter.getChildCount(j); i++) {
ItemCoordinate childCoordinate = mAdapter.getChildCoordinate(j, i);
if (childCoordinate == null) {
continue;
}
int pointStartX = childCoordinate.start.x;
int pointStartY = childCoordinate.start.y;
int pointEndX = childCoordinate.end.x;
int pointEndY = childCoordinate.end.y;
// item大小,包括间距
int width = (int) ((pointEndX - pointStartX) * itemWidth);
int height = (int) ((pointEndY - pointStartY) * itemHeight);
// item位置
int marginLeft = (int) (pointStartX * itemWidth + contentWidth * j);
int marginTop = (int) (pointStartY * itemHeight);
if (marginLeft < 0) {
marginLeft = 0;
}
if (marginTop < 0) {
marginTop = 0;
}
// 获取item view
View view = mAdapter.getChildView(j, i, width, height);
if (view == null) {
continue;
}
// 开始layout
// 扣除间距
LayoutParams params = new LayoutParams(width - mItemSpace * 2, height - mItemSpace * 2);
params.topMargin = marginTop + mItemSpace;
params.leftMargin = marginLeft + mItemSpace;
params.mItemCoordinate = childCoordinate;
params.pageIndex = j;
// 记录每一页长度
int maxWidth = marginLeft + width - contentWidth * j;
pageWidth = Math.max(pageWidth, maxWidth);
int maxRight = marginLeft + width;
mRightEdge = Math.max(mRightEdge, maxRight);
if (childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
mFirstChildOfPage.put(j, view);
}
if (j == 0 && childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
addView(view, 0, params);
} else {
addView(view, params);
}
}
mWidthOfPage.put(j, pageWidth);
}
}
public View getFirstChildOfScreen(int screenIndex) {
if (mFirstChildOfPage != null) {
return mFirstChildOfPage.get(screenIndex);
}
return null;
}
public void setOnBorderListener(OnBorderListener listener) {
mBorderListener = listener;
}
public void smoothScrollTo(int dx) {
smoothScrollBy(dx - getScrollX());
}
public void smoothScrollBy(int dx) {
if (getChildCount() == 0) {
// Nothing to do.
return;
}
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
final int width = getWidth() - getPaddingLeft();
final int rightEdge = mRightEdge + getPaddingRight();
final int maxX = Math.max(0, rightEdge - width);
if (duration > ANIMATED_SCROLL_GAP) {
if (mScrollListener != null) {
mScrollListener.onScrollStart();
}
mIsOnScrolling = true;
final int scrollX = getScrollX();
dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
mScroller.startScroll(scrollX, 0, dx, 0, ANIMATED_SCROLL_GAP);
postInvalidateOnAnimation();
} else {
int finalX = 0;
boolean needAdjustScrollX = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
finalX = mScroller.getFinalX();
needAdjustScrollX = true;
}
dx = Math.max(0, Math.min(finalX + dx, maxX)) - finalX;
if (needAdjustScrollX) {
dx = finalX + dx;
} else {
dx = getScrollX() + dx;
}
if (mScrollListener != null) {
mScrollListener.onScrollStart();
}
scrollTo(dx, getScrollY());
if (mScrollListener != null) {
post(new Runnable() {
@Override
public void run() {
mScrollListener.onScrollEnd();
}
});
}
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
private boolean executeKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mIsConsumeKeyEvent = false;
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
if (checkIfOnBorder(FOCUS_LEFT, sTwoInt)) {
mIsConsumeKeyEvent = true;
if (mBorderListener != null && mBorderListener.onLeft(sTwoInt[0], sTwoInt[1])) {
return true;
}
scrollToPage(sTwoInt[1]);
}
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (checkIfOnBorder(FOCUS_RIGHT, sTwoInt)) {
mIsConsumeKeyEvent = true;
if (mBorderListener != null && mBorderListener.onRight(sTwoInt[0], sTwoInt[1])) {
return true;
}
scrollToPage(sTwoInt[1]);
}
}
} else {
if (mIsConsumeKeyEvent) {
return true;
}
}
return false;
}
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
// GridMenuLayout与ViewPager合用时,当GridMenuLayout焦点在边界移动时,会去触发ViewPager的切菜单事件
// 对于下个菜单的GridMenuLayout,需要默认聚焦到第一个子View,使用系统默认的焦点寻找策略会出问题
// 所以在这个回调里进行处理
if (previouslyFocusedRect == null && (direction != FOCUS_UP && direction != FOCUS_DOWN)) {
final View view = mFirstChildOfPage.get(mCurPageIndex);
if (view != null) {
view.post(new Runnable() {
@Override
public void run() {
view.requestFocus();
}
});
}
}
return super.requestFocus(direction, previouslyFocusedRect);
}
private boolean checkIfOnBorder(int direction, int[] twoPage) {
if (direction == FOCUS_LEFT || direction == FOCUS_RIGHT) {
View view = getFocusedChild();
View childView = findChildView(view);
if (childView != null) {
int curPage = ((LayoutParams) childView.getLayoutParams()).pageIndex;
twoPage[0] = twoPage[1] = curPage;
View nextFocusView = view.focusSearch(direction);
View nextChildView = findChildView(nextFocusView);
if (nextChildView == null) {
return true;
}
int nextPage = ((LayoutParams) nextChildView.getLayoutParams()).pageIndex;
twoPage[1] = nextPage;
return curPage != nextPage;
}
}
return false;
}
public void scrollToPage(int pageIndex) {
scrollToPage(pageIndex, true);
}
private View findChildView(View view) {
View childView = null;
if (view != null) {
if (view.getParent() == this) {
childView = view;
} else {
boolean isChild = false;
ViewParent parent = view.getParent();
for (; parent.getParent() instanceof ViewGroup; ) {
if (parent.getParent() == this) {
isChild = true;
break;
}
parent = parent.getParent();
}
if (isChild) {
childView = (View) parent;
}
}
}
return childView;
}
public void scrollToPage(int pageIndex, boolean smooth) {
if (mCurPageIndex != pageIndex) {
int pageWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int scrollXDelta = (pageIndex - mCurPageIndex) * (pageWidth);
// todo 计算有问题
if (mCurPageIndex == 0 || mCurPageIndex == mAdapter.getPageCount() - 1) {
int w = mWidthOfPage.get(mCurPageIndex, 0);
if (pageWidth != w) {
int adjustW = pageWidth - w;
scrollXDelta += scrollXDelta > 0 ? -adjustW : adjustW;
}
}
if (scrollXDelta != 0) {
if (smooth) {
smoothScrollBy(scrollXDelta);
} else {
final int width = getWidth();
final int rightEdge = mRightEdge + getPaddingRight();
final int maxX = Math.max(0, rightEdge - width);
final int scrollX = getScrollX();
int dx = Math.max(0, Math.min(scrollX + scrollXDelta, maxX)) - scrollX;
scrollBy(dx, 0);
}
}
mCurPageIndex = pageIndex;
}
}
public void setOnScrollListener(OnScrollListener listener) {
mScrollListener = listener;
}
public int getCurrentPage() {
return mCurPageIndex;
}
public interface OnBorderListener {
boolean onLeft(int curPageIndex, int nextPageIndex);
boolean onRight(int curPageIndex, int nextPageIndex);
}
public interface OnScrollListener {
void onScrollStart();
void onScrollEnd();
}
public static abstract clreplaced Adapter {
public abstract int getPageRow(int pageIndex);
public abstract int getPageColumn(int pageIndex);
public abstract ItemCoordinate getChildCoordinate(int pageIndex, int childIndex);
public abstract View getChildView(int groupPosition, int childPosition, int childW, int childH);
public abstract int getChildCount(int pageIndex);
public abstract int getPageCount();
protected void onSwitchAdapter(Adapter newAdapter, Adapter oldAdapter) {
}
}
/**
* 用于记录每个小格item项的坐标位置
*/
public static clreplaced ItemCoordinate {
// 左上角坐标
public Point start;
// 右下角坐标
public Point end;
@Override
public String toString() {
return "ItemCoordinate{" + "start=" + start + ", end=" + end + '}';
}
}
private static clreplaced LayoutParams extends FrameLayout.LayoutParams {
ItemCoordinate mItemCoordinate;
int pageIndex;
public LayoutParams(int width, int height) {
super(width, height);
}
}
}
19
Source : SwipeMenuLayout.java
with Apache License 2.0
from weileng11
with Apache License 2.0
from weileng11
/**
* Created by Yan Zhenjie on 2016/7/27.
*/
public clreplaced SwipeMenuLayout extends FrameLayout implements SwipeSwitch {
public static final int DEFAULT_SCROLLER_DURATION = 200;
private int mLeftViewId = 0;
private int mContentViewId = 0;
private int mRightViewId = 0;
private float mOpenPercent = 0.5f;
private int mScrollerDuration = DEFAULT_SCROLLER_DURATION;
private int mScaledTouchSlop;
private int mLastX;
private int mLastY;
private int mDownX;
private int mDownY;
private View mContentView;
private SwipeLeftHorizontal mSwipeLeftHorizontal;
private SwipeRightHorizontal mSwipeRightHorizontal;
private SwipeHorizontal mSwipeCurrentHorizontal;
private boolean shouldResetSwipe;
private boolean mDragging;
private boolean swipeEnable = true;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mScaledMinimumFlingVelocity;
private int mScaledMaximumFlingVelocity;
public SwipeMenuLayout(Context context) {
this(context, null);
}
public SwipeMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.recycler_swipe_SwipeMenuLayout);
mLeftViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_leftViewId, mLeftViewId);
mContentViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_contentViewId, mContentViewId);
mRightViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_rightViewId, mRightViewId);
typedArray.recycle();
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mScaledTouchSlop = configuration.getScaledTouchSlop();
mScaledMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mScaledMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
mScroller = new OverScroller(getContext());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mLeftViewId != 0 && mSwipeLeftHorizontal == null) {
View view = findViewById(mLeftViewId);
mSwipeLeftHorizontal = new SwipeLeftHorizontal(view);
}
if (mRightViewId != 0 && mSwipeRightHorizontal == null) {
View view = findViewById(mRightViewId);
mSwipeRightHorizontal = new SwipeRightHorizontal(view);
}
if (mContentViewId != 0 && mContentView == null) {
mContentView = findViewById(mContentViewId);
} else {
TextView errorView = new TextView(getContext());
errorView.setClickable(true);
errorView.setGravity(Gravity.CENTER);
errorView.setTextSize(16);
errorView.setText("You may not have set the ContentView.");
mContentView = errorView;
addView(mContentView);
}
}
/**
* Set whether open swipe. Default is true.
*
* @param swipeEnable true open, otherwise false.
*/
public void setSwipeEnable(boolean swipeEnable) {
this.swipeEnable = swipeEnable;
}
/**
* Open the swipe function of the Item?
*
* @return open is true, otherwise is false.
*/
public boolean isSwipeEnable() {
return swipeEnable;
}
/**
* Set open percentage.
*
* @param openPercent such as 0.5F.
*/
public void setOpenPercent(float openPercent) {
this.mOpenPercent = openPercent;
}
/**
* Get open percentage.
*
* @return such as 0.5F.
*/
public float getOpenPercent() {
return mOpenPercent;
}
/**
* The duration of the set.
*
* @param scrollerDuration such as 500.
*/
public void setScrollerDuration(int scrollerDuration) {
this.mScrollerDuration = scrollerDuration;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercepted = super.onInterceptTouchEvent(ev);
int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
{
mDownX = mLastX = (int) ev.getX();
mDownY = (int) ev.getY();
return false;
}
case MotionEvent.ACTION_MOVE:
{
int disX = (int) (ev.getX() - mDownX);
int disY = (int) (ev.getY() - mDownY);
boolean i = Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);
return i;
}
case MotionEvent.ACTION_UP:
{
boolean isClick = mSwipeCurrentHorizontal != null && mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());
if (isMenuOpen() && isClick) {
smoothCloseMenu();
return true;
}
return false;
}
case MotionEvent.ACTION_CANCEL:
{
if (!mScroller.isFinished())
mScroller.abortAnimation();
return false;
}
}
return isIntercepted;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
int dx;
int dy;
int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
{
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
break;
}
case MotionEvent.ACTION_MOVE:
{
if (!isSwipeEnable())
break;
int disX = (int) (mLastX - ev.getX());
int disY = (int) (mLastY - ev.getY());
if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {
mDragging = true;
}
if (mDragging) {
if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {
if (disX < 0) {
if (mSwipeLeftHorizontal != null)
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
else
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
} else {
if (mSwipeRightHorizontal != null)
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
else
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
}
}
scrollBy(disX, 0);
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
shouldResetSwipe = false;
}
break;
}
case MotionEvent.ACTION_UP:
{
dx = (int) (mDownX - ev.getX());
dy = (int) (mDownY - ev.getY());
mDragging = false;
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
int velocityX = (int) mVelocityTracker.getXVelocity();
int velocity = Math.abs(velocityX);
if (velocity > mScaledMinimumFlingVelocity) {
if (mSwipeCurrentHorizontal != null) {
int duration = getSwipeDuration(ev, velocity);
if (mSwipeCurrentHorizontal instanceof SwipeRightHorizontal) {
if (velocityX < 0) {
smoothOpenMenu(duration);
} else {
smoothCloseMenu(duration);
}
} else {
if (velocityX > 0) {
smoothOpenMenu(duration);
} else {
smoothCloseMenu(duration);
}
}
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
judgeOpenClose(dx, dy);
}
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop || Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
{
mDragging = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
} else {
dx = (int) (mDownX - ev.getX());
dy = (int) (mDownY - ev.getY());
judgeOpenClose(dx, dy);
}
break;
}
}
return super.onTouchEvent(ev);
}
/**
* compute finish duration.
*
* @param ev up event.
* @param velocity velocity x.
* @return finish duration.
*/
private int getSwipeDuration(MotionEvent ev, int velocity) {
int sx = getScrollX();
int dx = (int) (ev.getX() - sx);
final int width = mSwipeCurrentHorizontal.getMenuWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
int duration;
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageDelta = (float) Math.abs(dx) / width;
duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, mScrollerDuration);
return duration;
}
float distanceInfluenceForSnapDuration(float f) {
// center the values about 0.
f -= 0.5f;
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
private void judgeOpenClose(int dx, int dy) {
if (mSwipeCurrentHorizontal != null) {
if (Math.abs(getScrollX()) >= (mSwipeCurrentHorizontal.getMenuView().getWidth() * mOpenPercent)) {
// auto open
if (Math.abs(dx) > mScaledTouchSlop || Math.abs(dy) > mScaledTouchSlop) {
// swipe up
if (isMenuOpenNotEqual())
smoothCloseMenu();
else
smoothOpenMenu();
} else {
// normal up
if (isMenuOpen())
smoothCloseMenu();
else
smoothOpenMenu();
}
} else {
// auto closeMenu
smoothCloseMenu();
}
}
}
@Override
public void scrollTo(int x, int y) {
if (mSwipeCurrentHorizontal == null) {
super.scrollTo(x, y);
} else {
SwipeHorizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);
shouldResetSwipe = checker.shouldResetSwipe;
if (checker.x != getScrollX()) {
super.scrollTo(checker.x, checker.y);
}
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset() && mSwipeCurrentHorizontal != null) {
if (mSwipeCurrentHorizontal instanceof SwipeRightHorizontal) {
scrollTo(Math.abs(mScroller.getCurrX()), 0);
invalidate();
} else {
scrollTo(-Math.abs(mScroller.getCurrX()), 0);
invalidate();
}
}
}
public boolean hasLeftMenu() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();
}
public boolean hasRightMenu() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();
}
@Override
public boolean isMenuOpen() {
return isLeftMenuOpen() || isRightMenuOpen();
}
@Override
public boolean isLeftMenuOpen() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());
}
@Override
public boolean isRightMenuOpen() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());
}
@Override
public boolean isCompleteOpen() {
return isLeftCompleteOpen() || isRightMenuOpen();
}
@Override
public boolean isLeftCompleteOpen() {
return mSwipeLeftHorizontal != null && !mSwipeLeftHorizontal.isCompleteClose(getScrollX());
}
@Override
public boolean isRightCompleteOpen() {
return mSwipeRightHorizontal != null && !mSwipeRightHorizontal.isCompleteClose(getScrollX());
}
@Override
public boolean isMenuOpenNotEqual() {
return isLeftMenuOpenNotEqual() || isRightMenuOpenNotEqual();
}
@Override
public boolean isLeftMenuOpenNotEqual() {
return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpenNotEqual(getScrollX());
}
@Override
public boolean isRightMenuOpenNotEqual() {
return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpenNotEqual(getScrollX());
}
@Override
public void smoothOpenMenu() {
smoothOpenMenu(mScrollerDuration);
}
@Override
public void smoothOpenLeftMenu() {
smoothOpenLeftMenu(mScrollerDuration);
}
@Override
public void smoothOpenRightMenu() {
smoothOpenRightMenu(mScrollerDuration);
}
@Override
public void smoothOpenLeftMenu(int duration) {
if (mSwipeLeftHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
smoothOpenMenu(duration);
}
}
@Override
public void smoothOpenRightMenu(int duration) {
if (mSwipeRightHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
smoothOpenMenu(duration);
}
}
private void smoothOpenMenu(int duration) {
if (mSwipeCurrentHorizontal != null) {
mSwipeCurrentHorizontal.autoOpenMenu(mScroller, getScrollX(), duration);
invalidate();
}
}
@Override
public void smoothCloseMenu() {
smoothCloseMenu(mScrollerDuration);
}
@Override
public void smoothCloseLeftMenu() {
if (mSwipeLeftHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
smoothCloseMenu();
}
}
@Override
public void smoothCloseRightMenu() {
if (mSwipeRightHorizontal != null) {
mSwipeCurrentHorizontal = mSwipeRightHorizontal;
smoothCloseMenu();
}
}
@Override
public void smoothCloseMenu(int duration) {
if (mSwipeCurrentHorizontal != null) {
mSwipeCurrentHorizontal.autoCloseMenu(mScroller, getScrollX(), duration);
invalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int contentViewHeight = 0;
if (mContentView != null) {
measureChildWithMargins(mContentView, widthMeasureSpec, 0, heightMeasureSpec, 0);
contentViewHeight = mContentView.getMeasuredHeight();
}
if (mSwipeLeftHorizontal != null) {
View leftMenu = mSwipeLeftHorizontal.getMenuView();
int menuViewHeight = contentViewHeight == 0 ? leftMenu.getMeasuredHeightAndState() : contentViewHeight;
int menuWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
int menuHeightSpec = MeasureSpec.makeMeasureSpec(menuViewHeight, MeasureSpec.EXACTLY);
leftMenu.measure(menuWidthSpec, menuHeightSpec);
}
if (mSwipeRightHorizontal != null) {
View rightMenu = mSwipeRightHorizontal.getMenuView();
int menuViewHeight = contentViewHeight == 0 ? rightMenu.getMeasuredHeightAndState() : contentViewHeight;
int menuWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
int menuHeightSpec = MeasureSpec.makeMeasureSpec(menuViewHeight, MeasureSpec.EXACTLY);
rightMenu.measure(menuWidthSpec, menuHeightSpec);
}
if (contentViewHeight > 0) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), contentViewHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int contentViewHeight;
if (mContentView != null) {
int contentViewWidth = mContentView.getMeasuredWidthAndState();
contentViewHeight = mContentView.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) mContentView.getLayoutParams();
int start = getPaddingLeft();
int top = getPaddingTop() + lp.topMargin;
mContentView.layout(start, top, start + contentViewWidth, top + contentViewHeight);
}
if (mSwipeLeftHorizontal != null) {
View leftMenu = mSwipeLeftHorizontal.getMenuView();
int menuViewWidth = leftMenu.getMeasuredWidthAndState();
int menuViewHeight = leftMenu.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) leftMenu.getLayoutParams();
int top = getPaddingTop() + lp.topMargin;
leftMenu.layout(-menuViewWidth, top, 0, top + menuViewHeight);
}
if (mSwipeRightHorizontal != null) {
View rightMenu = mSwipeRightHorizontal.getMenuView();
int menuViewWidth = rightMenu.getMeasuredWidthAndState();
int menuViewHeight = rightMenu.getMeasuredHeightAndState();
LayoutParams lp = (LayoutParams) rightMenu.getLayoutParams();
int top = getPaddingTop() + lp.topMargin;
int parentViewWidth = getMeasuredWidthAndState();
rightMenu.layout(parentViewWidth, top, parentViewWidth + menuViewWidth, top + menuViewHeight);
}
}
}
19
Source : Viewport.java
with Apache License 2.0
from weexteam
with Apache License 2.0
from weexteam
/**
* This is the default implementation for the viewport.
* This implementation so for a normal viewport
* where there is a horizontal x-axis and a
* vertical y-axis.
*/
public clreplaced Viewport {
/**
* this reference value is used to generate the
* vertical labels. It is used when the y axis bounds
* is set manual and humanRounding=false. it will be the minValueY value.
*/
protected double referenceY = Double.NaN;
/**
* this reference value is used to generate the
* horizontal labels. It is used when the x axis bounds
* is set manual and humanRounding=false. it will be the minValueX value.
*/
protected double referenceX = Double.NaN;
/**
* flag whether the vertical scaling is activated
*/
protected boolean scalableY;
/**
* the reference number to generate the labels
*
* @return by default 0, only when manual bounds and no human rounding
* is active, the min x value is returned
*/
protected double getReferenceX() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
if (Double.isNaN(referenceX)) {
referenceX = getMinX(false);
}
return referenceX;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
public interface OnXAxisBoundsChangedListener {
/**
* Called after scaling or scrolling with
* the new bounds
*
* @param minX min x value
* @param maxX max x value
*/
void onXAxisBoundsChanged(double minX, double maxX, OnXAxisBoundsChangedListener.Reason reason);
public enum Reason {
SCROLL, SCALE
}
}
/**
* listener for the scale gesture
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
/**
* called by android
* @param detector detector
* @return always true
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
// --- horizontal scaling ---
double viewportWidth = mCurrentViewport.width();
if (mMaxXAxisSize != 0) {
if (viewportWidth > mMaxXAxisSize) {
viewportWidth = mMaxXAxisSize;
}
}
double center = mCurrentViewport.left + viewportWidth / 2;
float scaleSpanX;
if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) {
scaleSpanX = detector.getCurrentSpanX() / detector.getPreviousSpanX();
} else {
scaleSpanX = detector.getScaleFactor();
}
viewportWidth /= scaleSpanX;
mCurrentViewport.left = center - viewportWidth / 2;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
// viewportStart must not be < minX
double minX = getMinX(true);
if (mCurrentViewport.left < minX) {
mCurrentViewport.left = minX;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
}
// viewportStart + viewportSize must not be > maxX
double maxX = getMaxX(true);
if (viewportWidth == 0) {
mCurrentViewport.right = maxX;
}
double overlap = mCurrentViewport.left + viewportWidth - maxX;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.left - overlap > minX) {
mCurrentViewport.left -= overlap;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
} else {
// maximal scale
mCurrentViewport.left = minX;
mCurrentViewport.right = maxX;
}
}
// --- vertical scaling ---
if (scalableY && android.os.Build.VERSION.SDK_INT >= 11) {
double viewportHeight = mCurrentViewport.height() * -1;
if (mMaxYAxisSize != 0) {
if (viewportHeight > mMaxYAxisSize) {
viewportHeight = mMaxYAxisSize;
}
}
center = mCurrentViewport.bottom + viewportHeight / 2;
viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
mCurrentViewport.bottom = center - viewportHeight / 2;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
// ignore bounds when second scale
// viewportStart must not be < minY
double minY = getMinY(true);
if (mCurrentViewport.bottom < minY) {
mCurrentViewport.bottom = minY;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
}
// viewportStart + viewportSize must not be > maxY
double maxY = getMaxY(true);
if (viewportHeight == 0) {
mCurrentViewport.top = maxY;
}
overlap = mCurrentViewport.bottom + viewportHeight - maxY;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.bottom - overlap > minY) {
mCurrentViewport.bottom -= overlap;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
} else {
// maximal scale
mCurrentViewport.bottom = minY;
mCurrentViewport.top = maxY;
}
}
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
/**
* called when scaling begins
*
* @param detector detector
* @return true if it is scalable
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (mIsScalable) {
mScalingActive = true;
return true;
} else {
return false;
}
}
/**
* called when sacling ends
* This will re-adjustSteps the viewport.
*
* @param detector detector
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
mScalingActive = false;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE);
}
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
};
/**
* simple gesture listener to track scroll events
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!mIsScrollable || mScalingActive)
return false;
// Initiates the decay phase of any active edge effects.
releaseEdgeEffects();
// Aborts any active scroll animations and invalidates.
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!mIsScrollable || mScalingActive)
return false;
// Scrolling uses math based on the viewport (as opposed to math using pixels).
/**
* Pixel offset is the offset in screen pixels, while viewport offset is the
* offset within the current viewport. For additional information on surface sizes
* and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
* additional information about the viewport, see the comments for
* {@link mCurrentViewport}.
*/
double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
int completeWidth = (int) ((mCompleteRange.width() / mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth());
int completeHeight = (int) ((mCompleteRange.height() / mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight());
int scrolledX = (int) (completeWidth * (mCurrentViewport.left + viewportOffsetX - mCompleteRange.left) / mCompleteRange.width());
int scrolledY = (int) (completeHeight * (mCurrentViewport.bottom + viewportOffsetY - mCompleteRange.bottom) / mCompleteRange.height() * -1);
boolean canScrollX = mCurrentViewport.left > mCompleteRange.left || mCurrentViewport.right < mCompleteRange.right;
boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom || mCurrentViewport.top < mCompleteRange.top;
// second scale
double viewportOffsetY2 = 0d;
canScrollY &= scrollableY;
if (canScrollX) {
if (viewportOffsetX < 0) {
double tooMuch = mCurrentViewport.left + viewportOffsetX - mCompleteRange.left;
if (tooMuch < 0) {
viewportOffsetX -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.right + viewportOffsetX - mCompleteRange.right;
if (tooMuch > 0) {
viewportOffsetX -= tooMuch;
}
}
mCurrentViewport.left += viewportOffsetX;
mCurrentViewport.right += viewportOffsetX;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL);
}
}
if (canScrollY) {
// if we have the second axis we ignore the max/min range
if (viewportOffsetY < 0) {
double tooMuch = mCurrentViewport.bottom + viewportOffsetY - mCompleteRange.bottom;
if (tooMuch < 0) {
viewportOffsetY -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.top + viewportOffsetY - mCompleteRange.top;
if (tooMuch > 0) {
viewportOffsetY -= tooMuch;
}
}
mCurrentViewport.top += viewportOffsetY;
mCurrentViewport.bottom += viewportOffsetY;
}
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
}
if (canScrollY && scrolledY < 0) {
mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
}
if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth()) / (float) mGraphView.getGraphContentWidth());
}
if (canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) {
mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight()) / (float) mGraphView.getGraphContentHeight());
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// fling((int) -velocityX, (int) -velocityY);
return true;
}
};
/**
* the state of the axis bounds
*/
public enum AxisBoundsStatus {
/**
* initial means that the bounds gets
* auto adjusted if they are not manual.
* After adjusting the status comes to
* #AUTO_ADJUSTED.
*/
INITIAL,
/**
* after the bounds got auto-adjusted,
* this status will set.
*/
AUTO_ADJUSTED,
/**
* means that the bounds are fix (manually) and
* are not to be auto-adjusted.
*/
FIX
}
/**
* paint to draw background
*/
private Paint mPaint;
/**
* reference to the graphview
*/
private final ChartView mGraphView;
/**
* this holds the current visible viewport
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCurrentViewport = new RectD();
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxXAxisSize = 0;
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxYAxisSize = 0;
/**
* this holds the whole range of the data
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCompleteRange = new RectD();
/**
* flag whether scaling is currently active
*/
protected boolean mScalingActive;
/**
* flag whether the viewport is scrollable
*/
private boolean mIsScrollable;
/**
* flag whether the viewport is scalable
*/
private boolean mIsScalable;
/**
* flag whether the viewport is scalable
* on the Y axis
*/
private boolean scrollableY;
/**
* gesture detector to detect scrolling
*/
protected GestureDetector mGestureDetector;
/**
* detect scaling
*/
protected ScaleGestureDetector mScaleGestureDetector;
/**
* not used - for fling
*/
protected OverScroller mScroller;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectTop;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectBottom;
/**
* glow effect when scrolling left
*/
private EdgeEffectCompat mEdgeEffectLeft;
/**
* glow effect when scrolling right
*/
private EdgeEffectCompat mEdgeEffectRight;
/**
* state of the x axis
*/
protected AxisBoundsStatus mXAxisBoundsStatus;
/**
* state of the y axis
*/
protected AxisBoundsStatus mYAxisBoundsStatus;
/**
* flag whether the x axis bounds are manual
*/
private boolean mXAxisBoundsManual;
/**
* flag whether the y axis bounds are manual
*/
private boolean mYAxisBoundsManual;
/**
* background color of the viewport area
* it is recommended to use a semi-transparent color
*/
private int mBackgroundColor;
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener;
/**
* optional draw a border between the labels
* and the viewport
*/
private boolean mDrawBorder;
/**
* color of the border
*
* @see #setDrawBorder(boolean)
*/
private Integer mBorderColor;
/**
* custom paint to use for the border
*
* @see #setDrawBorder(boolean)
*/
private Paint mBorderPaint;
/**
* creates the viewport
*
* @param graphView graphview
*/
Viewport(ChartView graphView) {
mScroller = new OverScroller(graphView.getContext());
mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
mGraphView = graphView;
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mBackgroundColor = Color.TRANSPARENT;
mPaint = new Paint();
}
/**
* will be called on a touch event.
* needed to use scaling and scrolling
*
* @param event
* @return true if it was consumed
*/
public boolean onTouchEvent(MotionEvent event) {
boolean b = mScaleGestureDetector.onTouchEvent(event);
b |= mGestureDetector.onTouchEvent(event);
return b;
}
/**
* change the state of the x axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setXAxisBoundsStatus(AxisBoundsStatus s) {
mXAxisBoundsStatus = s;
}
/**
* change the state of the y axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setYAxisBoundsStatus(AxisBoundsStatus s) {
mYAxisBoundsStatus = s;
}
/**
* @return whether the viewport is scrollable
*/
public boolean isScrollable() {
return mIsScrollable;
}
/**
* @param mIsScrollable whether is viewport is scrollable
*/
public void setScrollable(boolean mIsScrollable) {
this.mIsScrollable = mIsScrollable;
}
/**
* @return the x axis state
*/
public AxisBoundsStatus getXAxisBoundsStatus() {
return mXAxisBoundsStatus;
}
/**
* @return the y axis state
*/
public AxisBoundsStatus getYAxisBoundsStatus() {
return mYAxisBoundsStatus;
}
/**
* caches the complete range (minX, maxX, minY, maxY)
* by iterating all series and all datapoints and
* stores it into #mCompleteRange
* <p>
* for the x-range it will respect the series on the
* second scale - not for y-values
*/
public void calcCompleteRange() {
List<Series> series = mGraphView.getSeries();
List<Series> seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries());
mCompleteRange.set(0d, 0d, 0d, 0d);
if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) {
double d = seriesInclusiveSecondScale.get(0).getLowestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d > s.getLowestValueX()) {
d = s.getLowestValueX();
}
}
mCompleteRange.left = d;
d = seriesInclusiveSecondScale.get(0).getHighestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d < s.getHighestValueX()) {
d = s.getHighestValueX();
}
}
mCompleteRange.right = d;
if (!series.isEmpty() && !series.get(0).isEmpty()) {
d = series.get(0).getLowestValueY();
for (Series s : series) {
if (!s.isEmpty() && d > s.getLowestValueY()) {
d = s.getLowestValueY();
}
}
mCompleteRange.bottom = d;
d = series.get(0).getHighestValueY();
for (Series s : series) {
if (!s.isEmpty() && d < s.getHighestValueY()) {
d = s.getHighestValueY();
}
}
mCompleteRange.top = d;
}
}
// calc current viewport bounds
if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.top = mCompleteRange.top;
mCurrentViewport.bottom = mCompleteRange.bottom;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.left = mCompleteRange.left;
mCurrentViewport.right = mCompleteRange.right;
} else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
// getPerformanceList highest/lowest of current viewport
// lowest
double d = Double.MAX_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d > v) {
d = v;
}
}
}
if (d != Double.MAX_VALUE) {
mCurrentViewport.bottom = d;
}
// highest
d = Double.MIN_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d < v) {
d = v;
}
}
}
if (d != Double.MIN_VALUE) {
mCurrentViewport.top = d;
}
}
// fixes blank screen when range is zero
if (mCurrentViewport.left == mCurrentViewport.right)
mCurrentViewport.right++;
if (mCurrentViewport.top == mCurrentViewport.bottom)
mCurrentViewport.top++;
}
/**
* @param completeRange if true => minX of the complete range of all series
* if false => minX of the current visible viewport
* @return the min x value
*/
public double getMinX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.left;
} else {
return mCurrentViewport.left;
}
}
/**
* @param completeRange if true => maxX of the complete range of all series
* if false => maxX of the current visible viewport
* @return the max x value
*/
public double getMaxX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.right;
} else {
return mCurrentViewport.right;
}
}
/**
* @param completeRange if true => minY of the complete range of all series
* if false => minY of the current visible viewport
* @return the min y value
*/
public double getMinY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.bottom;
} else {
return mCurrentViewport.bottom;
}
}
/**
* @param completeRange if true => maxY of the complete range of all series
* if false => maxY of the current visible viewport
* @return the max y value
*/
public double getMaxY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.top;
} else {
return mCurrentViewport.top;
}
}
/**
* set the maximal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
*
* @param y max / highest value
*/
public void setMaxY(double y) {
mCurrentViewport.top = y;
}
/**
* set the minimal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
*
* @param y min / lowest value
*/
public void setMinY(double y) {
mCurrentViewport.bottom = y;
}
/**
* set the maximal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
*
* @param x max / highest value
*/
public void setMaxX(double x) {
mCurrentViewport.right = x;
}
/**
* set the minimal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
*
* @param x min / lowest value
*/
public void setMinX(double x) {
mCurrentViewport.left = x;
}
/**
* release the glowing effects
*/
private void releaseEdgeEffects() {
mEdgeEffectLeft.onRelease();
mEdgeEffectRight.onRelease();
mEdgeEffectTop.onRelease();
mEdgeEffectBottom.onRelease();
}
/**
* not used currently
*
* @param velocityX
* @param velocityY
*/
private void fling(int velocityX, int velocityY) {
velocityY = 0;
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
int maxX = (int) ((mCurrentViewport.width() / mCompleteRange.width()) * (float) mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
int maxY = (int) ((mCurrentViewport.height() / mCompleteRange.height()) * (float) mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
int startX = (int) ((mCurrentViewport.left - mCompleteRange.left) / mCompleteRange.width()) * maxX;
int startY = (int) ((mCurrentViewport.top - mCompleteRange.top) / mCompleteRange.height()) * maxY;
mScroller.forceFinished(true);
mScroller.fling(startX, startY, velocityX, velocityY, 0, maxX, 0, maxY, mGraphView.getGraphContentWidth() / 2, mGraphView.getGraphContentHeight() / 2);
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
/**
* not used currently
*/
public void computeScroll() {
}
/**
* Draws the overscroll "glow" at the four edges of the chart region, if necessary.
*
* @see EdgeEffectCompat
*/
private void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectTop.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectTop.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectBottom.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
canvas.rotate(180, mGraphView.getGraphContentWidth() / 2, 0);
mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectBottom.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
}
/**
* will be first called in order to draw
* the canvas
* Used to draw the background
*
* @param c canvas.
*/
public void drawFirst(Canvas c) {
// draw background
if (mBackgroundColor != Color.TRANSPARENT) {
mPaint.setColor(mBackgroundColor);
c.drawRect(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaint);
}
if (mDrawBorder) {
Paint p;
if (mBorderPaint != null) {
p = mBorderPaint;
} else {
p = mPaint;
p.setColor(getBorderColor());
}
c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
}
}
/**
* draws the glowing edge effect
*
* @param c canvas
*/
public void draw(Canvas c) {
drawEdgeEffectsUnclipped(c);
}
/**
* @return background of the viewport area
*/
public int getBackgroundColor() {
return mBackgroundColor;
}
/**
* @param mBackgroundColor background of the viewport area
* use transparent to have no background
*/
public void setBackgroundColor(int mBackgroundColor) {
this.mBackgroundColor = mBackgroundColor;
}
/**
* @return whether the viewport is scalable
*/
public boolean isScalable() {
return mIsScalable;
}
/**
* active the scaling/zooming feature
* notice: sets the x axis bounds to manual
*
* @param mIsScalable whether the viewport is scalable
*/
public void setScalable(boolean mIsScalable) {
this.mIsScalable = mIsScalable;
if (mIsScalable) {
mIsScrollable = true;
// set viewport to manual
setXAxisBoundsManual(true);
}
}
/**
* @return whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public boolean isXAxisBoundsManual() {
return mXAxisBoundsManual;
}
/**
* @param mXAxisBoundsManual whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
this.mXAxisBoundsManual = mXAxisBoundsManual;
if (mXAxisBoundsManual) {
mXAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* @return whether the y axis bound are manual
*/
public boolean isYAxisBoundsManual() {
return mYAxisBoundsManual;
}
/**
* @param mYAxisBoundsManual whether the y axis bounds are manual
* @see #setMaxY(double)
* @see #setMinY(double)
*/
public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
this.mYAxisBoundsManual = mYAxisBoundsManual;
if (mYAxisBoundsManual) {
mYAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* forces the viewport to scroll to the end
* of the range by keeping the current viewport size.
* <p>
* Important: Only takes effect if x axis bounds are manual.
*
* @see #setXAxisBoundsManual(boolean)
*/
public void scrollToEnd() {
if (mXAxisBoundsManual) {
double size = mCurrentViewport.width();
mCurrentViewport.right = mCompleteRange.right;
mCurrentViewport.left = mCompleteRange.right - size;
mGraphView.onDataChanged(true, false);
} else {
Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
}
}
/**
* @return the listener when there is one registered.
*/
public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() {
return mOnXAxisBoundsChangedListener;
}
/**
* set a listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*
* @param l the listener to use
*/
public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) {
mOnXAxisBoundsChangedListener = l;
}
/**
* optional draw a border between the labels
* and the viewport
*
* @param drawBorder true to draw the border
*/
public void setDrawBorder(boolean drawBorder) {
this.mDrawBorder = drawBorder;
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @return border color. by default the grid color is used
* @see #setDrawBorder(boolean)
*/
public int getBorderColor() {
if (mBorderColor != null) {
return mBorderColor;
}
return mGraphView.getGridLabelRenderer().getGridColor();
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @param borderColor null to reset
*/
public void setBorderColor(Integer borderColor) {
this.mBorderColor = borderColor;
}
/**
* custom paint to use for the border. border color
* will be ignored
*
* @param borderPaint
* @see #setDrawBorder(boolean)
*/
public void setBorderPaint(Paint borderPaint) {
this.mBorderPaint = borderPaint;
}
/**
* activate/deactivate the vertical scrolling
*
* @param scrollableY true to activate
*/
public void setScrollableY(boolean scrollableY) {
this.scrollableY = scrollableY;
}
/**
* the reference number to generate the labels
*
* @return by default 0, only when manual bounds and no human rounding
* is active, the min y value is returned
*/
protected double getReferenceY() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
if (Double.isNaN(referenceY)) {
referenceY = getMinY(false);
}
return referenceY;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* activate or deactivate the vertical zooming/scaling functionallity.
* This will automatically activate the vertical scrolling and the
* horizontal scaling/scrolling feature.
*
* @param scalableY true to activate
*/
public void setScalableY(boolean scalableY) {
if (scalableY) {
this.scrollableY = true;
setScalable(true);
if (android.os.Build.VERSION.SDK_INT < 11) {
Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)");
}
}
this.scalableY = scalableY;
}
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxXAxisSize() {
return mMaxXAxisSize;
}
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxYAxisSize() {
return mMaxYAxisSize;
}
/**
* Set the max viewport size (horizontal)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxXAxisViewportSize maximum size of viewport
*/
public void setMaxXAxisSize(double mMaxXAxisViewportSize) {
this.mMaxXAxisSize = mMaxXAxisViewportSize;
}
/**
* Set the max viewport size (vertical)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxYAxisViewportSize maximum size of viewport
*/
public void setMaxYAxisSize(double mMaxYAxisViewportSize) {
this.mMaxYAxisSize = mMaxYAxisViewportSize;
}
public clreplaced RectD {
public double left;
public double right;
public double top;
public double bottom;
public double width() {
return right - left;
}
public double height() {
return bottom - top;
}
public void set(double lLeft, double lTop, double lRight, double lBottom) {
left = lLeft;
right = lRight;
top = lTop;
bottom = lBottom;
}
}
}
19
Source : GingerScroller.java
with Apache License 2.0
from Vanish136
with Apache License 2.0
from Vanish136
public clreplaced GingerScroller extends ScrollerProxy {
protected final OverScroller mScroller;
private boolean mFirstScroll = false;
public GingerScroller(Context context) {
mScroller = new OverScroller(context);
}
@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
}
@Override
public boolean computeScrollOffset() {
// Workaround for first scroll returning 0 for the direction of the edge it hits.
// Simply recompute values.
if (mFirstScroll) {
mScroller.computeScrollOffset();
mFirstScroll = false;
}
return mScroller.computeScrollOffset();
}
@Override
public boolean isFinished() {
return mScroller.isFinished();
}
@Override
public void forceFinished(boolean finished) {
mScroller.forceFinished(finished);
}
@Override
public int getCurrX() {
return mScroller.getCurrX();
}
@Override
public int getCurrY() {
return mScroller.getCurrY();
}
}
19
Source : CardStackView.java
with Apache License 2.0
from triline3
with Apache License 2.0
from triline3
public clreplaced CardStackView extends ViewGroup implements ScrollDelegate {
private static final int INVALID_POINTER = -1;
public static final int INVALID_TYPE = -1;
public static final int ANIMATION_STATE_START = 0;
public static final int ANIMATION_STATE_END = 1;
public static final int ANIMATION_STATE_CANCEL = 2;
private static final String TAG = "CardStackView";
public static final int ALL_DOWN = 0;
public static final int UP_DOWN = 1;
public static final int UP_DOWN_STACK = 2;
static final int DEFAULT_SELECT_POSITION = -1;
private int mTotalLength;
private int mOverlapGaps;
private int mOverlapGapsCollapse;
private int mNumBottomShow;
private StackAdapter mStackAdapter;
private final ViewDataObserver mObserver = new ViewDataObserver();
private int mSelectPosition = DEFAULT_SELECT_POSITION;
private int mShowHeight;
private List<ViewHolder> mViewHolders;
private AnimatorAdapter mAnimatorAdapter;
private int mDuration;
private OverScroller mScroller;
private int mLastMotionY;
private boolean mIsBeingDragged = false;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMinimumVelocity;
private int mMaximumVelocity;
private int mActivePointerId = INVALID_POINTER;
private final int[] mScrollOffset = new int[2];
private int mNestedYOffset;
private boolean mScrollEnable = true;
private ScrollDelegate mScrollDelegate;
private ItemExpendListener mItemExpendListener;
public CardStackView(Context context) {
this(context, null);
}
public CardStackView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CardStackView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CardStackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CardStackView, defStyleAttr, defStyleRes);
setOverlapGaps(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGaps, dp2px(20)));
setOverlapGapsCollapse(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGapsCollapse, dp2px(20)));
setDuration(array.getInt(R.styleable.CardStackView_stackDuration, AnimatorAdapter.ANIMATION_DURATION));
setAnimationType(array.getInt(R.styleable.CardStackView_stackAnimationType, UP_DOWN_STACK));
setNumBottomShow(array.getInt(R.styleable.CardStackView_stackNumBottomShow, 3));
array.recycle();
mViewHolders = new ArrayList<>();
initScroller();
}
private void initScroller() {
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
private int dp2px(int value) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (value * scale + 0.5f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
checkContentHeightByParent();
measureChild(widthMeasureSpec, heightMeasureSpec);
}
private void checkContentHeightByParent() {
View parentView = (View) getParent();
mShowHeight = parentView.getMeasuredHeight() - parentView.getPaddingTop() - parentView.getPaddingBottom();
}
private void measureChild(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
mTotalLength = 0;
mTotalLength += getPaddingTop() + getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final int totalLength = mTotalLength;
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mHeaderHeight == -1)
lp.mHeaderHeight = child.getMeasuredHeight();
final int childHeight = lp.mHeaderHeight;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin);
mTotalLength -= mOverlapGaps * 2;
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
}
mTotalLength += mOverlapGaps * 2;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, mShowHeight);
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0), heightSizeAndState);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
layoutChild();
}
private void layoutChild() {
int childTop = getPaddingTop();
int childLeft = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
childTop += lp.topMargin;
if (i != 0) {
childTop -= mOverlapGaps * 2;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
} else {
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
childTop += lp.mHeaderHeight;
}
}
public void updateSelectPosition(final int selectPosition) {
post(new Runnable() {
@Override
public void run() {
doCardClickAnimation(mViewHolders.get(selectPosition), selectPosition);
}
});
}
public void clearSelectPosition() {
updateSelectPosition(mSelectPosition);
}
public void clearScrollYAndTranslation() {
if (mSelectPosition != DEFAULT_SELECT_POSITION) {
clearSelectPosition();
}
if (mScrollDelegate != null)
mScrollDelegate.setViewScrollY(0);
requestLayout();
}
public void setAdapter(StackAdapter stackAdapter) {
mStackAdapter = stackAdapter;
mStackAdapter.registerObserver(mObserver);
refreshView();
}
public void setAnimationType(int type) {
AnimatorAdapter animatorAdapter;
switch(type) {
case ALL_DOWN:
animatorAdapter = new AllMoveDownAnimatorAdapter(this);
break;
case UP_DOWN:
animatorAdapter = new UpDownAnimatorAdapter(this);
break;
default:
animatorAdapter = new UpDownStackAnimatorAdapter(this);
break;
}
setAnimatorAdapter(animatorAdapter);
}
public void setAnimatorAdapter(AnimatorAdapter animatorAdapter) {
clearScrollYAndTranslation();
mAnimatorAdapter = animatorAdapter;
if (mAnimatorAdapter instanceof UpDownStackAnimatorAdapter) {
mScrollDelegate = new StackScrollDelegateImpl(this);
} else {
mScrollDelegate = this;
}
}
private void refreshView() {
removeAllViews();
mViewHolders.clear();
for (int i = 0; i < mStackAdapter.gereplacedemCount(); i++) {
ViewHolder holder = getViewHolder(i);
holder.position = i;
holder.onItemExpand(i == mSelectPosition);
addView(holder.itemView);
setClickAnimator(holder, i);
mStackAdapter.bindViewHolder(holder, i);
}
requestLayout();
}
ViewHolder getViewHolder(int i) {
if (i == DEFAULT_SELECT_POSITION)
return null;
ViewHolder viewHolder;
if (mViewHolders.size() <= i || mViewHolders.get(i).mItemViewType != mStackAdapter.gereplacedemViewType(i)) {
viewHolder = mStackAdapter.createView(this, mStackAdapter.gereplacedemViewType(i));
mViewHolders.add(viewHolder);
} else {
viewHolder = mViewHolders.get(i);
}
return viewHolder;
}
private void setClickAnimator(final ViewHolder holder, final int position) {
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mSelectPosition == DEFAULT_SELECT_POSITION)
return;
performItemClick(mViewHolders.get(mSelectPosition));
}
});
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
performItemClick(holder);
}
});
}
public void next() {
if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == mViewHolders.size() - 1)
return;
performItemClick(mViewHolders.get(mSelectPosition + 1));
}
public void pre() {
if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == 0)
return;
performItemClick(mViewHolders.get(mSelectPosition - 1));
}
public boolean isExpending() {
return mSelectPosition != DEFAULT_SELECT_POSITION;
}
public void performItemClick(ViewHolder viewHolder) {
doCardClickAnimation(viewHolder, viewHolder.position);
}
private void doCardClickAnimation(final ViewHolder viewHolder, int position) {
checkContentHeightByParent();
mAnimatorAdapter.itemClick(viewHolder, position);
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (getViewScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch(action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
{
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
LogUtil.e("Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN:
{
final int y = (int) ev.getY();
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mIsBeingDragged = !mScroller.isFinished();
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getViewScrollX(), getViewScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (!mScrollEnable) {
mIsBeingDragged = false;
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mIsBeingDragged) {
super.onTouchEvent(ev);
}
performClick();
if (!mScrollEnable) {
return true;
}
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch(actionMasked) {
case MotionEvent.ACTION_DOWN:
{
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
LogUtil.e("Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
mLastMotionY = y - mScrollOffset[1];
final int range = getScrollRange();
if (mScrollDelegate instanceof StackScrollDelegateImpl) {
mScrollDelegate.scrollViewTo(0, deltaY + mScrollDelegate.getViewScrollY());
} else {
if (overScrollBy(0, deltaY, 0, getViewScrollY(), 0, range, 0, 0, true)) {
mVelocityTracker.clear();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidate();
}
}
mActivePointerId = INVALID_POINTER;
}
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidate();
}
mActivePointerId = INVALID_POINTER;
}
endDrag();
break;
case MotionEvent.ACTION_POINTER_DOWN:
{
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
private int getScrollRange() {
int scrollRange = 0;
if (getChildCount() > 0) {
scrollRange = Math.max(0, mTotalLength - mShowHeight);
}
return scrollRange;
}
@Override
public boolean performClick() {
return super.performClick();
}
@Override
protected int computeVerticalScrollRange() {
final int count = getChildCount();
final int contentHeight = mShowHeight;
if (count == 0) {
return contentHeight;
}
int scrollRange = mTotalLength;
final int scrollY = mScrollDelegate.getViewScrollY();
final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
if (scrollY < 0) {
scrollRange -= scrollY;
} else if (scrollY > overscrollBottom) {
scrollRange += scrollY - overscrollBottom;
}
return scrollRange;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (!mScroller.isFinished()) {
final int oldX = mScrollDelegate.getViewScrollX();
final int oldY = mScrollDelegate.getViewScrollY();
mScrollDelegate.setViewScrollX(scrollX);
mScrollDelegate.setViewScrollY(scrollY);
onScrollChanged(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
}
@Override
protected int computeVerticalScrollOffset() {
return Math.max(0, super.computeVerticalScrollOffset());
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mScrollDelegate.scrollViewTo(0, mScroller.getCurrY());
postInvalidate();
}
}
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = mShowHeight;
int bottom = mTotalLength;
mScroller.fling(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, 0);
postInvalidate();
}
}
@Override
public void scrollTo(int x, int y) {
if (getChildCount() > 0) {
x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), getWidth());
y = clamp(y, mShowHeight, mTotalLength);
if (x != mScrollDelegate.getViewScrollX() || y != mScrollDelegate.getViewScrollY()) {
super.scrollTo(x, y);
}
}
}
@Override
public int getViewScrollX() {
return getScrollX();
}
@Override
public void scrollViewTo(int x, int y) {
scrollTo(x, y);
}
@Override
public void setViewScrollY(int y) {
setScrollY(y);
}
@Override
public void setViewScrollX(int x) {
setScrollX(x);
}
@Override
public int getViewScrollY() {
return getScrollY();
}
private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
}
private static int clamp(int n, int my, int child) {
if (my >= child || n < 0) {
return 0;
}
if ((my + n) > child) {
return child - my;
}
return n;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public static clreplaced LayoutParams extends MarginLayoutParams {
public int mHeaderHeight;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.CardStackView);
mHeaderHeight = array.getDimensionPixelSize(R.styleable.CardStackView_stackHeaderHeight, -1);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
public static abstract clreplaced Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
VH createView(ViewGroup parent, int viewType) {
VH holder = onCreateView(parent, viewType);
holder.mItemViewType = viewType;
return holder;
}
protected abstract VH onCreateView(ViewGroup parent, int viewType);
public void bindViewHolder(VH holder, int position) {
onBindViewHolder(holder, position);
}
protected abstract void onBindViewHolder(VH holder, int position);
public abstract int gereplacedemCount();
public int gereplacedemViewType(int position) {
return 0;
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
public void registerObserver(AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
}
public static abstract clreplaced ViewHolder {
public View itemView;
int mItemViewType = INVALID_TYPE;
int position;
public ViewHolder(View view) {
itemView = view;
}
public Context getContext() {
return itemView.getContext();
}
public abstract void onItemExpand(boolean b);
protected void onAnimationStateChange(int state, boolean willBeSelect) {
}
}
public static clreplaced AdapterDataObservable extends Observable<AdapterDataObserver> {
public boolean hasObservers() {
return !mObservers.isEmpty();
}
public void notifyChanged() {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public static abstract clreplaced AdapterDataObserver {
public void onChanged() {
}
}
private clreplaced ViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
refreshView();
}
}
public int getSelectPosition() {
return mSelectPosition;
}
public void setSelectPosition(int selectPosition) {
mSelectPosition = selectPosition;
mItemExpendListener.onItemExpend(mSelectPosition != DEFAULT_SELECT_POSITION);
}
public int getOverlapGaps() {
return mOverlapGaps;
}
public void setOverlapGaps(int overlapGaps) {
mOverlapGaps = overlapGaps;
}
public int getOverlapGapsCollapse() {
return mOverlapGapsCollapse;
}
public void setOverlapGapsCollapse(int overlapGapsCollapse) {
mOverlapGapsCollapse = overlapGapsCollapse;
}
public void setScrollEnable(boolean scrollEnable) {
mScrollEnable = scrollEnable;
}
public int getShowHeight() {
return mShowHeight;
}
public int getTotalLength() {
return mTotalLength;
}
public void setDuration(int duration) {
mDuration = duration;
}
public int getDuration() {
if (mAnimatorAdapter != null)
return mDuration;
return 0;
}
public void setNumBottomShow(int numBottomShow) {
mNumBottomShow = numBottomShow;
}
public int getNumBottomShow() {
return mNumBottomShow;
}
public ScrollDelegate getScrollDelegate() {
return mScrollDelegate;
}
public ItemExpendListener gereplacedemExpendListener() {
return mItemExpendListener;
}
public void sereplacedemExpendListener(ItemExpendListener itemExpendListener) {
mItemExpendListener = itemExpendListener;
}
public interface ItemExpendListener {
void onItemExpend(boolean expend);
}
}
19
Source : InnerRuler.java
with MIT License
from totond
with MIT License
from totond
/**
* 内部尺子抽象类
*/
public abstract clreplaced InnerRuler extends View {
public static final String TAG = "ruler";
protected Context mContext;
protected BooheeRuler mParent;
// 加入放大倍数来防止精度丢失而导致无限绘制
protected static final int SCALE_TO_PX_FACTOR = 100;
// 惯性回滚最小偏移值,小于这个值就应该直接滑动到目的点
protected static final int MIN_SCROLLER_DP = 1;
protected float minScrollerPx = MIN_SCROLLER_DP;
protected Paint mSmallScalePaint, mBigScalePaint, mTextPaint, mOutLinePaint;
// 当前刻度值
protected float mCurrentScale = 0;
// 最大刻度数
protected int mMaxLength = 0;
// 长度、最小可滑动值、最大可滑动值
protected int mLength, mMinPosition = 0, mMaxPosition = 0;
// 控制滑动
protected OverScroller mOverScroller;
// 一格大刻度多少格小刻度
protected int mCount = 10;
// 提前刻画量
protected int mDrawOffset;
// 速度获取
protected VelocityTracker mVelocityTracker;
// 惯性最大最小速度
protected int mMaximumVelocity, mMinimumVelocity;
// 回调接口
protected RulerCallback mRulerCallback;
// 边界效果
protected EdgeEffect mStartEdgeEffect, mEndEdgeEffect;
// 边缘效应长度
protected int mEdgeLength;
public InnerRuler(Context context, BooheeRuler booheeRuler) {
super(context);
mParent = booheeRuler;
init(context);
}
public void init(Context context) {
mContext = context;
mMaxLength = mParent.getMaxScale() - mParent.getMinScale();
mCurrentScale = mParent.getCurrentScale();
mCount = mParent.getCount();
mDrawOffset = mCount * mParent.getInterval() / 2;
minScrollerPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MIN_SCROLLER_DP, context.getResources().getDisplayMetrics());
initPaints();
mOverScroller = new OverScroller(mContext);
// mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 配置速度
mVelocityTracker = VelocityTracker.obtain();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
initEdgeEffects();
// 第一次进入,跳转到设定刻度
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
goToScale(mCurrentScale);
}
});
checkAPILevel();
}
// 初始化画笔
private void initPaints() {
mSmallScalePaint = new Paint();
mSmallScalePaint.setStrokeWidth(mParent.getSmallScaleWidth());
mSmallScalePaint.setColor(mParent.getScaleColor());
mSmallScalePaint.setStrokeCap(Paint.Cap.ROUND);
mBigScalePaint = new Paint();
mBigScalePaint.setColor(mParent.getScaleColor());
mBigScalePaint.setStrokeWidth(mParent.getBigScaleWidth());
mBigScalePaint.setStrokeCap(Paint.Cap.ROUND);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mParent.getTextColor());
mTextPaint.setTextSize(mParent.getTextSize());
mTextPaint.setTextAlign(Paint.Align.CENTER);
// mTextPaint.setStrokeJoin(Paint.Join.ROUND);
mOutLinePaint = new Paint();
mOutLinePaint.setStrokeWidth(mParent.getOutLineWidth());
mOutLinePaint.setAntiAlias(true);
mOutLinePaint.setColor(mParent.getScaleColor());
}
// 初始化边缘效果
public void initEdgeEffects() {
if (mParent.canEdgeEffect()) {
if (mStartEdgeEffect == null || mEndEdgeEffect == null) {
mStartEdgeEffect = new EdgeEffect(mContext);
mEndEdgeEffect = new EdgeEffect(mContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mStartEdgeEffect.setColor(mParent.getEdgeColor());
mEndEdgeEffect.setColor(mParent.getEdgeColor());
}
mEdgeLength = mParent.getCursorHeight() + mParent.getInterval() * mParent.getCount();
}
}
}
// API小于18则关闭硬件加速,否则setAntiAlias()方法不生效
private void checkAPILevel() {
if (Build.VERSION.SDK_INT < 18) {
setLayerType(LAYER_TYPE_NONE, null);
}
}
@Override
public void computeScroll() {
if (mOverScroller.computeScrollOffset()) {
scrollTo(mOverScroller.getCurrX(), mOverScroller.getCurrY());
// 这是最后OverScroller的最后一次滑动,如果这次滑动完了mCurrentScale不是整数,则把尺子移动到最近的整数位置
if (!mOverScroller.computeScrollOffset()) {
int currentIntScale = Math.round(mCurrentScale);
if ((Math.abs(mCurrentScale - currentIntScale) > 0.001f)) {
// Fling完进行一次检测回滚
scrollBackToCurrentScale(currentIntScale);
}
}
postInvalidate();
}
}
protected abstract void scrollBackToCurrentScale();
protected abstract void scrollBackToCurrentScale(int currentIntScale);
protected abstract void goToScale(float scale);
public abstract void refreshSize();
// 设置尺子当前刻度
public void setCurrentScale(float currentScale) {
this.mCurrentScale = currentScale;
goToScale(mCurrentScale);
}
public void setRulerCallback(RulerCallback RulerCallback) {
this.mRulerCallback = RulerCallback;
}
public float getCurrentScale() {
return mCurrentScale;
}
}
19
Source : ScrollAndScaleView.java
with Apache License 2.0
from tifezh
with Apache License 2.0
from tifezh
/**
* 可以滑动和放大的view
* Created by tian on 2016/5/3.
*/
public abstract clreplaced ScrollAndScaleView extends RelativeLayout implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener {
protected int mScrollX = 0;
protected GestureDetectorCompat mDetector;
protected ScaleGestureDetector mScaleDetector;
protected boolean isLongPress = false;
private OverScroller mScroller;
protected boolean touch = false;
protected float mScaleX = 1;
protected float mScaleXMax = 2f;
protected float mScaleXMin = 0.5f;
private boolean mMultipleTouch = false;
private boolean mScrollEnable = true;
private boolean mScaleEnable = true;
public ScrollAndScaleView(Context context) {
super(context);
init();
}
public ScrollAndScaleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScrollAndScaleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setWillNotDraw(false);
mDetector = new GestureDetectorCompat(getContext(), this);
mScaleDetector = new ScaleGestureDetector(getContext(), this);
mScroller = new OverScroller(getContext());
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isLongPress && !isMultipleTouch()) {
scrollBy(Math.round(distanceX), 0);
return true;
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
isLongPress = true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (!isTouch() && isScrollEnable()) {
mScroller.fling(mScrollX, 0, Math.round(velocityX / mScaleX), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (!isTouch()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
} else {
mScroller.forceFinished(true);
}
}
}
@Override
public void scrollBy(int x, int y) {
scrollTo(mScrollX - Math.round(x / mScaleX), 0);
}
@Override
public void scrollTo(int x, int y) {
if (!isScrollEnable()) {
mScroller.forceFinished(true);
return;
}
int oldX = mScrollX;
mScrollX = x;
if (mScrollX < getMinScrollX()) {
mScrollX = getMinScrollX();
onRightSide();
mScroller.forceFinished(true);
} else if (mScrollX > getMaxScrollX()) {
mScrollX = getMaxScrollX();
onLeftSide();
mScroller.forceFinished(true);
}
onScrollChanged(mScrollX, 0, oldX, 0);
invalidate();
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (!isScaleEnable()) {
return false;
}
float oldScale = mScaleX;
mScaleX *= detector.getScaleFactor();
if (mScaleX < mScaleXMin) {
mScaleX = mScaleXMin;
} else if (mScaleX > mScaleXMax) {
mScaleX = mScaleXMax;
} else {
onScaleChanged(mScaleX, oldScale);
}
return true;
}
protected void onScaleChanged(float scale, float oldScale) {
invalidate();
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
touch = true;
break;
case MotionEvent.ACTION_MOVE:
if (event.getPointerCount() == 1) {
// 长按之后移动
if (isLongPress) {
onLongPress(event);
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
invalidate();
break;
case MotionEvent.ACTION_UP:
isLongPress = false;
touch = false;
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
isLongPress = false;
touch = false;
invalidate();
break;
}
mMultipleTouch = event.getPointerCount() > 1;
this.mDetector.onTouchEvent(event);
this.mScaleDetector.onTouchEvent(event);
return true;
}
/**
* 滑到了最左边
*/
abstract public void onLeftSide();
/**
* 滑到了最右边
*/
abstract public void onRightSide();
/**
* 是否在触摸中
*
* @return
*/
public boolean isTouch() {
return touch;
}
/**
* 获取位移的最小值
*
* @return
*/
public abstract int getMinScrollX();
/**
* 获取位移的最大值
*
* @return
*/
public abstract int getMaxScrollX();
/**
* 设置ScrollX
*
* @param scrollX
*/
public void setScrollX(int scrollX) {
this.mScrollX = scrollX;
scrollTo(scrollX, 0);
}
/**
* 是否是多指触控
* @return
*/
public boolean isMultipleTouch() {
return mMultipleTouch;
}
protected void checkAndFixScrollX() {
if (mScrollX < getMinScrollX()) {
mScrollX = getMinScrollX();
mScroller.forceFinished(true);
} else if (mScrollX > getMaxScrollX()) {
mScrollX = getMaxScrollX();
mScroller.forceFinished(true);
}
}
public float getScaleXMax() {
return mScaleXMax;
}
public float getScaleXMin() {
return mScaleXMin;
}
public boolean isScrollEnable() {
return mScrollEnable;
}
public boolean isScaleEnable() {
return mScaleEnable;
}
/**
* 设置缩放的最大值
*/
public void setScaleXMax(float scaleXMax) {
mScaleXMax = scaleXMax;
}
/**
* 设置缩放的最小值
*/
public void setScaleXMin(float scaleXMin) {
mScaleXMin = scaleXMin;
}
/**
* 设置是否可以滑动
*/
public void setScrollEnable(boolean scrollEnable) {
mScrollEnable = scrollEnable;
}
/**
* 设置是否可以缩放
*/
public void setScaleEnable(boolean scaleEnable) {
mScaleEnable = scaleEnable;
}
@Override
public float getScaleX() {
return mScaleX;
}
}
19
Source : RNScrollView.java
with MIT License
from TaumuLu
with MIT License
from TaumuLu
@TargetApi(11)
public clreplaced RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {
@Nullable
private static Field sScrollerField;
private static boolean sTriedToGetScrollerField = false;
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
@Nullable
private final OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
@Nullable
private Rect mClippingRect;
private boolean mDoneFlinging;
private boolean mDragging;
private boolean mFlinging;
private boolean mRemoveClippedSubviews;
static private boolean mScrollEnabled = true;
private boolean mSendMomentumEvents;
@Nullable
private FpsListener mFpsListener = null;
@Nullable
private String mScrollPerfTag;
@Nullable
private Drawable mEndBackground;
private int mEndFillColor = Color.TRANSPARENT;
private View mContentView;
private ReactViewBackgroundManager mReactBackgroundManager;
public RNScrollView(ReactContext context) {
this(context, null);
}
public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
super(context);
mFpsListener = fpsListener;
mReactBackgroundManager = new ReactViewBackgroundManager(this);
mScroller = getOverScrollerFromParent();
setOnHierarchyChangeListener(this);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
}
@Nullable
private OverScroller getOverScrollerFromParent() {
OverScroller scroller;
if (!sTriedToGetScrollerField) {
sTriedToGetScrollerField = true;
try {
sScrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
sScrollerField.setAccessible(true);
} catch (NoSuchFieldException e) {
Log.w(ReactConstants.TAG, "Failed to get mScroller field for ScrollView! " + "This app will exhibit the bounce-back scrolling bug :(");
}
}
if (sScrollerField != null) {
try {
Object scrollerValue = sScrollerField.get(this);
if (scrollerValue instanceof OverScroller) {
scroller = (OverScroller) scrollerValue;
} else {
Log.w(ReactConstants.TAG, "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + "This app will exhibit the bounce-back scrolling bug :(");
scroller = null;
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
}
} else {
scroller = null;
}
return scroller;
}
public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
}
public void setScrollPerfTag(@Nullable String scrollPerfTag) {
mScrollPerfTag = scrollPerfTag;
}
public void setScrollEnabled(boolean scrollEnabled) {
mScrollEnabled = scrollEnabled;
}
public void flashScrollIndicators() {
awakenScrollBars();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecreplacedertions.replacedertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Call with the present values in order to re-layout if necessary
scrollTo(getScrollX(), getScrollY());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mRemoveClippedSubviews) {
updateClippingRect();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mRemoveClippedSubviews) {
updateClippingRect();
}
}
@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
if (mRemoveClippedSubviews) {
updateClippingRect();
}
if (mFlinging) {
mDoneFlinging = false;
}
ReactScrollViewHelper.emitScrollEvent(this, mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity());
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mScrollEnabled) {
return false;
}
if (super.onInterceptTouchEvent(ev)) {
// 会将滑动时的触摸操作停止
// NativeGestureUtil.notifyNativeGestureStarted(this, ev);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
enableFpsListener();
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mScrollEnabled) {
return false;
}
mVelocityHelper.calculateVelocity(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
ReactScrollViewHelper.emitScrollEndDragEvent(this, mVelocityHelper.getXVelocity(), mVelocityHelper.getYVelocity());
mDragging = false;
disableFpsListener();
}
return super.onTouchEvent(ev);
}
@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews && mClippingRect == null) {
mClippingRect = new Rect();
}
mRemoveClippedSubviews = removeClippedSubviews;
updateClippingRect();
}
@Override
public boolean getRemoveClippedSubviews() {
return mRemoveClippedSubviews;
}
@Override
public void updateClippingRect() {
if (!mRemoveClippedSubviews) {
return;
}
replacedertions.replacedertNotNull(mClippingRect);
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
View contentView = getChildAt(0);
if (contentView instanceof ReactClippingViewGroup) {
((ReactClippingViewGroup) contentView).updateClippingRect();
}
}
@Override
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(replacedertions.replacedertNotNull(mClippingRect));
}
@Override
public void fling(int velocityY) {
if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// We provide our own version of fling that uses a different call to the standard OverScroller
// which takes into account the possibility of adding new content while the ScrollView is
// animating. Because we give essentially no max Y for the fling, the fling will continue as long
// as there is content. See #onOverScrolled() to see the second part of this change which properly
// aborts the scroller animation when we get to the bottom of the ScrollView content.
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Integer.MAX_VALUE, 0, scrollWindowHeight / 2);
ViewCompat.postInvalidateOnAnimation(this);
// END FB SCROLLVIEW CHANGE
} else {
super.fling(velocityY);
}
if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
mFlinging = true;
enableFpsListener();
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, 0, velocityY);
Runnable r = new Runnable() {
@Override
public void run() {
if (mDoneFlinging) {
mFlinging = false;
disableFpsListener();
ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this);
} else {
mDoneFlinging = true;
ViewCompat.postOnAnimationDelayed(RNScrollView.this, this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
};
ViewCompat.postOnAnimationDelayed(this, r, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
private void enableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
replacedertions.replacedertNotNull(mFpsListener);
replacedertions.replacedertNotNull(mScrollPerfTag);
mFpsListener.enable(mScrollPerfTag);
}
}
private void disableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
replacedertions.replacedertNotNull(mFpsListener);
replacedertions.replacedertNotNull(mScrollPerfTag);
mFpsListener.disable(mScrollPerfTag);
}
}
private boolean isScrollPerfLoggingEnabled() {
return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
}
private int getMaxScrollY() {
int contentHeight = mContentView.getHeight();
int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
return Math.max(0, contentHeight - viewportHeight);
}
@Override
public void draw(Canvas canvas) {
if (mEndFillColor != Color.TRANSPARENT) {
final View content = getChildAt(0);
if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
mEndBackground.draw(canvas);
}
}
super.draw(canvas);
}
public void setEndFillColor(int color) {
if (color != mEndFillColor) {
mEndFillColor = color;
mEndBackground = new ColorDrawable(mEndFillColor);
}
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
// more information.
if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
int scrollRange = getMaxScrollY();
if (scrollY >= scrollRange) {
mScroller.abortAnimation();
scrollY = scrollRange;
}
}
// END FB SCROLLVIEW CHANGE
}
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
@Override
public void onChildViewAdded(View parent, View child) {
mContentView = child;
mContentView.addOnLayoutChangeListener(this);
}
@Override
public void onChildViewRemoved(View parent, View child) {
mContentView.removeOnLayoutChangeListener(this);
mContentView = null;
}
/**
* Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
* after the content resizes. Without this, the user would see a blank ScrollView when the scroll
* position is larger than the ScrollView's max scroll position after the content shrinks.
*/
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (mContentView == null) {
return;
}
int currentScrollY = getScrollY();
int maxScrollY = getMaxScrollY();
if (currentScrollY > maxScrollY) {
scrollTo(getScrollX(), maxScrollY);
}
}
@Override
public void setBackgroundColor(int color) {
mReactBackgroundManager.setBackgroundColor(color);
}
public void setBorderWidth(int position, float width) {
mReactBackgroundManager.setBorderWidth(position, width);
}
public void setBorderColor(int position, float color, float alpha) {
mReactBackgroundManager.setBorderColor(position, color, alpha);
}
public void setBorderRadius(float borderRadius) {
mReactBackgroundManager.setBorderRadius(borderRadius);
}
public void setBorderRadius(float borderRadius, int position) {
mReactBackgroundManager.setBorderRadius(borderRadius, position);
}
public void setBorderStyle(@Nullable String style) {
mReactBackgroundManager.setBorderStyle(style);
}
}
19
Source : RNScrollView.java
with MIT License
from TaumuLu
with MIT License
from TaumuLu
public clreplaced RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {
private static Field sScrollerField;
private static boolean sTriedToGetScrollerField = false;
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
@Nullable
private Rect mClippingRect;
private boolean mDoneFlinging;
private boolean mDragging;
private boolean mFlinging;
private boolean mRemoveClippedSubviews;
static private boolean mScrollEnabled = true;
private boolean mSendMomentumEvents;
@Nullable
private FpsListener mFpsListener = null;
@Nullable
private String mScrollPerfTag;
@Nullable
private Drawable mEndBackground;
private int mEndFillColor = Color.TRANSPARENT;
private View mContentView;
@Nullable
private ReactViewBackgroundDrawable mReactBackgroundDrawable;
public RNScrollView(ReactContext context) {
this(context, null);
}
public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
super(context);
mFpsListener = fpsListener;
if (!sTriedToGetScrollerField) {
sTriedToGetScrollerField = true;
try {
sScrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
sScrollerField.setAccessible(true);
} catch (NoSuchFieldException e) {
Log.w(ReactConstants.TAG, "Failed to get mScroller field for ScrollView! " + "This app will exhibit the bounce-back scrolling bug :(");
}
}
if (sScrollerField != null) {
try {
Object scroller = sScrollerField.get(this);
if (scroller instanceof OverScroller) {
mScroller = (OverScroller) scroller;
} else {
Log.w(ReactConstants.TAG, "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + "This app will exhibit the bounce-back scrolling bug :(");
mScroller = null;
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
}
} else {
mScroller = null;
}
setOnHierarchyChangeListener(this);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
}
public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
}
public void setScrollPerfTag(String scrollPerfTag) {
mScrollPerfTag = scrollPerfTag;
}
public void setScrollEnabled(boolean scrollEnabled) {
mScrollEnabled = scrollEnabled;
// Log.i("setScrollEnabled", "value:" + scrollEnabled + " setValue:" + mScrollEnabled);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecreplacedertions.replacedertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Call with the present values in order to re-layout if necessary
scrollTo(getScrollX(), getScrollY());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mRemoveClippedSubviews) {
updateClippingRect();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mRemoveClippedSubviews) {
updateClippingRect();
}
}
@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
if (mRemoveClippedSubviews) {
updateClippingRect();
}
if (mFlinging) {
mDoneFlinging = false;
}
ReactScrollViewHelper.emitScrollEvent(this, mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity());
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Log.i("onInterceptTouchEvent", "value:" + mScrollEnabled);
if (!mScrollEnabled) {
return false;
}
if (super.onInterceptTouchEvent(ev)) {
// 会将滑动时的触摸操作停止
// NativeGestureUtil.notifyNativeGestureStarted(this, ev);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
enableFpsListener();
return true;
}
return false;
}
// @Override
// public boolean dispatchTouchEvent(MotionEvent ev) {
// Log.i("dispatchTouchEvent", "value:" + mScrollEnabled);
// if (!mScrollEnabled) {
// return false;
// }
// return super.dispatchTouchEvent(ev);
// }
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Log.i("onTouchEvent", "value:" + mScrollEnabled);
if (!mScrollEnabled) {
return false;
}
mVelocityHelper.calculateVelocity(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
ReactScrollViewHelper.emitScrollEndDragEvent(this, mVelocityHelper.getXVelocity(), mVelocityHelper.getYVelocity());
mDragging = false;
disableFpsListener();
}
return super.onTouchEvent(ev);
}
@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews && mClippingRect == null) {
mClippingRect = new Rect();
}
mRemoveClippedSubviews = removeClippedSubviews;
updateClippingRect();
}
@Override
public boolean getRemoveClippedSubviews() {
return mRemoveClippedSubviews;
}
@Override
public void updateClippingRect() {
if (!mRemoveClippedSubviews) {
return;
}
replacedertions.replacedertNotNull(mClippingRect);
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
View contentView = getChildAt(0);
if (contentView instanceof ReactClippingViewGroup) {
((ReactClippingViewGroup) contentView).updateClippingRect();
}
}
@Override
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(replacedertions.replacedertNotNull(mClippingRect));
}
@Override
public void fling(int velocityY) {
if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// We provide our own version of fling that uses a different call to the standard OverScroller
// which takes into account the possibility of adding new content while the ScrollView is
// animating. Because we give essentially no max Y for the fling, the fling will continue as long
// as there is content. See #onOverScrolled() to see the second part of this change which properly
// aborts the scroller animation when we get to the bottom of the ScrollView content.
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Integer.MAX_VALUE, 0, scrollWindowHeight / 2);
postInvalidateOnAnimation();
// END FB SCROLLVIEW CHANGE
} else {
super.fling(velocityY);
}
if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
mFlinging = true;
enableFpsListener();
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
Runnable r = new Runnable() {
@Override
public void run() {
if (mDoneFlinging) {
mFlinging = false;
disableFpsListener();
ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this);
} else {
mDoneFlinging = true;
RNScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
};
postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
private void enableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
replacedertions.replacedertNotNull(mFpsListener);
replacedertions.replacedertNotNull(mScrollPerfTag);
mFpsListener.enable(mScrollPerfTag);
}
}
private void disableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
replacedertions.replacedertNotNull(mFpsListener);
replacedertions.replacedertNotNull(mScrollPerfTag);
mFpsListener.disable(mScrollPerfTag);
}
}
private boolean isScrollPerfLoggingEnabled() {
return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
}
private int getMaxScrollY() {
int contentHeight = mContentView.getHeight();
int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
return Math.max(0, contentHeight - viewportHeight);
}
@Override
public void draw(Canvas canvas) {
if (mEndFillColor != Color.TRANSPARENT) {
final View content = getChildAt(0);
if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
mEndBackground.draw(canvas);
}
}
super.draw(canvas);
}
public void setEndFillColor(int color) {
if (color != mEndFillColor) {
mEndFillColor = color;
mEndBackground = new ColorDrawable(mEndFillColor);
}
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
// more information.
if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
int scrollRange = getMaxScrollY();
if (scrollY >= scrollRange) {
mScroller.abortAnimation();
scrollY = scrollRange;
}
}
// END FB SCROLLVIEW CHANGE
}
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
@Override
public void onChildViewAdded(View parent, View child) {
mContentView = child;
mContentView.addOnLayoutChangeListener(this);
}
@Override
public void onChildViewRemoved(View parent, View child) {
mContentView.removeOnLayoutChangeListener(this);
mContentView = null;
}
/**
* Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
* after the content resizes. Without this, the user would see a blank ScrollView when the scroll
* position is larger than the ScrollView's max scroll position after the content shrinks.
*/
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (mContentView == null) {
return;
}
int currentScrollY = getScrollY();
int maxScrollY = getMaxScrollY();
if (currentScrollY > maxScrollY) {
scrollTo(getScrollX(), maxScrollY);
}
}
public void setBackgroundColor(int color) {
if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) {
// don't do anything, no need to allocate ReactBackgroundDrawable for transparent background
} else {
getOrCreateReactViewBackground().setColor(color);
}
}
public void setBorderWidth(int position, float width) {
getOrCreateReactViewBackground().setBorderWidth(position, width);
}
public void setBorderColor(int position, float color, float alpha) {
getOrCreateReactViewBackground().setBorderColor(position, color, alpha);
}
public void setBorderRadius(float borderRadius) {
getOrCreateReactViewBackground().setRadius(borderRadius);
}
public void setBorderRadius(float borderRadius, int position) {
getOrCreateReactViewBackground().setRadius(borderRadius, position);
}
public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}
private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
if (mReactBackgroundDrawable == null) {
mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
Drawable backgroundDrawable = getBackground();
// required so that drawable callback is cleared before we add the
super.setBackground(null);
// drawable back as a part of LayerDrawable
if (backgroundDrawable == null) {
super.setBackground(mReactBackgroundDrawable);
} else {
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { mReactBackgroundDrawable, backgroundDrawable });
super.setBackground(layerDrawable);
}
}
return mReactBackgroundDrawable;
}
}
19
Source : SmartDragLayout.java
with Apache License 2.0
from TanZhiL
with Apache License 2.0
from TanZhiL
/**
* Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
* Create by dance, at 2018/12/23
*/
public clreplaced SmartDragLayout extends FrameLayout implements NestedScrollingParent {
private static final String TAG = "SmartDragLayout";
private View child;
OverScroller scroller;
VelocityTracker tracker;
ShadowBgAnimator bgAnimator = new ShadowBgAnimator();
// 是否启用手势拖拽
boolean enableDrag = true;
boolean dismissOnTouchOutside = true;
boolean hreplacedhadowBg = true;
boolean isUserClose = false;
// 是否开启三段拖拽
boolean isThreeDrag = false;
LayoutStatus status = LayoutStatus.Close;
public SmartDragLayout(Context context) {
this(context, null);
}
public SmartDragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (enableDrag) {
scroller = new OverScroller(context);
}
}
int maxY;
int minY;
@Override
public void onViewAdded(View c) {
super.onViewAdded(c);
child = c;
}
int lastHeight;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
maxY = child.getMeasuredHeight();
minY = 0;
int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
if (enableDrag) {
// horizontal center
child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
if (status == LayoutStatus.Open) {
if (isThreeDrag) {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
} else {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
}
}
} else {
// like bottom gravity
child.layout(l, getMeasuredHeight() - child.getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight());
}
lastHeight = maxY;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
isUserClose = true;
return super.dispatchTouchEvent(ev);
}
float touchX, touchY;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (enableDrag && scroller.computeScrollOffset()) {
touchX = 0;
touchY = 0;
return true;
}
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (enableDrag) {
if (tracker != null)
tracker.clear();
tracker = VelocityTracker.obtain();
}
touchX = event.getX();
touchY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (enableDrag && tracker != null) {
tracker.addMovement(event);
tracker.computeCurrentVelocity(1000);
int dy = (int) (event.getY() - touchY);
scrollTo(getScrollX(), getScrollY() - dy);
touchY = event.getY();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// click in child rect
Rect rect = new Rect();
child.getGlobalVisibleRect(rect);
if (!XPopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
performClick();
}
} else {
}
if (enableDrag && tracker != null) {
float yVelocity = tracker.getYVelocity();
if (yVelocity > 1500 && !isThreeDrag) {
close();
} else {
finishScroll();
}
// tracker.recycle();
tracker = null;
}
break;
}
return true;
}
private void finishScroll() {
if (enableDrag) {
int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
if (isThreeDrag) {
int per = maxY / 3;
if (getScrollY() > per * 2.5f) {
dy = maxY - getScrollY();
} else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
dy = per * 2 - getScrollY();
} else if (getScrollY() > per) {
dy = per - getScrollY();
} else {
dy = minY - getScrollY();
}
}
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, XPopup.getAnimationDuration());
ViewCompat.postInvalidateOnAnimation(this);
}
}
boolean isScrollUp;
@Override
public void scrollTo(int x, int y) {
if (y > maxY)
y = maxY;
if (y < minY)
y = minY;
float fraction = (y - minY) * 1f / (maxY - minY);
isScrollUp = y > getScrollY();
if (hreplacedhadowBg)
setBackgroundColor(bgAnimator.calculateBgColor(fraction));
if (listener != null) {
if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
status = LayoutStatus.Close;
listener.onClose();
} else if (fraction == 1f && status != LayoutStatus.Open) {
status = LayoutStatus.Open;
listener.onOpen();
}
listener.onDrag(y, fraction, isScrollUp);
}
super.scrollTo(x, y);
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isScrollUp = false;
isUserClose = false;
setTranslationY(0);
}
public void open() {
post(new Runnable() {
@Override
public void run() {
int dy = maxY - getScrollY();
smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
status = LayoutStatus.Opening;
}
});
}
public void close() {
isUserClose = true;
post(new Runnable() {
@Override
public void run() {
scroller.abortAnimation();
smoothScroll(minY - getScrollY(), false);
status = LayoutStatus.Closing;
}
});
}
public void smoothScroll(final int dy, final boolean isOpen) {
post(new Runnable() {
@Override
public void run() {
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? XPopup.getAnimationDuration() : XPopup.getAnimationDuration() * 0.8f));
ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
}
});
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// 必须要取消,否则会导致滑动初次延迟
scroller.abortAnimation();
}
@Override
public void onStopNestedScroll(View target) {
finishScroll();
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0) {
// scroll up
int newY = getScrollY() + dy;
if (newY < maxY) {
// dy不一定能消费完
consumed[1] = dy;
}
scrollTo(getScrollX(), newY);
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
if (isDragging && velocityY < -1500 && !isThreeDrag) {
close();
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return ViewCompat.SCROLL_AXIS_VERTICAL;
}
public void isThreeDrag(boolean isThreeDrag) {
this.isThreeDrag = isThreeDrag;
}
public void enableDrag(boolean enableDrag) {
this.enableDrag = enableDrag;
}
public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
this.dismissOnTouchOutside = dismissOnTouchOutside;
}
public void hreplacedhadowBg(boolean hreplacedhadowBg) {
this.hreplacedhadowBg = hreplacedhadowBg;
}
private OnCloseListener listener;
public void setOnCloseListener(OnCloseListener listener) {
this.listener = listener;
}
public interface OnCloseListener {
void onClose();
void onDrag(int y, float percent, boolean isScrollUp);
void onOpen();
}
}
19
Source : GingerScroller.java
with Apache License 2.0
from SwiftyWang
with Apache License 2.0
from SwiftyWang
@TargetApi(9)
public clreplaced GingerScroller extends ScrollerProxy {
protected final OverScroller mScroller;
private boolean mFirstScroll = false;
public GingerScroller(Context context) {
mScroller = new OverScroller(context);
}
@Override
public boolean computeScrollOffset() {
// Workaround for first scroll returning 0 for the direction of the edge it hits.
// Simply recompute values.
if (mFirstScroll) {
mScroller.computeScrollOffset();
mFirstScroll = false;
}
return mScroller.computeScrollOffset();
}
@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
}
@Override
public void forceFinished(boolean finished) {
mScroller.forceFinished(finished);
}
@Override
public boolean isFinished() {
return mScroller.isFinished();
}
@Override
public int getCurrX() {
return mScroller.getCurrX();
}
@Override
public int getCurrY() {
return mScroller.getCurrY();
}
}
19
Source : SystemGesturesPointerEventListener.java
with Apache License 2.0
from starscryer
with Apache License 2.0
from starscryer
/*
* Listens for system-wide input gestures, firing callbacks when detected.
* @hide
*/
public clreplaced SystemGesturesPointerEventListener implements PointerEventListener {
private static final String TAG = "SystemGestures";
private static final boolean DEBUG = false;
private static final long SWIPE_TIMEOUT_MS = 500;
// max per input system
private static final int MAX_TRACKED_POINTERS = 32;
private static final int UNTRACKED_POINTER = -1;
private static final int MAX_FLING_TIME_MILLIS = 5000;
private static final int SWIPE_NONE = 0;
private static final int SWIPE_FROM_TOP = 1;
private static final int SWIPE_FROM_BOTTOM = 2;
private static final int SWIPE_FROM_RIGHT = 3;
private static final int SWIPE_FROM_LEFT = 4;
private final Context mContext;
private final int mSwipeStartThreshold;
private final int mSwipeDistanceThreshold;
private final Callbacks mCallbacks;
private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];
private final float[] mDownX = new float[MAX_TRACKED_POINTERS];
private final float[] mDownY = new float[MAX_TRACKED_POINTERS];
private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];
private GestureDetector mGestureDetector;
private OverScroller mOverscroller;
private int screenHeight;
private int screenWidth;
private int mDownPointers;
private boolean mSwipeFireable;
private boolean mDebugFireable;
private boolean mMouseHoveringAtEdge;
private long mLastFlingTime;
public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
mContext = context;
mCallbacks = checkNull("callbacks", callbacks);
mSwipeStartThreshold = 100;
// = checkNull("context", context).getResources()
// .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
mSwipeDistanceThreshold = mSwipeStartThreshold;
if (DEBUG)
Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
}
private static <T> T checkNull(String name, T arg) {
if (arg == null) {
throw new IllegalArgumentException(name + " must not be null");
}
return arg;
}
public void systemReady() {
Handler h = new Handler(Looper.myLooper());
mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
mOverscroller = new OverScroller(mContext);
}
@Override
public void onPointerEvent(MotionEvent event) {
if (mGestureDetector != null && event.isTouchEvent()) {
mGestureDetector.onTouchEvent(event);
}
switch(event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mSwipeFireable = true;
mDebugFireable = true;
mDownPointers = 0;
captureDown(event, 0);
if (mMouseHoveringAtEdge) {
mMouseHoveringAtEdge = false;
mCallbacks.onMouseLeaveFromEdge();
}
mCallbacks.onDown();
break;
case MotionEvent.ACTION_POINTER_DOWN:
captureDown(event, event.getActionIndex());
if (mDebugFireable) {
mDebugFireable = event.getPointerCount() < 5;
if (!mDebugFireable) {
if (DEBUG)
Slog.d(TAG, "Firing debug");
mCallbacks.onDebug();
}
}
break;
case MotionEvent.ACTION_MOVE:
if (mSwipeFireable) {
final int swipe = detectSwipe(event);
mSwipeFireable = swipe == SWIPE_NONE;
if (swipe == SWIPE_FROM_TOP) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromTop");
mCallbacks.onSwipeFromTop();
} else if (swipe == SWIPE_FROM_BOTTOM) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromBottom");
int pointerId = event.getPointerId(0);
final int i = findIndex(pointerId);
int x = (int) mDownX[i];
int y = (int) mDownY[i];
mCallbacks.onSwipeFromBottom(x, y);
} else if (swipe == SWIPE_FROM_RIGHT) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromRight");
mCallbacks.onSwipeFromRight();
} else if (swipe == SWIPE_FROM_LEFT) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromLeft");
mCallbacks.onSwipeFromLeft();
}
}
break;
case MotionEvent.ACTION_HOVER_MOVE:
if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!mMouseHoveringAtEdge && event.getY() == 0) {
mCallbacks.onMouseHoverAtTop();
mMouseHoveringAtEdge = true;
} else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
mCallbacks.onMouseHoverAtBottom();
mMouseHoveringAtEdge = true;
} else if (mMouseHoveringAtEdge && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
mCallbacks.onMouseLeaveFromEdge();
mMouseHoveringAtEdge = false;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mSwipeFireable = false;
mDebugFireable = false;
mCallbacks.onUpOrCancel();
break;
default:
if (DEBUG)
Slog.d(TAG, "Ignoring " + event);
}
}
private void captureDown(MotionEvent event, int pointerIndex) {
final int pointerId = event.getPointerId(pointerIndex);
final int i = findIndex(pointerId);
if (DEBUG)
Slog.d(TAG, "pointer " + pointerId + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
if (i != UNTRACKED_POINTER) {
mDownX[i] = event.getX(pointerIndex);
mDownY[i] = event.getY(pointerIndex);
mDownTime[i] = event.getEventTime();
if (DEBUG)
Slog.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]);
}
}
private int findIndex(int pointerId) {
for (int i = 0; i < mDownPointers; i++) {
if (mDownPointerId[i] == pointerId) {
return i;
}
}
if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
return UNTRACKED_POINTER;
}
mDownPointerId[mDownPointers++] = pointerId;
return mDownPointers - 1;
}
private int detectSwipe(MotionEvent move) {
final int historySize = move.getHistorySize();
final int pointerCount = move.getPointerCount();
for (int p = 0; p < pointerCount; p++) {
final int pointerId = move.getPointerId(p);
final int i = findIndex(pointerId);
if (i != UNTRACKED_POINTER) {
for (int h = 0; h < historySize; h++) {
final long time = move.getHistoricalEventTime(h);
final float x = move.getHistoricalX(p, h);
final float y = move.getHistoricalY(p, h);
final int swipe = detectSwipe(i, time, x, y);
if (swipe != SWIPE_NONE) {
return swipe;
}
}
final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
if (swipe != SWIPE_NONE) {
return swipe;
}
}
}
return SWIPE_NONE;
}
private int detectSwipe(int i, long time, float x, float y) {
final float fromX = mDownX[i];
final float fromY = mDownY[i];
final long elapsed = time - mDownTime[i];
if (DEBUG)
Slog.d(TAG, "pointer " + mDownPointerId[i] + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
if (fromY <= mSwipeStartThreshold && y > fromY + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_TOP;
}
if (fromY >= screenHeight - mSwipeStartThreshold && y < fromY - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_BOTTOM;
}
if (fromX >= screenWidth - mSwipeStartThreshold && x < fromX - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_RIGHT;
}
if (fromX <= mSwipeStartThreshold && x > fromX + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_LEFT;
}
return SWIPE_NONE;
}
private final clreplaced FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (!mOverscroller.isFinished()) {
mOverscroller.forceFinished(true);
}
return true;
}
@Override
public boolean onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY) {
mOverscroller.computeScrollOffset();
long now = SystemClock.uptimeMillis();
if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
mOverscroller.forceFinished(true);
}
mOverscroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
int duration = mOverscroller.getDuration();
if (duration > MAX_FLING_TIME_MILLIS) {
duration = MAX_FLING_TIME_MILLIS;
}
mLastFlingTime = now;
mCallbacks.onFling(duration);
return true;
}
}
interface Callbacks {
void onSwipeFromTop();
void onSwipeFromBottom(int x, int y);
void onSwipeFromRight();
void onSwipeFromLeft();
void onFling(int durationMs);
void onDown();
void onUpOrCancel();
void onMouseHoverAtTop();
void onMouseHoverAtBottom();
void onMouseLeaveFromEdge();
void onDebug();
}
}
19
Source : CalendarView.java
with MIT License
from snollidea
with MIT License
from snollidea
public clreplaced CalendarView extends View {
/**
* Velocity threshold for smooth scroll to another month.
*/
private static final int VELOCITY_THRESHOLD = 2000;
/**
* Ratio between different values
*/
private static final float RATIO_ROW_HEIGHT_WIDTH = 0.098f;
private static final float RATIO_WIDTH_PADDING_X = 12.0f;
private static final float RATIO_WIDTH_PADDING_Y = 15.0f;
private static final float RATIO_WIDTH_TEXT_HEIGHT = 36.0f;
private static final float RATIO_WIDTH_CIRCLE_RADIUS = 27.0f;
private static final float RATIO_DURATION_DISTANCE = 0.75f;
private static final int DEFAULT_DAYS_IN_WEEK = 7;
/**
* Duration of resize animation in ms
*/
private static final int RESIZE_ANIMATION_DURATION = 200;
/**
* Used for shifting drawing items
*/
private int mOffset;
/**
* mPaddingX used for left and right padding
* mPaddingY used for top and bottom padding
*/
private float mPaddingX;
private float mPaddingY;
/**
* Space between objects in calendar for X and Y axis
*/
private float mBetweenX;
private float mBetweenY;
/**
* Used for dynamic resize from different parts of code
*/
private int mViewHeight;
private int mRowsCount;
/**
* Used for measuring text with Paint.getTextBounds method
*/
private Rect mTextRect = new Rect();
/**
* Names of days of the week
*/
private String[] mWeekDayNames;
/**
* Is view in resize animation
*/
private boolean mIsResize;
/**
* First day of the week is; e.g., SUNDAY in the U.S., MONDAY in France.
*/
private int mFirstDayOfWeek;
// XML Attributes
private int mBackgroundColor;
private int mTextColor;
private int mTextInsideCircleColor;
private int mWeekDaysNamesColor;
private int mCurrentDayCircleColor;
private int mSelectedDayCircleColor;
// Listeners
private OnDateSelectedListener mOnDateSelectedListener;
private OnMonthChangedListener mOnMonthChangedListener;
// Interactive
private GestureDetectorCompat mDetector;
private VelocityTracker mVelocityTracker;
private OverScroller mScroller;
// Drawing
private Paint mTextInsideCirclePaint;
private Paint mTextPaint;
private float mTextHeight;
private Paint mSelectedDayCirclePaint;
private Paint mCurrentDayCirclePaint;
private float mCircleRadius;
private Paint mEventCirclePaint;
private Paint mBackgroundPaint;
private Paint mWeekDaysNamesTextPaint;
private float mEventCircleRadius;
// Place where events points are located
private float mPlaceForPointsWidth;
private MonthPager mMonthPager;
public CalendarView(Context context) {
this(context, null);
}
public CalendarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CalendarView, 0, 0);
try {
mBackgroundColor = typedArray.getColor(R.styleable.CalendarView_backgroundColor, 0xffffffff);
mTextColor = typedArray.getColor(R.styleable.CalendarView_textColor, Color.BLACK);
mTextInsideCircleColor = typedArray.getColor(R.styleable.CalendarView_textInsideCircleColor, Color.WHITE);
mWeekDaysNamesColor = typedArray.getColor(R.styleable.CalendarView_weekDaysNamesColor, Color.GRAY);
mCurrentDayCircleColor = typedArray.getColor(R.styleable.CalendarView_currentDayCircleColor, Color.BLACK);
mSelectedDayCircleColor = typedArray.getColor(R.styleable.CalendarView_selectedCircleColor, Color.LTGRAY);
mFirstDayOfWeek = typedArray.getInt(R.styleable.CalendarView_firstDayOfWeek, Calendar.MONDAY);
} finally {
typedArray.recycle();
}
}
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Background drawing
canvas.drawRect(0, 0, getWidth(), getHeight(), mBackgroundPaint);
// Focused month drawing
drawMonth(canvas, FOCUSED_MONTH);
// If mOffset <= 0 previous month out of sight
if (mOffset > 0) {
drawMonth(canvas, PREVIOUS_MONTH);
}
// If mOffset >= 0 next month out of sight
if (mOffset < 0) {
drawMonth(canvas, NEXT_MONTH);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int minWidth = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
int width = resolveSizeAndState(minWidth, widthMeasureSpec, 1);
int height;
if (mIsResize) {
// mViewHeight is changing by resizeView() method
height = mViewHeight;
} else {
height = (int) (width * RATIO_ROW_HEIGHT_WIDTH * getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
mPaddingX = width / RATIO_WIDTH_PADDING_X;
mPaddingY = width / RATIO_WIDTH_PADDING_Y;
mBetweenX = (width - mPaddingX * 2) / (DEFAULT_DAYS_IN_WEEK - 1);
mBetweenY = (height / mRowsCount * 6 - mPaddingY * 2) / 5;
mTextHeight = width / RATIO_WIDTH_TEXT_HEIGHT;
mCircleRadius = width / RATIO_WIDTH_CIRCLE_RADIUS;
mEventCircleRadius = mCircleRadius / 7;
mPlaceForPointsWidth = mBetweenX / 2;
mTextPaint.setTextSize(mTextHeight);
mTextInsideCirclePaint.setTextSize(mTextHeight);
mWeekDaysNamesTextPaint.setTextSize(mTextHeight);
}
setMeasuredDimension(width, height);
}
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
switch(motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(motionEvent);
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
mVelocityTracker.computeCurrentVelocity(1000);
handleGesture(mVelocityTracker.getXVelocity());
mVelocityTracker.recycle();
mVelocityTracker.clear();
mVelocityTracker = null;
break;
}
return this.mDetector.onTouchEvent(motionEvent) || super.onTouchEvent(motionEvent);
}
private void init() {
mMonthPager = new MonthPager(mFirstDayOfWeek);
mScroller = new OverScroller(getContext());
mDetector = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
if (!mScroller.isFinished()) {
return true;
}
CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
float x = motionEvent.getX(), y = motionEvent.getY();
int day = getDayNumberOfCrd(x, y, calendarMonth.getFirstWeekDay());
if (day < 1 || day > calendarMonth.getAmountOfDays()) {
return true;
}
mMonthPager.selectDay(day);
invalidate();
dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(day));
return true;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float dx, float dy) {
getParent().requestDisallowInterceptTouchEvent(true);
int width = getWidth();
// Return if MonthPager reached max or min date
if ((dx > 0 && mMonthPager.isReachedMax()) || (dx < 0 && mMonthPager.isReachedMin())) {
return true;
}
mOffset -= dx;
// Set max offset value, if offset has reached one of the edges
if (mOffset > width) {
mOffset = width;
} else if (mOffset < -width) {
mOffset = -width;
}
invalidate();
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
mRowsCount = getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH));
mWeekDayNames = CommonUtils.getWeekDaysAbbreviation(mFirstDayOfWeek);
// Text of days numbers
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
// Text of selected and current day number
mTextInsideCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextInsideCirclePaint.setColor(mTextInsideCircleColor);
mSelectedDayCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mSelectedDayCirclePaint.setStyle(Paint.Style.FILL);
mSelectedDayCirclePaint.setColor(mSelectedDayCircleColor);
mCurrentDayCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCurrentDayCirclePaint.setStyle(Paint.Style.FILL);
mCurrentDayCirclePaint.setColor(mCurrentDayCircleColor);
mEventCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mEventCirclePaint.setStyle(Paint.Style.FILL);
// Week Name Text
mWeekDaysNamesTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWeekDaysNamesTextPaint.setColor(mWeekDaysNamesColor);
mWeekDaysNamesTextPaint.setTypeface(Typeface.DEFAULT_BOLD);
// Background
mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBackgroundPaint.setStyle(Paint.Style.FILL);
mBackgroundPaint.setColor(mBackgroundColor);
}
/**
* @param x The x position of touch event.
* @param y The y position of touch event.
* @param firstDayOfWeek First day of the week.
* @return Selected day of focused month.
*/
private int getDayNumberOfCrd(float x, float y, int firstDayOfWeek) {
// Measure text
mWeekDaysNamesTextPaint.getTextBounds(mWeekDayNames[0], 0, mWeekDayNames[0].length(), mTextRect);
float weekDaysNamesHeight = mTextRect.top + mBetweenY;
float widthPerDay = (getWidth() - mPaddingX * 2 + mBetweenX) / DEFAULT_DAYS_IN_WEEK;
float heightPerDay = (getHeight() - mPaddingY * 2 - weekDaysNamesHeight + mBetweenY) / (mRowsCount - 1);
x = x - mPaddingX + mBetweenX;
y = y - mPaddingY + mBetweenY - weekDaysNamesHeight;
int row = Math.round(x / widthPerDay);
int column = Math.round(y / heightPerDay);
return (column - 1) * DEFAULT_DAYS_IN_WEEK + row - firstDayOfWeek;
}
/**
* @param index The index on calendar grid
* @param text Day number text, for calculation text center, if it is needed. Set null for calculating circle position.
* @param monthIndex Month index, for calculating offset
* @return Float array for x[0] and y[1] position
*/
private float[] calculateCrdForIndex(int index, @Nullable String text, @MonthIndex int monthIndex) {
int rowIndex = (index - 1) % DEFAULT_DAYS_IN_WEEK;
int column = (index - 1) / DEFAULT_DAYS_IN_WEEK;
float x = mPaddingX + (mBetweenX * rowIndex) + (getWidth() * monthIndex);
float y = mPaddingY + (mBetweenY * column);
x += mOffset;
// Calculation of the text center
if (text != null) {
// Measure text size
mTextPaint.getTextBounds(text, 0, text.length(), mTextRect);
x -= mTextRect.centerX();
y -= mTextRect.centerY();
}
return new float[] { x, y };
}
private void drawMonth(Canvas canvas, @MonthIndex int monthIndex) {
CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(monthIndex);
// Selected day circle drawing
if (monthIndex == FOCUSED_MONTH) {
float[] crdCircle = calculateCrdForIndex(calendarMonth.getDayIndex(mMonthPager.getSelectedDay()), null, monthIndex);
canvas.drawCircle(crdCircle[0], crdCircle[1], mCircleRadius, mSelectedDayCirclePaint);
}
// Current day circle drawing
if (mMonthPager.isOnCurrentMonth(monthIndex)) {
float[] crdCircle = calculateCrdForIndex(calendarMonth.getDayIndex(mMonthPager.getCurrentDay()), null, monthIndex);
canvas.drawCircle(crdCircle[0], crdCircle[1], mCircleRadius, mCurrentDayCirclePaint);
}
// Week days names drawing
for (int i = 1; i <= DEFAULT_DAYS_IN_WEEK; i++) {
float[] crd = calculateCrdForIndex(i, mWeekDayNames[i - 1], monthIndex);
canvas.drawText(mWeekDayNames[i - 1], crd[0], crd[1], mWeekDaysNamesTextPaint);
}
// Numbers of days drawing
for (int day = 1; day <= calendarMonth.getAmountOfDays(); day++) {
int index = calendarMonth.getDayIndex(day);
float[] crd = calculateCrdForIndex(index, Integer.toString(day), monthIndex);
boolean isCurrentDay = mMonthPager.isOnCurrentMonth(monthIndex) && day == mMonthPager.getCurrentDay();
boolean isSelectedDay = monthIndex == FOCUSED_MONTH && day == mMonthPager.getSelectedDay();
canvas.drawText(Integer.toString(day), crd[0], crd[1], isCurrentDay || isSelectedDay ? mTextInsideCirclePaint : mTextPaint);
// Events drawing
List<CalendarEvent> events = calendarMonth.getEventOfDay(day);
if (events != null && !isCurrentDay && !isSelectedDay) {
drawEventsOfDay(canvas, events, crd, day);
}
}
}
private void drawEventsOfDay(Canvas canvas, List<CalendarEvent> events, float[] crd, int day) {
// Measure text of events day number
String dayText = Integer.toString(day);
mTextPaint.getTextBounds(dayText, 0, dayText.length(), mTextRect);
float offsetForCenter = mPlaceForPointsWidth / 2 - mTextRect.centerX();
// Space between events points for X axis
float betweenPoints = mPlaceForPointsWidth / (events.size() + 1);
for (int i = 0; i < events.size(); i++) {
mEventCirclePaint.setColor(events.get(i).getColor());
canvas.drawCircle(crd[0] - offsetForCenter + betweenPoints * (i + 1), crd[1] + mCircleRadius / 2, mEventCircleRadius, mEventCirclePaint);
}
}
private void handleGesture(float velocity) {
if (velocity == 0 && mOffset == 0) {
return;
}
if ((velocity > VELOCITY_THRESHOLD || mOffset > getWidth() / 2) && (!mMonthPager.isReachedMin())) {
if (!canGoBack()) {
handleGesture(0);
return;
}
mMonthPager.goBack();
int distance = getWidth() - mOffset;
// Invalidate offset, because of changed focused month
mOffset = mOffset - getWidth();
mScroller.startScroll(mOffset, 0, distance, 0, (int) (Math.abs(distance) * RATIO_DURATION_DISTANCE));
dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
resizeView(getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
ViewCompat.postInvalidateOnAnimation(this);
} else if ((velocity < -VELOCITY_THRESHOLD || mOffset < -getWidth() / 2) && (!mMonthPager.isReachedMax())) {
if (!canGoForward()) {
handleGesture(0);
return;
}
mMonthPager.goForward();
int distance = -getWidth() - mOffset;
// Invalidate offset, because of changed focused month
mOffset = mOffset + getWidth();
mScroller.startScroll(mOffset, 0, distance, 0, (int) (Math.abs(distance) * RATIO_DURATION_DISTANCE));
dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
resizeView(getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
ViewCompat.postInvalidateOnAnimation(this);
} else {
// Smooth scroll to focused month
int distance = -mOffset;
mScroller.startScroll(mOffset, 0, distance, 0, // More slowly scroll (x2 duration)
(int) (Math.abs(distance) * RATIO_DURATION_DISTANCE * 2));
ViewCompat.postInvalidateOnAnimation(this);
}
}
private boolean canGoForward() {
return mOffset <= 0;
}
private boolean canGoBack() {
return mOffset >= 0;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
mOffset = mScroller.getCurrX();
invalidate();
if (mOffset == mScroller.getFinalX()) {
mScroller.forceFinished(true);
// First day of month selected, after changing month
CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
}
}
}
/**
* @param calendarMonth Target month.
* @return Number of required rows, for drawing target month.
*/
private int getMonthRowsCount(CalendarMonth calendarMonth) {
float rowsCount = (float) (calendarMonth.getAmountOfDays() + calendarMonth.getFirstWeekDay()) / DEFAULT_DAYS_IN_WEEK;
// + 1 for week days names row
return (int) Math.ceil(rowsCount) + 1;
}
/**
* Change view height, for next month
* @param targetRowsCount Number of next month rows
*/
private void resizeView(int targetRowsCount) {
// If current rows count are equals to target rows count resize not required
if (mRowsCount == targetRowsCount) {
return;
}
clreplaced ResizeAnimation extends Animation {
private int mTargetHeight;
private View mView;
private int mStartHeight;
private ResizeAnimation(View view, int targetHeight, int startHeight) {
mView = view;
mTargetHeight = targetHeight;
mStartHeight = startHeight;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newHeight = (int) (mStartHeight + (mTargetHeight - mStartHeight) * interpolatedTime);
mViewHeight = newHeight;
mView.getLayoutParams().height = newHeight;
mView.requestLayout();
if (interpolatedTime == 1.0f) {
// Animation is over
mIsResize = false;
}
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
}
ResizeAnimation resizeAnimation = new ResizeAnimation(this, getHeight() * targetRowsCount / mRowsCount, getHeight());
resizeAnimation.setDuration(RESIZE_ANIMATION_DURATION);
startAnimation(resizeAnimation);
mIsResize = true;
mRowsCount = targetRowsCount;
}
// Perform listeners
private void dispatchOnDateSelected(Calendar calendar, List<CalendarEvent> eventsOfDay) {
if (mOnDateSelectedListener != null) {
mOnDateSelectedListener.onDateSelected(calendar, eventsOfDay);
}
}
private void dispatchOnMonthChanged(Calendar calendar) {
if (mOnMonthChangedListener != null) {
mOnMonthChangedListener.onMonthChanged(calendar);
}
}
// Public methods
public void updateEvents() {
mMonthPager.updateEvents();
}
public void setFirstDayOfWeek(int dayOfWeek) {
if (dayOfWeek < 1 || dayOfWeek > 7) {
throw new IllegalArgumentException("Day must be from Java Calendar clreplaced");
}
mFirstDayOfWeek = dayOfWeek;
mWeekDayNames = CommonUtils.getWeekDaysAbbreviation(mFirstDayOfWeek);
mMonthPager.setFirstDayOfWeek(dayOfWeek);
invalidate();
}
public void setMinimumDate(long timeInMillis) {
mMonthPager.setMinimumDate(timeInMillis);
dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
invalidate();
}
public void setMaximumDate(long timeInMillis) {
mMonthPager.setMaximumDate(timeInMillis);
dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
invalidate();
}
public void setOnDateSelectedListener(OnDateSelectedListener onDateSelectedListener) {
mOnDateSelectedListener = onDateSelectedListener;
CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
}
public void setOnMonthChangedListener(OnMonthChangedListener onMonthChangedListener) {
mOnMonthChangedListener = onMonthChangedListener;
dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
}
public void setOnLoadEventsListener(OnLoadEventsListener onLoadEventsListener) {
mMonthPager.setOnLoadEventsListener(onLoadEventsListener);
CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
}
public void setBackgroundColor(@ColorInt int color) {
mBackgroundColor = color;
mBackgroundPaint.setColor(mBackgroundColor);
invalidate();
}
public int getBackgroundColor() {
return mBackgroundColor;
}
public void setTextColor(@ColorInt int color) {
mTextColor = color;
mTextPaint.setColor(mTextColor);
invalidate();
}
public int getTextColor() {
return mTextColor;
}
public void setTextInsideCircleColor(@ColorInt int color) {
mTextInsideCircleColor = color;
mTextInsideCirclePaint.setColor(mTextInsideCircleColor);
invalidate();
}
public int getTextInsideCircleColor() {
return mTextInsideCircleColor;
}
public void setWeekDaysNamesColor(@ColorInt int color) {
mWeekDaysNamesColor = color;
mWeekDaysNamesTextPaint.setColor(mWeekDaysNamesColor);
invalidate();
}
public int getWeekDaysNamesColor() {
return mWeekDaysNamesColor;
}
public void setCurrentDayCircleColor(@ColorInt int color) {
mCurrentDayCircleColor = color;
mCurrentDayCirclePaint.setColor(mCurrentDayCircleColor);
invalidate();
}
public int getCurrentDayCircleColor() {
return mCurrentDayCircleColor;
}
public void setSelectedDayCircleColor(@ColorInt int color) {
mSelectedDayCircleColor = color;
mSelectedDayCirclePaint.setColor(mSelectedDayCircleColor);
invalidate();
}
public int getSelectedDayCircleColor() {
return mSelectedDayCircleColor;
}
public Calendar getFocusedMonthCalendar() {
return mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar();
}
}
19
Source : CoordinatorLinearLayout.java
with MIT License
from Skykai521
with MIT License
from Skykai521
/**
* Created by sky on 17/3/1.
*/
public clreplaced CoordinatorLinearLayout extends LinearLayout implements CoordinatorListener {
public static int DEFAULT_DURATION = 500;
private int state = WHOLE_STATE;
private int topBarHeight;
private int topViewHeight;
private int minScrollToTop;
private int minScrollToWhole;
private int maxScrollDistance;
private float lastPositionY;
private boolean beingDragged;
private Context context;
private OverScroller scroller;
public CoordinatorLinearLayout(Context context) {
this(context, null);
}
public CoordinatorLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CoordinatorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init() {
scroller = new OverScroller(context);
}
public void setTopViewParam(int topViewHeight, int topBarHeight) {
this.topViewHeight = topViewHeight;
this.topBarHeight = topBarHeight;
this.maxScrollDistance = this.topViewHeight - this.topBarHeight;
this.minScrollToTop = this.topBarHeight;
this.minScrollToWhole = maxScrollDistance - this.topBarHeight;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
int y = (int) ev.getY();
lastPositionY = y;
if (state == COLLAPSE_STATE && y < topBarHeight) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final int y = (int) ev.getRawY();
switch(action) {
case MotionEvent.ACTION_DOWN:
lastPositionY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = (int) (lastPositionY - y);
if (state == COLLAPSE_STATE && deltaY < 0) {
beingDragged = true;
setScrollY(maxScrollDistance + deltaY);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (beingDragged) {
onSwitch();
return true;
}
break;
}
return true;
}
@Override
public boolean onCoordinateScroll(int x, int y, int deltaX, int deltaY, boolean isScrollToTop) {
if (y < topViewHeight && state == WHOLE_STATE && getScrollY() < getScrollRange()) {
beingDragged = true;
setScrollY(topViewHeight - y);
return true;
} else if (isScrollToTop && state == COLLAPSE_STATE && deltaY < 0) {
beingDragged = true;
setScrollY(maxScrollDistance + deltaY);
return true;
} else {
return false;
}
}
@Override
public void onSwitch() {
if (state == WHOLE_STATE) {
if (getScrollY() >= minScrollToTop) {
switchToTop();
} else {
switchToWhole();
}
} else if (state == COLLAPSE_STATE) {
if (getScrollY() <= minScrollToWhole) {
switchToWhole();
} else {
switchToTop();
}
}
}
@Override
public boolean isBeingDragged() {
return beingDragged;
}
public void switchToWhole() {
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
scroller.startScroll(0, getScrollY(), 0, -getScrollY(), DEFAULT_DURATION);
postInvalidate();
state = WHOLE_STATE;
beingDragged = false;
}
public void switchToTop() {
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
scroller.startScroll(0, getScrollY(), 0, getScrollRange() - getScrollY(), DEFAULT_DURATION);
postInvalidate();
state = COLLAPSE_STATE;
beingDragged = false;
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
setScrollY(scroller.getCurrY());
postInvalidate();
}
}
private int getScrollRange() {
return maxScrollDistance;
}
}
19
Source : MainHeaderBehavior.java
with Apache License 2.0
from SheHuan
with Apache License 2.0
from SheHuan
public clreplaced MainHeaderBehavior extends ViewOffsetBehavior<View> {
private static final int STATE_OPENED = 0;
private static final int STATE_CLOSED = 1;
private static final int DURATION_SHORT = 300;
private static final int DURATION_LONG = 600;
private int mCurState = STATE_OPENED;
private OnHeaderStateListener mHeaderStateListener;
private OverScroller mOverScroller;
// CoordinatorLayout
private WeakReference<CoordinatorLayout> mParent;
// CoordinatorLayout的子View,即header
private WeakReference<View> mChild;
// 界面整体向上滑动,达到列表可滑动的临界点
private boolean upReach;
// 列表向上滑动后,再向下滑动,达到界面整体可滑动的临界点
private boolean downReach;
// 列表上一个全部可见的item位置
private int lastPosition = -1;
private FlingRunnable mFlingRunnable;
private Context mContext;
// tab上移结束后是否悬浮在固定位置
private boolean tabSuspension = false;
public MainHeaderBehavior() {
init();
}
public MainHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
private void init() {
mOverScroller = new OverScroller(mContext);
}
@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
super.layoutChild(parent, child, layoutDirection);
mParent = new WeakReference<>(parent);
mChild = new WeakReference<>(child);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
if (tabSuspension) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && !isClosed();
}
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) {
lastPosition = -1;
return !isClosed();
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent ev) {
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downReach = false;
upReach = false;
break;
case MotionEvent.ACTION_UP:
handleActionUp(child);
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
/**
* @param coordinatorLayout
* @param child 代表header
* @param target 代表RecyclerView
* @param dx
* @param dy 上滑 dy>0, 下滑dy<0
* @param consumed
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
// 制造滑动视察,使header的移动比手指滑动慢
float scrollY = dy / 4.0f;
if (target instanceof NestedLinearLayout) {
// 处理header滑动
float finalY = child.getTranslationY() - scrollY;
if (finalY < getHeaderOffset()) {
finalY = getHeaderOffset();
} else if (finalY > 0) {
finalY = 0;
}
child.setTranslationY(finalY);
consumed[1] = dy;
} else if (target instanceof RecyclerView) {
// 处理列表滑动
RecyclerView list = (RecyclerView) target;
int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
// header closed状态下,列表上滑再下滑到第一个item全部显示,此时不让CoordinatorLayout整体下滑,
// 手指重新抬起再下滑才可以整体滑动
if (pos == 0 && pos < lastPosition) {
downReach = true;
}
if (pos == 0 && canScroll(child, scrollY)) {
// 如果列表第一个item全部可见、或者header已展开,则让CoordinatorLayout消费掉事件
float finalY = child.getTranslationY() - scrollY;
// header已经closed,整体不能继续上滑,手指抬起重新上滑列表开始滚动
if (finalY < getHeaderOffset()) {
finalY = getHeaderOffset();
upReach = true;
} else if (finalY > 0) {
// header已经opened,整体不能继续下滑
finalY = 0;
}
child.setTranslationY(finalY);
// 让CoordinatorLayout消费掉事件,实现整体滑动
consumed[1] = dy;
}
lastPosition = pos;
}
}
/**
* 是否可以整体滑动
*
* @return
*/
private boolean canScroll(View child, float scrollY) {
if (scrollY > 0 && child.getTranslationY() > getHeaderOffset()) {
return true;
}
if (child.getTranslationY() == getHeaderOffset() && upReach) {
return true;
}
if (scrollY < 0 && !downReach) {
return true;
}
return false;
}
private int getHeaderOffset() {
return mContext.getResources().getDimensionPixelOffset(R.dimen.header_offset);
}
private void handleActionUp(View child) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
// 手指抬起时,header上滑距离超过总距离三分之一,则整体自动上滑到关闭状态
if (child.getTranslationY() < getHeaderOffset() / 3.0f) {
scrollToClose(DURATION_SHORT);
} else {
scrollToOpen(DURATION_SHORT);
}
}
private void onFlingFinished(View layout) {
changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
}
/**
* 直接展开
*/
public void openHeader() {
openHeader(DURATION_LONG);
}
private void openHeader(int duration) {
if (isClosed() && mChild.get() != null) {
if (mFlingRunnable != null) {
mChild.get().removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
scrollToOpen(duration);
}
}
public void closeHeader() {
closeHeader(DURATION_LONG);
}
private void closeHeader(int duration) {
if (!isClosed() && mChild.get() != null) {
if (mFlingRunnable != null) {
mChild.get().removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
scrollToClose(duration);
}
}
private boolean isClosed(View child) {
return child.getTranslationY() == getHeaderOffset();
}
public boolean isClosed() {
return mCurState == STATE_CLOSED;
}
private void changeState(int newState) {
if (mCurState != newState) {
mCurState = newState;
if (mHeaderStateListener == null) {
return;
}
if (mCurState == STATE_OPENED) {
mHeaderStateListener.onHeaderOpened();
} else {
mHeaderStateListener.onHeaderClosed();
}
}
}
private void scrollToClose(int duration) {
int curTranslationY = (int) mChild.get().getTranslationY();
int dy = getHeaderOffset() - curTranslationY;
mOverScroller.startScroll(0, curTranslationY, 0, dy, duration);
start();
}
private void scrollToOpen(int duration) {
float curTranslationY = mChild.get().getTranslationY();
mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY, duration);
start();
}
private void start() {
if (mOverScroller.computeScrollOffset()) {
mFlingRunnable = new FlingRunnable(mParent.get(), mChild.get());
ViewCompat.postOnAnimation(mChild.get(), mFlingRunnable);
} else {
onFlingFinished(mChild.get());
}
}
private clreplaced FlingRunnable implements Runnable {
private final CoordinatorLayout mParent;
private final View mLayout;
FlingRunnable(CoordinatorLayout parent, View layout) {
mParent = parent;
mLayout = layout;
}
@Override
public void run() {
if (mLayout != null && mOverScroller != null) {
if (mOverScroller.computeScrollOffset()) {
mLayout.setTranslationY(mOverScroller.getCurrY());
ViewCompat.postOnAnimation(mLayout, this);
} else {
onFlingFinished(mLayout);
}
}
}
}
public void setTabSuspension(boolean tabSuspension) {
this.tabSuspension = tabSuspension;
}
public void setHeaderStateListener(OnHeaderStateListener headerStateListener) {
mHeaderStateListener = headerStateListener;
}
public interface OnHeaderStateListener {
void onHeaderClosed();
void onHeaderOpened();
}
}
19
Source : NestedScrollChildSample.java
with Apache License 2.0
from RubiTree
with Apache License 2.0
from RubiTree
/**
* >> Description <<
* 虽然没有报错,但这不是可以运行的代码,这是剔除 NestedScrollView 中关于 parent 的部分,得到的可以认为是官方的
* NestedScrollingChild 接口的实现建议,关键是在在触摸和滚动时怎么调用 NestedScrollingChild 的方法,也就是下
* 面 onInterceptTouchEvent() 、 onTouchEvent() 、 computeScroll() 中不到 200 行的代码
* <p>
* >> Attention <<
* 这里为了让主线逻辑更加清晰,省略了多点触控相关的代码,实际开发如果需要,可以直接参考 NestedScrollView 中的写
* 法,也不会很麻烦
* <p>
* >> Others <<
* <p>
* Created by RubiTree ; On 2019-01-08.
*/
public clreplaced NestedScrollChildSample extends FrameLayout implements NestedScrollingChild3 {
private OverScroller mScroller;
/**
* True if the user is currently dragging this ScrollView around. This is
* not the same as 'is being flinged', which can be checked by
* mScroller.isFinished() (flinging begins when the user lifts his finger).
*/
private boolean mIsBeingDragged = false;
/**
* Determines speed during touch scrolling
*/
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMinimumVelocity;
private int mMaximumVelocity;
/**
* Used during scrolling to retrieve the new offset within the window.
*/
private final int[] mScrollOffset = new int[2];
private int mNestedYOffset;
private final int[] mScrollConsumed = new int[2];
private int mLastScrollerY;
private int mLastMotionY;
private final NestedScrollingChildHelper mChildHelper;
/*--------------------------------------------------------------------------------------------*/
public NestedScrollChildSample(@NonNull Context context) {
this(context, null);
}
public NestedScrollChildSample(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollChildSample(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
/*--------------------------------------------------------------------------------------------*/
// NestedScrollingChild3
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
// NestedScrollingChild2
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent(int type) {
return mChildHelper.hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void stopNestedScroll() {
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean hasNestedScrollingParent() {
return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
/*--------------------------------------------------------------------------------------------*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch(action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
{
mLastMotionY = (int) ev.getY();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
mScroller.computeScrollOffset();
mIsBeingDragged = isSelfScrolling();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
{
if (!mIsBeingDragged) {
final int y = (int) ev.getY();
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
requestParentDisallowInterceptTouchEvent();
}
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
trySpringBack();
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int actionMasked = ev.getActionMasked();
MotionEvent vtev = MotionEvent.obtain(ev);
if (actionMasked == MotionEvent.ACTION_DOWN)
mNestedYOffset = 0;
vtev.offsetLocation(0, mNestedYOffset);
switch(actionMasked) {
case MotionEvent.ACTION_DOWN:
{
if ((mIsBeingDragged = isSelfScrolling()))
requestParentDisallowInterceptTouchEvent();
// If being flinged and user touches, stop the fling. isFinished will be false if being flinged.
if (isSelfScrolling())
abortAnimatedScroll();
mLastMotionY = (int) ev.getY();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
final int y = (int) ev.getY();
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
requestParentDisallowInterceptTouchEvent();
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
// Calling overScrollByCompat will call onOverScrolled, which calls onScrollChanged if applicable.
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, getScrollRange(), 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
deltaY -= mScrollConsumed[1];
if (canOverscroll())
showOverScrollEdgeEffect();
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity();
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
if (!dispatchNestedPreFling(0, -initialVelocity)) {
dispatchNestedFling(0, -initialVelocity, true);
fling(-initialVelocity);
}
} else {
trySpringBack();
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0)
trySpringBack();
endDrag();
break;
}
if (mVelocityTracker != null)
mVelocityTracker.addMovement(vtev);
vtev.recycle();
return true;
}
@Override
public void computeScroll() {
if (mScroller.isFinished())
return;
mScroller.computeScrollOffset();
final int y = mScroller.getCurrY();
int unconsumed = y - mLastScrollerY;
mLastScrollerY = y;
// Nested Scrolling Pre Preplaced
mScrollConsumed[1] = 0;
dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH);
unconsumed -= mScrollConsumed[1];
if (unconsumed != 0) {
// Internal Scroll
final int oldScrollY = getScrollY();
overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(), 0, 0, false);
final int scrolledByMe = getScrollY() - oldScrollY;
unconsumed -= scrolledByMe;
// Nested Scrolling Post Preplaced
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, null, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
unconsumed -= mScrollConsumed[1];
}
if (unconsumed != 0) {
if (canOverscroll())
showOverScrollEdgeEffect();
abortAnimatedScroll();
}
if (isSelfScrolling())
ViewCompat.postInvalidateOnAnimation(this);
}
private void abortAnimatedScroll() {
mScroller.abortAnimation();
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
public void fling(int velocityY) {
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
}
private void endDrag() {
mIsBeingDragged = false;
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
/*--------------------------------------------------------------------------------------------*/
// Fake
private int getScrollRange() {
return 0;
}
// Fake
// scroll self
private boolean overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
return true;
}
// Fake
private void showOverScrollEdgeEffect() {
}
private boolean isSelfScrolling() {
return !mScroller.isFinished();
}
private void requestParentDisallowInterceptTouchEvent() {
final ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(true);
}
private void trySpringBack() {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private boolean canOverscroll() {
final int mode = getOverScrollMode();
return mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && getScrollRange() > 0);
}
}
19
Source : ResolverDrawerLayout.java
with MIT License
from RikkaApps
with MIT License
from RikkaApps
public clreplaced ResolverDrawerLayout extends ViewGroup {
private static final String TAG = "ResolverDrawerLayout";
/**
* Max width of the whole drawer layout
*/
private int mMaxWidth;
/**
* Max total visible height of views not marked always-show when in the closed/initial state
*/
private int mMaxCollapsedHeight;
/**
* Max total visible height of views not marked always-show when in the closed/initial state
* when a default option is present
*/
private int mMaxCollapsedHeightSmall;
private boolean mSmallCollapsed;
/**
* Move views down from the top by this much in px
*/
private float mCollapseOffset;
/**
* Track fractions of pixels from drag calculations. Without this, the view offsets get
* out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
*/
private float mDragRemainder = 0.0f;
private int mCollapsibleHeight;
private int mUncollapsibleHeight;
private int mAlwaysShowHeight;
/**
* The height in pixels of reserved space added to the top of the collapsed UI;
* e.g. chooser targets
*/
private int mCollapsibleHeightReserved;
private int mTopOffset;
private boolean mShowAtTop;
private boolean mIsDragging;
private boolean mOpenOnClick;
private boolean mOpenOnLayout;
private boolean mDismissOnScrollerFinished;
private final int mTouchSlop;
private final float mMinFlingVelocity;
private final OverScroller mScroller;
private final VelocityTracker mVelocityTracker;
private Drawable mScrollIndicatorDrawable;
private OnDismissedListener mOnDismissedListener;
private RunOnDismissedListener mRunOnDismissedListener;
private OnCollapsedChangedListener mOnCollapsedChangedListener;
private boolean mDismissLocked;
private float mInitialTouchX;
private float mInitialTouchY;
private float mLastTouchY;
private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
private final Rect mTempRect = new Rect();
private AbsListView mNestedScrollingChild;
private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = new ViewTreeObserver.OnTouchModeChangeListener() {
@Override
public void onTouchModeChanged(boolean isInTouchMode) {
if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
smoothScrollTo(0, 0);
}
}
};
public ResolverDrawerLayout(Context context) {
this(context, null);
}
public ResolverDrawerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, defStyleAttr, 0);
mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
mMaxCollapsedHeight = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
mMaxCollapsedHeightSmall = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, mMaxCollapsedHeight);
mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
a.recycle();
mScrollIndicatorDrawable = context.getDrawable(R.drawable.scroll_indicator_material);
mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, android.R.interpolator.decelerate_quint));
mVelocityTracker = VelocityTracker.obtain();
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
public void setSmallCollapsed(boolean smallCollapsed) {
mSmallCollapsed = smallCollapsed;
requestLayout();
}
public boolean isSmallCollapsed() {
return mSmallCollapsed;
}
public boolean isCollapsed() {
return mCollapseOffset > 0;
}
public void setShowAtTop(boolean showOnTop) {
mShowAtTop = showOnTop;
invalidate();
requestLayout();
}
public boolean getShowAtTop() {
return mShowAtTop;
}
public void setCollapsed(boolean collapsed) {
if (!isLaidOut()) {
mOpenOnLayout = collapsed;
} else {
smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
}
}
public void setCollapsibleHeightReserved(int heightPixels) {
final int oldReserved = mCollapsibleHeightReserved;
mCollapsibleHeightReserved = heightPixels;
final int dReserved = mCollapsibleHeightReserved - oldReserved;
if (dReserved != 0 && mIsDragging) {
mLastTouchY -= dReserved;
}
final int oldCollapsibleHeight = mCollapsibleHeight;
mCollapsibleHeight = Math.max(mCollapsibleHeight, getMaxCollapsedHeight());
if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
return;
}
invalidate();
}
public void setDismissLocked(boolean locked) {
mDismissLocked = locked;
}
private boolean isMoving() {
return mIsDragging || !mScroller.isFinished();
}
private boolean isDragging() {
return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
}
private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
if (oldCollapsibleHeight == mCollapsibleHeight) {
return false;
}
if (getShowAtTop()) {
// Keep the drawer fully open.
mCollapseOffset = 0;
return false;
}
if (isLaidOut()) {
final boolean isCollapsedOld = mCollapseOffset != 0;
if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight && mCollapseOffset == oldCollapsibleHeight)) {
// Stay closed even at the new height.
mCollapseOffset = mCollapsibleHeight;
} else {
mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
}
final boolean isCollapsedNew = mCollapseOffset != 0;
if (isCollapsedOld != isCollapsedNew) {
onCollapsedChanged(isCollapsedNew);
}
} else {
// Start out collapsed at first unless we restored state for otherwise
mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
}
return true;
}
private int getMaxCollapsedHeight() {
return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) + mCollapsibleHeightReserved;
}
public void setOnDismissedListener(OnDismissedListener listener) {
mOnDismissedListener = listener;
}
private boolean isDismissable() {
return mOnDismissedListener != null && !mDismissLocked;
}
public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
mOnCollapsedChangedListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final float x = ev.getX();
final float y = ev.getY();
mInitialTouchX = x;
mInitialTouchY = mLastTouchY = y;
mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
}
break;
case MotionEvent.ACTION_MOVE:
{
final float x = ev.getX();
final float y = ev.getY();
final float dy = y - mInitialTouchY;
if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mActivePointerId = ev.getPointerId(0);
mIsDragging = true;
mLastTouchY = Math.max(mLastTouchY - mTouchSlop, Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
{
onSecondaryPointerUp(ev);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
{
resetTouch();
}
break;
}
if (mIsDragging) {
abortAnimation();
}
return mIsDragging || mOpenOnClick;
}
private boolean isNestedChildScrolled() {
return mNestedScrollingChild != null && mNestedScrollingChild.getChildCount() > 0 && (mNestedScrollingChild.getFirstVisiblePosition() > 0 || mNestedScrollingChild.getChildAt(0).getTop() < 0);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
mVelocityTracker.addMovement(ev);
boolean handled = false;
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final float x = ev.getX();
final float y = ev.getY();
mInitialTouchX = x;
mInitialTouchY = mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
handled = isDismissable() || mCollapsibleHeight > 0;
mIsDragging = hitView && handled;
abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
{
int index = ev.findPointerIndex(mActivePointerId);
if (index < 0) {
Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
index = 0;
mActivePointerId = ev.getPointerId(0);
mInitialTouchX = ev.getX();
mInitialTouchY = mLastTouchY = ev.getY();
}
final float x = ev.getX(index);
final float y = ev.getY(index);
if (!mIsDragging) {
final float dy = y - mInitialTouchY;
if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
handled = mIsDragging = true;
mLastTouchY = Math.max(mLastTouchY - mTouchSlop, Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
}
}
if (mIsDragging) {
final float dy = y - mLastTouchY;
if (dy > 0 && isNestedChildScrolled()) {
mNestedScrollingChild.smoothScrollBy((int) -dy, 0);
} else {
performDrag(dy);
}
}
mLastTouchY = y;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
{
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
mActivePointerId = pointerId;
mInitialTouchX = ev.getX(pointerIndex);
mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
}
break;
case MotionEvent.ACTION_POINTER_UP:
{
onSecondaryPointerUp(ev);
}
break;
case MotionEvent.ACTION_UP:
{
final boolean wasDragging = mIsDragging;
mIsDragging = false;
if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && findChildUnder(ev.getX(), ev.getY()) == null) {
if (isDismissable()) {
dispatchOnDismissed();
resetTouch();
return true;
}
}
if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
smoothScrollTo(0, 0);
return true;
}
mVelocityTracker.computeCurrentVelocity(1000);
final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(yvel) > mMinFlingVelocity) {
if (getShowAtTop()) {
if (isDismissable() && yvel < 0) {
abortAnimation();
dismiss();
} else {
smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
}
} else {
if (isDismissable() && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
mDismissOnScrollerFinished = true;
} else {
if (isNestedChildScrolled()) {
mNestedScrollingChild.smoothScrollToPosition(0);
}
smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
}
}
} else {
smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
}
resetTouch();
}
break;
case MotionEvent.ACTION_CANCEL:
{
if (mIsDragging) {
smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
}
resetTouch();
return true;
}
}
return handled;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mInitialTouchX = ev.getX(newPointerIndex);
mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
private void resetTouch() {
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
mIsDragging = false;
mOpenOnClick = false;
mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
mVelocityTracker.clear();
}
private void dismiss() {
mRunOnDismissedListener = new RunOnDismissedListener();
post(mRunOnDismissedListener);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
final boolean keepGoing = !mScroller.isFinished();
performDrag(mScroller.getCurrY() - mCollapseOffset);
if (keepGoing) {
postInvalidateOnAnimation();
} else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
dismiss();
}
}
}
private void abortAnimation() {
mScroller.abortAnimation();
mRunOnDismissedListener = null;
mDismissOnScrollerFinished = false;
}
private float performDrag(float dy) {
if (getShowAtTop()) {
return 0;
}
final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mCollapsibleHeight + mUncollapsibleHeight));
if (newPos != mCollapseOffset) {
dy = newPos - mCollapseOffset;
mDragRemainder += dy - (int) dy;
if (mDragRemainder >= 1.0f) {
mDragRemainder -= 1.0f;
dy += 1.0f;
} else if (mDragRemainder <= -1.0f) {
mDragRemainder += 1.0f;
dy -= 1.0f;
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.ignoreOffset) {
child.offsetTopAndBottom((int) dy);
}
}
final boolean isCollapsedOld = mCollapseOffset != 0;
mCollapseOffset = newPos;
mTopOffset += dy;
final boolean isCollapsedNew = newPos != 0;
if (isCollapsedOld != isCollapsedNew) {
onCollapsedChanged(isCollapsedNew);
}
onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
postInvalidateOnAnimation();
return dy;
}
return 0;
}
private void onCollapsedChanged(boolean isCollapsed) {
/*notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);*/
if (mScrollIndicatorDrawable != null) {
setWillNotDraw(!isCollapsed);
}
if (mOnCollapsedChangedListener != null) {
mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
}
}
void dispatchOnDismissed() {
if (mOnDismissedListener != null) {
mOnDismissedListener.onDismissed();
}
if (mRunOnDismissedListener != null) {
removeCallbacks(mRunOnDismissedListener);
mRunOnDismissedListener = null;
}
}
private void smoothScrollTo(int yOffset, float velocity) {
abortAnimation();
final int sy = (int) mCollapseOffset;
int dy = yOffset - sy;
if (dy == 0) {
return;
}
final int height = getHeight();
final int halfHeight = height / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
final float distance = halfHeight + halfHeight * distanceInfluenceForSnapDuration(distanceRatio);
int duration = 0;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageDelta = (float) Math.abs(dy) / height;
duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, 300);
mScroller.startScroll(0, sy, 0, dy, duration);
postInvalidateOnAnimation();
}
private float distanceInfluenceForSnapDuration(float f) {
// center the values about 0.
f -= 0.5f;
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
/**
* Note: this method doesn't take Z into account for overlapping views
* since it is only used in contexts where this doesn't affect the outcome.
*/
private View findChildUnder(float x, float y) {
return findChildUnder(this, x, y);
}
private static View findChildUnder(ViewGroup parent, float x, float y) {
final int childCount = parent.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = parent.getChildAt(i);
if (isChildUnder(child, x, y)) {
return child;
}
}
return null;
}
private View findListChildUnder(float x, float y) {
View v = findChildUnder(x, y);
while (v != null) {
x -= v.getX();
y -= v.getY();
if (v instanceof AbsListView) {
// One more after this.
return findChildUnder((ViewGroup) v, x, y);
}
v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
}
return v;
}
/**
* This only checks clipping along the bottom edge.
*/
private boolean isListChildUnderClipped(float x, float y) {
final View listChild = findListChildUnder(x, y);
return listChild != null && isDescendantClipped(listChild);
}
private boolean isDescendantClipped(View child) {
mTempRect.set(0, 0, child.getWidth(), child.getHeight());
offsetDescendantRectToMyCoords(child, mTempRect);
View directChild;
if (child.getParent() == this) {
directChild = child;
} else {
View v = child;
ViewParent p = child.getParent();
while (p != this) {
v = (View) p;
p = v.getParent();
}
directChild = v;
}
// ResolverDrawerLayout lays out vertically in child order;
// the next view and forward is what to check against.
int clipEdge = getHeight() - getPaddingBottom();
final int childCount = getChildCount();
for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
final View nextChild = getChildAt(i);
if (nextChild.getVisibility() == GONE) {
continue;
}
clipEdge = Math.min(clipEdge, nextChild.getTop());
}
return mTempRect.bottom > clipEdge;
}
private static boolean isChildUnder(View child, float x, float y) {
final float left = child.getX();
final float top = child.getY();
final float right = left + child.getWidth();
final float bottom = top + child.getHeight();
return x >= left && y >= top && x < right && y < bottom;
}
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
if (!isInTouchMode() && isDescendantClipped(focused)) {
smoothScrollTo(0, 0);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
abortAnimation();
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
if (child instanceof AbsListView) {
mNestedScrollingChild = (AbsListView) child;
}
return true;
}
return false;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onStopNestedScroll(View child) {
super.onStopNestedScroll(child);
if (mScroller.isFinished()) {
smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
performDrag(-dyUnconsumed);
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0) {
consumed[1] = (int) -performDrag(-dy);
}
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
smoothScrollTo(0, velocityY);
return true;
}
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
if (getShowAtTop()) {
if (isDismissable() && velocityY > 0) {
abortAnimation();
dismiss();
} else {
smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
}
} else {
if (isDismissable() && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
mDismissOnScrollerFinished = true;
} else {
smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
}
}
return true;
}
return false;
}
@Override
public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
return true;
}
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
smoothScrollTo(0, 0);
return true;
}
return false;
}
@Override
public CharSequence getAccessibilityClreplacedName() {
// Since we support scrolling, make this ViewGroup look like a
// ScrollView. This is kind of a hack until we have support for
// specifying auto-scroll behavior.
return android.widget.ScrollView.clreplaced.getName();
}
/*@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
if (isEnabled()) {
if (mCollapseOffset != 0) {
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.setScrollable(true);
}
}
// This view should never get accessibility focus, but it's interactive
// via nested scrolling, so we can't hide it completely.
info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
}
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
// This view should never get accessibility focus.
return false;
}
if (super.performAccessibilityActionInternal(action, arguments)) {
return true;
}
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
smoothScrollTo(0, 0);
return true;
}
return false;
}*/
@Override
public void onDrawForeground(Canvas canvas) {
if (mScrollIndicatorDrawable != null) {
mScrollIndicatorDrawable.draw(canvas);
}
super.onDrawForeground(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
int widthSize = sourceWidth;
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// Single-use layout; just ignore the mode and use available space.
// Clamp to maxWidth.
if (mMaxWidth >= 0) {
widthSize = Math.min(widthSize, mMaxWidth);
}
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
// Currently we allot more height than is really needed so that the entirety of the
// sheet may be pulled up.
// TODO: Restrict the height here to be the right value.
int heightUsed = 0;
// Measure always-show children first.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.alwaysShow && child.getVisibility() != GONE) {
if (lp.maxHeight != -1) {
final int remainingHeight = heightSize - heightUsed;
measureChildWithMargins(child, widthSpec, 0, MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
} else {
measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
}
heightUsed += child.getMeasuredHeight();
}
}
mAlwaysShowHeight = heightUsed;
// And now the rest.
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.alwaysShow && child.getVisibility() != GONE) {
if (lp.maxHeight != -1) {
final int remainingHeight = heightSize - heightUsed;
measureChildWithMargins(child, widthSpec, 0, MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
} else {
measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
}
heightUsed += child.getMeasuredHeight();
}
}
final int oldCollapsibleHeight = mCollapsibleHeight;
mCollapsibleHeight = Math.max(0, heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
updateCollapseOffset(oldCollapsibleHeight, !isDragging());
if (getShowAtTop()) {
mTopOffset = 0;
} else {
mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
}
setMeasuredDimension(sourceWidth, heightSize);
}
/**
* @return The space reserved by views with 'alwaysShow=true'
*/
public int getAlwaysShowHeight() {
return mAlwaysShowHeight;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getWidth();
View indicatorHost = null;
int ypos = mTopOffset;
int leftEdge = getPaddingLeft();
int rightEdge = width - getPaddingRight();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.hasNestedScrollIndicator) {
indicatorHost = child;
}
if (child.getVisibility() == GONE) {
continue;
}
int top = ypos + lp.topMargin;
if (lp.ignoreOffset) {
top -= mCollapseOffset;
}
final int bottom = top + child.getMeasuredHeight();
final int childWidth = child.getMeasuredWidth();
final int widthAvailable = rightEdge - leftEdge;
final int left = leftEdge + (widthAvailable - childWidth) / 2;
final int right = left + childWidth;
child.layout(left, top, right, bottom);
ypos = bottom + lp.bottomMargin;
}
if (mScrollIndicatorDrawable != null) {
if (indicatorHost != null) {
final int left = indicatorHost.getLeft();
final int right = indicatorHost.getRight();
final int bottom = indicatorHost.getTop();
final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
setWillNotDraw(!isCollapsed());
} else {
mScrollIndicatorDrawable = null;
setWillNotDraw(true);
}
}
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LayoutParams) {
return new LayoutParams((LayoutParams) p);
} else if (p instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) p);
}
return new LayoutParams(p);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected Parcelable onSaveInstanceState() {
final SavedState ss = new SavedState(super.onSaveInstanceState());
ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mOpenOnLayout = ss.open;
}
public static clreplaced LayoutParams extends MarginLayoutParams {
public boolean alwaysShow;
public boolean ignoreOffset;
public boolean hasNestedScrollIndicator;
public int maxHeight;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout_Layout);
alwaysShow = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_alwaysShow, false);
ignoreOffset = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_ignoreOffset, false);
hasNestedScrollIndicator = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_hasNestedScrollIndicator, false);
maxHeight = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_Layout_layout_maxHeight, -1);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(LayoutParams source) {
super(source);
this.alwaysShow = source.alwaysShow;
this.ignoreOffset = source.ignoreOffset;
this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
this.maxHeight = source.maxHeight;
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
static clreplaced SavedState extends BaseSavedState {
boolean open;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
open = in.readInt() != 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(open ? 1 : 0);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/**
* Listener for sheet dismissed events.
*/
public interface OnDismissedListener {
/**
* Callback when the sheet is dismissed by the user.
*/
void onDismissed();
}
/**
* Listener for sheet collapsed / expanded events.
*/
public interface OnCollapsedChangedListener {
/**
* Callback when the sheet is either fully expanded or collapsed.
*
* @param isCollapsed true when collapsed, false when expanded.
*/
void onCollapsedChanged(boolean isCollapsed);
}
private clreplaced RunOnDismissedListener implements Runnable {
@Override
public void run() {
dispatchOnDismissed();
}
}
}
19
Source : SwipeMenuLayout.java
with MIT License
from Omega-R
with MIT License
from Omega-R
public abstract clreplaced SwipeMenuLayout extends FrameLayout {
public static final int DEFAULT_SCROLLER_DURATION = 250;
public static final float DEFAULT_AUTO_OPEN_PERCENT = 0.5f;
protected float mAutoOpenPercent = DEFAULT_AUTO_OPEN_PERCENT;
protected int mScrollerDuration = DEFAULT_SCROLLER_DURATION;
protected int mScaledTouchSlop;
protected int mLastX;
protected int mLastY;
protected int mDownX;
protected int mDownY;
@Nullable
protected View mContentView;
@Nullable
protected Swiper mBeginSwiper;
@Nullable
protected Swiper mEndSwiper;
@Nullable
protected Swiper mCurrentSwiper;
protected boolean shouldResetSwiper;
protected boolean mDragging;
protected boolean swipeEnable = true;
protected OverScroller mScroller;
protected Interpolator mInterpolator;
protected VelocityTracker mVelocityTracker;
protected int mScaledMinimumFlingVelocity;
protected int mScaledMaximumFlingVelocity;
protected SwipeSwitchListener mSwipeSwitchListener;
protected SwipeFractionListener mSwipeFractionListener;
protected NumberFormat mDecimalFormat = new DecimalFormat("#.00", new DecimalFormatSymbols(Locale.US));
public SwipeMenuLayout(Context context) {
this(context, null);
}
public SwipeMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* Not in the place, the swipe menu is swiping
* @return int the place or not
*/
public abstract boolean isNotInPlace();
public void init() {
ViewConfiguration mViewConfig = ViewConfiguration.get(getContext());
mScaledTouchSlop = mViewConfig.getScaledTouchSlop();
mScroller = new OverScroller(getContext(), mInterpolator);
mScaledMinimumFlingVelocity = mViewConfig.getScaledMinimumFlingVelocity();
mScaledMaximumFlingVelocity = mViewConfig.getScaledMaximumFlingVelocity();
}
public void smoothOpenMenu(SwipeDirection direction) {
switch(direction) {
case LEFT:
mCurrentSwiper = mBeginSwiper;
break;
case RIGHT:
mCurrentSwiper = mEndSwiper;
break;
}
if (mCurrentSwiper == null)
throw new IllegalArgumentException("No menu!");
smoothOpenMenu();
}
public void smoothCloseBeginMenu(SwipeDirection direction) {
switch(direction) {
case LEFT:
mCurrentSwiper = mBeginSwiper;
break;
case RIGHT:
mCurrentSwiper = mEndSwiper;
break;
}
if (mCurrentSwiper == null)
throw new IllegalArgumentException("No menu!");
smoothCloseMenu();
}
public abstract void smoothOpenMenu(int duration);
public void smoothOpenMenu() {
smoothOpenMenu(mScrollerDuration);
}
public abstract void smoothCloseMenu(int duration);
public void smoothCloseMenu() {
smoothCloseMenu(mScrollerDuration);
}
public void setSwipeEnable(boolean swipeEnable) {
this.swipeEnable = swipeEnable;
}
public boolean isSwipeEnable() {
return swipeEnable;
}
public abstract void setSwipeEnable(SwipeDirection direction, boolean swipeEnable);
public abstract boolean isSwipeEnable(SwipeDirection direction);
public void setSwipeListener(SwipeSwitchListener swipeSwitchListener) {
mSwipeSwitchListener = swipeSwitchListener;
}
public void setSwipeFractionListener(SwipeFractionListener swipeFractionListener) {
mSwipeFractionListener = swipeFractionListener;
}
abstract int getMoveLen(MotionEvent event);
abstract int getLen();
/**
* compute finish duration
*
* @param ev up event
* @param velocity velocity
* @return finish duration
*/
int getSwipeDuration(MotionEvent ev, int velocity) {
int moveLen = getMoveLen(ev);
final int len = getLen();
final int halfLen = len / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(moveLen) / len);
final float distance = halfLen + halfLen * distanceInfluenceForSnapDuration(distanceRatio);
int duration;
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageDelta = (float) Math.abs(moveLen) / len;
duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, mScrollerDuration);
return duration;
}
float distanceInfluenceForSnapDuration(float f) {
// center the values about 0.
f -= 0.5f;
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (isNotInPlace()) {
smoothCloseMenu(0);
}
}
}
19
Source : Viewport.java
with Apache License 2.0
from niedev
with Apache License 2.0
from niedev
/**
* This is the default implementation for the viewport.
* This implementation so for a normal viewport
* where there is a horizontal x-axis and a
* vertical y-axis.
* This viewport is compatible with
* - {@link com.jjoe64.graphview.series.BarGraphSeries}
* - {@link com.jjoe64.graphview.series.LineGraphSeries}
* - {@link com.jjoe64.graphview.series.PointsGraphSeries}
*
* @author jjoe64
*/
public clreplaced Viewport {
/**
* this reference value is used to generate the
* vertical labels. It is used when the y axis bounds
* is set manual and humanRoundingY=false. it will be the minValueY value.
*/
protected double referenceY = Double.NaN;
/**
* this reference value is used to generate the
* horizontal labels. It is used when the x axis bounds
* is set manual and humanRoundingX=false. it will be the minValueX value.
*/
protected double referenceX = Double.NaN;
/**
* flag whether the vertical scaling is activated
*/
protected boolean scalableY;
/**
* minimal viewport used for scaling and scrolling.
* this is used if the data that is available is
* less then the viewport that we want to be able to display.
*
* Double.NaN to disable this value
*/
private RectD mMinimalViewport = new RectD(Double.NaN, Double.NaN, Double.NaN, Double.NaN);
/**
* the reference number to generate the labels
* @return by default 0, only when manual bounds and no human rounding
* is active, the min x value is returned
*/
protected double getReferenceX() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingX()) {
if (Double.isNaN(referenceX)) {
referenceX = getMinX(false);
}
return referenceX;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
public interface OnXAxisBoundsChangedListener {
/**
* Called after scaling or scrolling with
* the new bounds
* @param minX min x value
* @param maxX max x value
*/
void onXAxisBoundsChanged(double minX, double maxX, Reason reason);
public enum Reason {
SCROLL, SCALE
}
}
/**
* listener for the scale gesture
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
/**
* called by android
* @param detector detector
* @return always true
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
// --- horizontal scaling ---
double viewportWidth = mCurrentViewport.width();
if (mMaxXAxisSize != 0) {
if (viewportWidth > mMaxXAxisSize) {
viewportWidth = mMaxXAxisSize;
}
}
double center = mCurrentViewport.left + viewportWidth / 2;
float scaleSpanX;
if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) {
scaleSpanX = detector.getCurrentSpanX() / detector.getPreviousSpanX();
} else {
scaleSpanX = detector.getScaleFactor();
}
viewportWidth /= scaleSpanX;
mCurrentViewport.left = center - viewportWidth / 2;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
// viewportStart must not be < minX
double minX = getMinX(true);
if (!Double.isNaN(mMinimalViewport.left)) {
minX = Math.min(minX, mMinimalViewport.left);
}
if (mCurrentViewport.left < minX) {
mCurrentViewport.left = minX;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
}
// viewportStart + viewportSize must not be > maxX
double maxX = getMaxX(true);
if (!Double.isNaN(mMinimalViewport.right)) {
maxX = Math.max(maxX, mMinimalViewport.right);
}
if (viewportWidth == 0) {
mCurrentViewport.right = maxX;
}
double overlap = mCurrentViewport.left + viewportWidth - maxX;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.left - overlap > minX) {
mCurrentViewport.left -= overlap;
mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
} else {
// maximal scale
mCurrentViewport.left = minX;
mCurrentViewport.right = maxX;
}
}
// --- vertical scaling ---
if (scalableY && android.os.Build.VERSION.SDK_INT >= 11 && detector.getCurrentSpanY() != 0f && detector.getPreviousSpanY() != 0f) {
boolean hreplacedecondScale = mGraphView.mSecondScale != null;
double viewportHeight = mCurrentViewport.height() * -1;
if (mMaxYAxisSize != 0) {
if (viewportHeight > mMaxYAxisSize) {
viewportHeight = mMaxYAxisSize;
}
}
center = mCurrentViewport.bottom + viewportHeight / 2;
viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
mCurrentViewport.bottom = center - viewportHeight / 2;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
// ignore bounds when second scale
if (!hreplacedecondScale) {
// viewportStart must not be < minY
double minY = getMinY(true);
if (!Double.isNaN(mMinimalViewport.bottom)) {
minY = Math.min(minY, mMinimalViewport.bottom);
}
if (mCurrentViewport.bottom < minY) {
mCurrentViewport.bottom = minY;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
}
// viewportStart + viewportSize must not be > maxY
double maxY = getMaxY(true);
if (!Double.isNaN(mMinimalViewport.top)) {
maxY = Math.max(maxY, mMinimalViewport.top);
}
if (viewportHeight == 0) {
mCurrentViewport.top = maxY;
}
overlap = mCurrentViewport.bottom + viewportHeight - maxY;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.bottom - overlap > minY) {
mCurrentViewport.bottom -= overlap;
mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
} else {
// maximal scale
mCurrentViewport.bottom = minY;
mCurrentViewport.top = maxY;
}
}
} else {
// ---- second scale ---
viewportHeight = mGraphView.mSecondScale.mCurrentViewport.height() * -1;
center = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight / 2;
viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
mGraphView.mSecondScale.mCurrentViewport.bottom = center - viewportHeight / 2;
mGraphView.mSecondScale.mCurrentViewport.top = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight;
}
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
/**
* called when scaling begins
*
* @param detector detector
* @return true if it is scalable
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// cursor mode
if (mGraphView.isCursorMode()) {
return false;
}
if (mIsScalable) {
mScalingActive = true;
return true;
} else {
return false;
}
}
/**
* called when sacling ends
* This will re-adjustSteps the viewport.
*
* @param detector detector
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
mScalingActive = false;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE);
}
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
};
/**
* simple gesture listener to track scroll events
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
// cursor mode
if (mGraphView.isCursorMode()) {
return true;
}
if (!mIsScrollable || mScalingActive)
return false;
// Initiates the decay phase of any active edge effects.
releaseEdgeEffects();
// Aborts any active scroll animations and invalidates.
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// cursor mode
if (mGraphView.isCursorMode()) {
return true;
}
if (!mIsScrollable || mScalingActive)
return false;
// Scrolling uses math based on the viewport (as opposed to math using pixels).
/**
* Pixel offset is the offset in screen pixels, while viewport offset is the
* offset within the current viewport. For additional information on surface sizes
* and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
* additional information about the viewport, see the comments for
* {@link mCurrentViewport}.
*/
double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
// respect minimal viewport
double completeRangeLeft = mCompleteRange.left;
if (!Double.isNaN(mMinimalViewport.left)) {
completeRangeLeft = Math.min(completeRangeLeft, mMinimalViewport.left);
}
double completeRangeRight = mCompleteRange.right;
if (!Double.isNaN(mMinimalViewport.right)) {
completeRangeRight = Math.max(completeRangeRight, mMinimalViewport.right);
}
double completeRangeWidth = completeRangeRight - completeRangeLeft;
double completeRangeBottom = mCompleteRange.bottom;
if (!Double.isNaN(mMinimalViewport.bottom)) {
completeRangeBottom = Math.min(completeRangeBottom, mMinimalViewport.bottom);
}
double completeRangeTop = mCompleteRange.top;
if (!Double.isNaN(mMinimalViewport.top)) {
completeRangeTop = Math.max(completeRangeTop, mMinimalViewport.top);
}
double completeRangeHeight = completeRangeTop - completeRangeBottom;
int completeWidth = (int) ((completeRangeWidth / mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth());
int completeHeight = (int) ((completeRangeHeight / mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight());
int scrolledX = (int) (completeWidth * (mCurrentViewport.left + viewportOffsetX - completeRangeLeft) / completeRangeWidth);
int scrolledY = (int) (completeHeight * (mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom) / completeRangeHeight * -1);
boolean canScrollX = mCurrentViewport.left > completeRangeLeft || mCurrentViewport.right < completeRangeRight;
boolean canScrollY = mCurrentViewport.bottom > completeRangeBottom || mCurrentViewport.top < completeRangeTop;
boolean hreplacedecondScale = mGraphView.mSecondScale != null;
// second scale
double viewportOffsetY2 = 0d;
if (hreplacedecondScale) {
viewportOffsetY2 = distanceY * mGraphView.mSecondScale.mCurrentViewport.height() / mGraphView.getGraphContentHeight();
canScrollY |= mGraphView.mSecondScale.mCurrentViewport.bottom > mGraphView.mSecondScale.mCompleteRange.bottom || mGraphView.mSecondScale.mCurrentViewport.top < mGraphView.mSecondScale.mCompleteRange.top;
}
canScrollY &= scrollableY;
if (canScrollX) {
if (viewportOffsetX < 0) {
double tooMuch = mCurrentViewport.left + viewportOffsetX - completeRangeLeft;
if (tooMuch < 0) {
viewportOffsetX -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.right + viewportOffsetX - completeRangeRight;
if (tooMuch > 0) {
viewportOffsetX -= tooMuch;
}
}
mCurrentViewport.left += viewportOffsetX;
mCurrentViewport.right += viewportOffsetX;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL);
}
}
if (canScrollY) {
// if we have the second axis we ignore the max/min range
if (!hreplacedecondScale) {
if (viewportOffsetY < 0) {
double tooMuch = mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom;
if (tooMuch < 0) {
viewportOffsetY -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.top + viewportOffsetY - completeRangeTop;
if (tooMuch > 0) {
viewportOffsetY -= tooMuch;
}
}
}
mCurrentViewport.top += viewportOffsetY;
mCurrentViewport.bottom += viewportOffsetY;
// second scale
if (hreplacedecondScale) {
mGraphView.mSecondScale.mCurrentViewport.top += viewportOffsetY2;
mGraphView.mSecondScale.mCurrentViewport.bottom += viewportOffsetY2;
}
}
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
}
if (!hreplacedecondScale && canScrollY && scrolledY < 0) {
mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
}
if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth()) / (float) mGraphView.getGraphContentWidth());
}
if (!hreplacedecondScale && canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) {
mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight()) / (float) mGraphView.getGraphContentHeight());
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// fling((int) -velocityX, (int) -velocityY);
return true;
}
};
/**
* the state of the axis bounds
*/
public enum AxisBoundsStatus {
/**
* initial means that the bounds gets
* auto adjusted if they are not manual.
* After adjusting the status comes to
* #AUTO_ADJUSTED.
*/
INITIAL,
/**
* after the bounds got auto-adjusted,
* this status will set.
*/
AUTO_ADJUSTED,
/**
* means that the bounds are fix (manually) and
* are not to be auto-adjusted.
*/
FIX
}
/**
* paint to draw background
*/
private Paint mPaint;
/**
* reference to the graphview
*/
private final GraphView mGraphView;
/**
* this holds the current visible viewport
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCurrentViewport = new RectD();
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxXAxisSize = 0;
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxYAxisSize = 0;
/**
* this holds the whole range of the data
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCompleteRange = new RectD();
/**
* flag whether scaling is currently active
*/
protected boolean mScalingActive;
/**
* flag whether the viewport is scrollable
*/
private boolean mIsScrollable;
/**
* flag whether the viewport is scalable
*/
private boolean mIsScalable;
/**
* flag whether the viewport is scalable
* on the Y axis
*/
private boolean scrollableY;
/**
* gesture detector to detect scrolling
*/
protected GestureDetector mGestureDetector;
/**
* detect scaling
*/
protected ScaleGestureDetector mScaleGestureDetector;
/**
* not used - for fling
*/
protected OverScroller mScroller;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectTop;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectBottom;
/**
* glow effect when scrolling left
*/
private EdgeEffectCompat mEdgeEffectLeft;
/**
* glow effect when scrolling right
*/
private EdgeEffectCompat mEdgeEffectRight;
/**
* state of the x axis
*/
protected AxisBoundsStatus mXAxisBoundsStatus;
/**
* state of the y axis
*/
protected AxisBoundsStatus mYAxisBoundsStatus;
/**
* flag whether the x axis bounds are manual
*/
private boolean mXAxisBoundsManual;
/**
* flag whether the y axis bounds are manual
*/
private boolean mYAxisBoundsManual;
/**
* background color of the viewport area
* it is recommended to use a semi-transparent color
*/
private int mBackgroundColor;
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener;
/**
* optional draw a border between the labels
* and the viewport
*/
private boolean mDrawBorder;
/**
* color of the border
* @see #setDrawBorder(boolean)
*/
private Integer mBorderColor;
/**
* custom paint to use for the border
* @see #setDrawBorder(boolean)
*/
private Paint mBorderPaint;
/**
* creates the viewport
*
* @param graphView graphview
*/
Viewport(GraphView graphView) {
mScroller = new OverScroller(graphView.getContext());
mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
mGraphView = graphView;
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mBackgroundColor = Color.TRANSPARENT;
mPaint = new Paint();
}
/**
* will be called on a touch event.
* needed to use scaling and scrolling
*
* @param event
* @return true if it was consumed
*/
public boolean onTouchEvent(MotionEvent event) {
boolean b = mScaleGestureDetector.onTouchEvent(event);
b |= mGestureDetector.onTouchEvent(event);
if (mGraphView.isCursorMode()) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mGraphView.getCursorMode().onDown(event);
b |= true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
mGraphView.getCursorMode().onMove(event);
b |= true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
b |= mGraphView.getCursorMode().onUp(event);
}
}
return b;
}
/**
* change the state of the x axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setXAxisBoundsStatus(AxisBoundsStatus s) {
mXAxisBoundsStatus = s;
}
/**
* change the state of the y axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setYAxisBoundsStatus(AxisBoundsStatus s) {
mYAxisBoundsStatus = s;
}
/**
* @return whether the viewport is scrollable
*/
public boolean isScrollable() {
return mIsScrollable;
}
/**
* @param mIsScrollable whether is viewport is scrollable
*/
public void setScrollable(boolean mIsScrollable) {
this.mIsScrollable = mIsScrollable;
}
/**
* @return the x axis state
*/
public AxisBoundsStatus getXAxisBoundsStatus() {
return mXAxisBoundsStatus;
}
/**
* @return the y axis state
*/
public AxisBoundsStatus getYAxisBoundsStatus() {
return mYAxisBoundsStatus;
}
/**
* caches the complete range (minX, maxX, minY, maxY)
* by iterating all series and all datapoints and
* stores it into #mCompleteRange
*
* for the x-range it will respect the series on the
* second scale - not for y-values
*/
public void calcCompleteRange() {
List<Series> series = mGraphView.getSeries();
List<Series> seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries());
if (mGraphView.mSecondScale != null) {
seriesInclusiveSecondScale.addAll(mGraphView.mSecondScale.getSeries());
}
mCompleteRange.set(0d, 0d, 0d, 0d);
if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) {
double d = seriesInclusiveSecondScale.get(0).getLowestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d > s.getLowestValueX()) {
d = s.getLowestValueX();
}
}
mCompleteRange.left = d;
d = seriesInclusiveSecondScale.get(0).getHighestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d < s.getHighestValueX()) {
d = s.getHighestValueX();
}
}
mCompleteRange.right = d;
if (!series.isEmpty() && !series.get(0).isEmpty()) {
d = series.get(0).getLowestValueY();
for (Series s : series) {
if (!s.isEmpty() && d > s.getLowestValueY()) {
d = s.getLowestValueY();
}
}
mCompleteRange.bottom = d;
d = series.get(0).getHighestValueY();
for (Series s : series) {
if (!s.isEmpty() && d < s.getHighestValueY()) {
d = s.getHighestValueY();
}
}
mCompleteRange.top = d;
}
}
// calc current viewport bounds
if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.top = mCompleteRange.top;
mCurrentViewport.bottom = mCompleteRange.bottom;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.left = mCompleteRange.left;
mCurrentViewport.right = mCompleteRange.right;
} else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
// get highest/lowest of current viewport
// lowest
double d = Double.MAX_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d > v) {
d = v;
}
}
}
if (d != Double.MAX_VALUE) {
mCurrentViewport.bottom = d;
}
// highest
d = Double.MIN_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d < v) {
d = v;
}
}
}
if (d != Double.MIN_VALUE) {
mCurrentViewport.top = d;
}
}
// fixes blank screen when range is zero
if (mCurrentViewport.left == mCurrentViewport.right)
mCurrentViewport.right++;
if (mCurrentViewport.top == mCurrentViewport.bottom)
mCurrentViewport.top++;
}
/**
* @param completeRange if true => minX of the complete range of all series
* if false => minX of the current visible viewport
* @return the min x value
*/
public double getMinX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.left;
} else {
return mCurrentViewport.left;
}
}
/**
* @param completeRange if true => maxX of the complete range of all series
* if false => maxX of the current visible viewport
* @return the max x value
*/
public double getMaxX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.right;
} else {
return mCurrentViewport.right;
}
}
/**
* @param completeRange if true => minY of the complete range of all series
* if false => minY of the current visible viewport
* @return the min y value
*/
public double getMinY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.bottom;
} else {
return mCurrentViewport.bottom;
}
}
/**
* @param completeRange if true => maxY of the complete range of all series
* if false => maxY of the current visible viewport
* @return the max y value
*/
public double getMaxY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.top;
} else {
return mCurrentViewport.top;
}
}
/**
* set the maximal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y max / highest value
*/
public void setMaxY(double y) {
mCurrentViewport.top = y;
}
/**
* set the minimal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y min / lowest value
*/
public void setMinY(double y) {
mCurrentViewport.bottom = y;
}
/**
* set the maximal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x max / highest value
*/
public void setMaxX(double x) {
mCurrentViewport.right = x;
}
/**
* set the minimal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x min / lowest value
*/
public void setMinX(double x) {
mCurrentViewport.left = x;
}
/**
* release the glowing effects
*/
private void releaseEdgeEffects() {
mEdgeEffectLeft.onRelease();
mEdgeEffectRight.onRelease();
mEdgeEffectTop.onRelease();
mEdgeEffectBottom.onRelease();
}
/**
* not used currently
*
* @param velocityX
* @param velocityY
*/
private void fling(int velocityX, int velocityY) {
velocityY = 0;
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
int maxX = (int) ((mCurrentViewport.width() / mCompleteRange.width()) * (float) mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
int maxY = (int) ((mCurrentViewport.height() / mCompleteRange.height()) * (float) mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
int startX = (int) ((mCurrentViewport.left - mCompleteRange.left) / mCompleteRange.width()) * maxX;
int startY = (int) ((mCurrentViewport.top - mCompleteRange.top) / mCompleteRange.height()) * maxY;
mScroller.forceFinished(true);
mScroller.fling(startX, startY, velocityX, velocityY, 0, maxX, 0, maxY, mGraphView.getGraphContentWidth() / 2, mGraphView.getGraphContentHeight() / 2);
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
/**
* not used currently
*/
public void computeScroll() {
}
/**
* Draws the overscroll "glow" at the four edges of the chart region, if necessary.
*
* @see EdgeEffectCompat
*/
private void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectTop.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectTop.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectBottom.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
canvas.rotate(180, mGraphView.getGraphContentWidth() / 2, 0);
mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectBottom.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
}
/**
* will be first called in order to draw
* the canvas
* Used to draw the background
*
* @param c canvas.
*/
public void drawFirst(Canvas c) {
// draw background
if (mBackgroundColor != Color.TRANSPARENT) {
mPaint.setColor(mBackgroundColor);
c.drawRect(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaint);
}
if (mDrawBorder) {
Paint p;
if (mBorderPaint != null) {
p = mBorderPaint;
} else {
p = mPaint;
p.setColor(getBorderColor());
}
c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
// on the right side if we have second scale
if (mGraphView.mSecondScale != null) {
c.drawLine(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
}
}
}
/**
* draws the glowing edge effect
*
* @param c canvas
*/
public void draw(Canvas c) {
drawEdgeEffectsUnclipped(c);
}
/**
* @return background of the viewport area
*/
public int getBackgroundColor() {
return mBackgroundColor;
}
/**
* @param mBackgroundColor background of the viewport area
* use transparent to have no background
*/
public void setBackgroundColor(int mBackgroundColor) {
this.mBackgroundColor = mBackgroundColor;
}
/**
* @return whether the viewport is scalable
*/
public boolean isScalable() {
return mIsScalable;
}
/**
* active the scaling/zooming feature
* notice: sets the x axis bounds to manual
*
* @param mIsScalable whether the viewport is scalable
*/
public void setScalable(boolean mIsScalable) {
this.mIsScalable = mIsScalable;
if (mIsScalable) {
mIsScrollable = true;
// set viewport to manual
setXAxisBoundsManual(true);
}
}
/**
* @return whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public boolean isXAxisBoundsManual() {
return mXAxisBoundsManual;
}
/**
* @param mXAxisBoundsManual whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
this.mXAxisBoundsManual = mXAxisBoundsManual;
if (mXAxisBoundsManual) {
mXAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* @return whether the y axis bound are manual
*/
public boolean isYAxisBoundsManual() {
return mYAxisBoundsManual;
}
/**
* @param mYAxisBoundsManual whether the y axis bounds are manual
* @see #setMaxY(double)
* @see #setMinY(double)
*/
public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
this.mYAxisBoundsManual = mYAxisBoundsManual;
if (mYAxisBoundsManual) {
mYAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* forces the viewport to scroll to the end
* of the range by keeping the current viewport size.
*
* Important: Only takes effect if x axis bounds are manual.
*
* @see #setXAxisBoundsManual(boolean)
*/
public void scrollToEnd() {
if (mXAxisBoundsManual) {
double size = mCurrentViewport.width();
mCurrentViewport.right = mCompleteRange.right;
mCurrentViewport.left = mCompleteRange.right - size;
mGraphView.onDataChanged(true, false);
} else {
Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
}
}
/**
* @return the listener when there is one registered.
*/
public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() {
return mOnXAxisBoundsChangedListener;
}
/**
* set a listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*
* @param l the listener to use
*/
public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) {
mOnXAxisBoundsChangedListener = l;
}
/**
* optional draw a border between the labels
* and the viewport
*
* @param drawBorder true to draw the border
*/
public void setDrawBorder(boolean drawBorder) {
this.mDrawBorder = drawBorder;
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @see #setDrawBorder(boolean)
* @return border color. by default the grid color is used
*/
public int getBorderColor() {
if (mBorderColor != null) {
return mBorderColor;
}
return mGraphView.getGridLabelRenderer().getGridColor();
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @param borderColor null to reset
*/
public void setBorderColor(Integer borderColor) {
this.mBorderColor = borderColor;
}
/**
* custom paint to use for the border. border color
* will be ignored
*
* @see #setDrawBorder(boolean)
* @param borderPaint
*/
public void setBorderPaint(Paint borderPaint) {
this.mBorderPaint = borderPaint;
}
/**
* activate/deactivate the vertical scrolling
*
* @param scrollableY true to activate
*/
public void setScrollableY(boolean scrollableY) {
this.scrollableY = scrollableY;
}
/**
* the reference number to generate the labels
* @return by default 0, only when manual bounds and no human rounding
* is active, the min y value is returned
*/
protected double getReferenceY() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingY()) {
if (Double.isNaN(referenceY)) {
referenceY = getMinY(false);
}
return referenceY;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* activate or deactivate the vertical zooming/scaling functionallity.
* This will automatically activate the vertical scrolling and the
* horizontal scaling/scrolling feature.
*
* @param scalableY true to activate
*/
public void setScalableY(boolean scalableY) {
if (scalableY) {
this.scrollableY = true;
setScalable(true);
if (android.os.Build.VERSION.SDK_INT < 11) {
Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)");
}
}
this.scalableY = scalableY;
}
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxXAxisSize() {
return mMaxXAxisSize;
}
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxYAxisSize() {
return mMaxYAxisSize;
}
/**
* Set the max viewport size (horizontal)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxXAxisViewportSize maximum size of viewport
*/
public void setMaxXAxisSize(double mMaxXAxisViewportSize) {
this.mMaxXAxisSize = mMaxXAxisViewportSize;
}
/**
* Set the max viewport size (vertical)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxYAxisViewportSize maximum size of viewport
*/
public void setMaxYAxisSize(double mMaxYAxisViewportSize) {
this.mMaxYAxisSize = mMaxYAxisViewportSize;
}
/**
* minimal viewport used for scaling and scrolling.
* this is used if the data that is available is
* less then the viewport that we want to be able to display.
*
* if Double.NaN is used, then this value is ignored
*
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public void setMinimalViewport(double minX, double maxX, double minY, double maxY) {
mMinimalViewport.set(minX, maxY, maxX, minY);
}
}
19
Source : OverScrollerCompat.java
with Apache License 2.0
from niedev
with Apache License 2.0
from niedev
/**
* @see android.view.ScaleGestureDetector#getCurrentSpanY()
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static float getCurrVelocity(OverScroller overScroller) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return overScroller.getCurrVelocity();
} else {
return 0;
}
}
19
Source : LinkageScrollLayout.java
with Apache License 2.0
from MFC-TEC
with Apache License 2.0
from MFC-TEC
/**
* Layout container for two view(TopView and BottomView) that can be scrolled by user,
* allowing the total height of two child to be larger than the physical display.
* LinkageScrollLayout is a {@link ViewGroup}, which place two View in it,
* <P>Children in the container must implement ILinkageScrollHandler{@link ILinkageScrollHandler}, or
* the layout container will not work properly.</P>
*
* @author lorienzhang
*/
public clreplaced LinkageScrollLayout extends ViewGroup {
public static final String TAG = "LinkageScrollLayout";
/**
* multi finger touch
*/
private int mActivePointerId = INVALID_POINTER;
/**
* top view,first child view
*/
private View mTopView;
/**
* bottom view,second child view
*/
private View mBottomView;
/**
* top view,scroll handler
*/
private LinkageScrollHandler mTopHandler;
/**
* bottom view,scroll handler
*/
private LinkageScrollHandler mBottomHandler;
private int mLastScrollY;
private PosIndicator mPosIndicator;
private int mTopViewHeight;
private int mBottomViewHeight;
private int mVisualHeight;
private int mHeight;
/**
* Last MotionEvent
*/
private MotionEvent mLastMotionEvent;
private boolean mHreplacedendCancelEvent;
private OverScroller mScroller;
/**
* to track child view's velocity
*/
private OverScroller mTrackScroller;
private int mTouchSlop;
private VelocityTracker mVelocityTracker;
private int mMaximumVelocity, mMinimumVelocity;
private boolean isControl;
/**
* scroll listener
*/
private LinkageScrollListenerHolder mLinkageScrollListener = LinkageScrollListenerHolder.create();
private LinkageScrollEvent mBottomViewScrollEvent = new LinkageScrollEvent() {
@Override
public void onContentScrollToTop() {
if (!mPosIndicator.isInStartPos()) {
return;
}
if (mTrackScroller.computeScrollOffset()) {
int curVelocity = (int) mTrackScroller.getCurrVelocity();
fling(curVelocity);
mTrackScroller.abortAnimation();
}
}
@Override
public void onContentScrollToBottom() {
}
};
private LinkageScrollEvent mTopViewScrollEvent = new LinkageScrollEvent() {
@Override
public void onContentScrollToTop() {
}
@Override
public void onContentScrollToBottom() {
if (!mPosIndicator.isInEndPos()) {
return;
}
if (mTrackScroller.computeScrollOffset()) {
int curVelocity = (int) mTrackScroller.getCurrVelocity();
fling(-curVelocity);
mTrackScroller.abortAnimation();
}
}
};
public LinkageScrollLayout(Context context) {
this(context, null);
}
public LinkageScrollLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LinkageScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mPosIndicator = new PosIndicator();
mScroller = new OverScroller(context);
mTrackScroller = new OverScroller(context);
mVelocityTracker = VelocityTracker.obtain();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mPosIndicator.setTouchSlop(mTouchSlop);
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final int childCount = getChildCount();
if (childCount != 2) {
throw new RuntimeException("child count in LinkageScrollLayout must no more 2");
}
mTopView = getChildAt(0);
mBottomView = getChildAt(1);
if (!(mBottomView instanceof ILinkageScrollHandler) || !(mTopView instanceof ILinkageScrollHandler)) {
throw new RuntimeException("child in LinkageScrollLayout must implement IContentHandler");
}
mBottomHandler = ((ILinkageScrollHandler) mBottomView).provideScrollHandler();
mTopHandler = ((ILinkageScrollHandler) mTopView).provideScrollHandler();
if (mTopHandler == null || mBottomHandler == null) {
throw new RuntimeException("LinkageScrollHandler provided by child must not be null");
}
((ILinkageScrollHandler) mBottomView).setOnContentViewScrollEvent(mBottomViewScrollEvent);
((ILinkageScrollHandler) mTopView).setOnContentViewScrollEvent(mTopViewScrollEvent);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTopView != null) {
mTopView.measure(widthMeasureSpec, heightMeasureSpec);
mTopViewHeight = mTopView.getMeasuredHeight();
}
if (mBottomView != null) {
mBottomView.measure(widthMeasureSpec, heightMeasureSpec);
mBottomViewHeight = mBottomView.getMeasuredHeight();
}
// height of visual height
mVisualHeight = getMeasuredHeight();
mHeight = mTopViewHeight + mBottomViewHeight;
int widthSize = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
setMeasuredDimension(widthSize, mHeight);
Log.d(TAG, "#onMeasure# topHeight: " + mTopViewHeight + ", bottomHeight: " + mBottomViewHeight + ", layoutHeight: " + mHeight);
// init position indicator
mPosIndicator.initStartAndEndPos(0, mVisualHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// set current position
mPosIndicator.setCurrentPos(mTopViewHeight);
Log.d(TAG, "#onSizeChanged# current position: " + mTopViewHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int curPos = mPosIndicator.getCurrentPos();
int left = l;
int top = curPos - mTopViewHeight;
int right = r;
int bottom = curPos;
if (mTopView != null) {
mTopView.layout(left, top, right, bottom);
Log.d(TAG, "#onLayout# layout top: top: " + top + ", bottom: " + bottom);
}
top = curPos;
bottom = top + mBottomViewHeight;
if (mBottomView != null) {
mBottomView.layout(left, top, right, bottom);
Log.d(TAG, "#onLayout# layout bottom: top: " + top + ", bottom: " + bottom);
}
}
}
/**
* motion event preplaced to children
*
* @param event
* @return
*/
private boolean dispatchTouchEventSupper(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
int curScrollY = mScroller.getCurrY();
int offsetY = curScrollY - mLastScrollY;
mLastScrollY = curScrollY;
if (offsetY != 0) {
moveChildrenToNewPos(offsetY);
int velocity = (int) mScroller.getCurrVelocity();
if (mPosIndicator.isInStartPos()) {
// bad case:不同机型表现效果不一样。
// oneplus效果OK, 华为mate 9 webview的velocity不是很匹配
// 这里需要优化,包括判断条件
if (mBottomView instanceof WebView) {
velocity /= 2;
}
mBottomHandler.flingContentVertically(mBottomView, velocity);
mScroller.abortAnimation();
}
if (mPosIndicator.isInEndPos()) {
// bad case:不同机型表现效果不一样。
// oneplus的效果OK, 华为mate9webview的velocity不是很匹配
if (mTopView instanceof WebView) {
velocity /= 2;
}
mTopHandler.flingContentVertically(mTopView, -velocity);
mScroller.abortAnimation();
}
}
invalidate();
}
}
/**
* fling children
*
* @param velocityY
*/
private void fling(int velocityY) {
// Log.d(TAG, "#fling# velocity]Y: " + velocityY);
mScroller.fling(0, 0, 0, velocityY, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
mLastScrollY = 0;
invalidate();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
int actionIndex = ev.getActionIndex();
mVelocityTracker.addMovement(ev);
switch(ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(actionIndex);
mPosIndicator.onDown(x, y);
mHreplacedendCancelEvent = false;
mVelocityTracker.clear();
mVelocityTracker.addMovement(ev);
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
if (!mTrackScroller.isFinished()) {
mTrackScroller.abortAnimation();
}
// down 停止content view scroll
mTopHandler.stopContentScroll(mTopView);
mBottomHandler.stopContentScroll(mBottomView);
return dispatchTouchEventSupper(ev);
case MotionEvent.ACTION_POINTER_DOWN:
mActivePointerId = ev.getPointerId(actionIndex);
x = ev.getX(actionIndex);
y = ev.getY(actionIndex);
mPosIndicator.onPointerDown(x, y);
return dispatchTouchEventSupper(ev);
case MotionEvent.ACTION_POINTER_UP:
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
x = (int) ev.getX(newPointerIndex);
y = (int) ev.getY(newPointerIndex);
mPosIndicator.onPointerUp(x, y);
}
return dispatchTouchEventSupper(ev);
case MotionEvent.ACTION_MOVE:
mLastMotionEvent = ev;
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex < 0) {
return false;
}
x = ev.getX(activePointerIndex);
y = ev.getY(activePointerIndex);
mPosIndicator.onMove(x, y);
if (mPosIndicator.isDragging()) {
if (mPosIndicator.isMoveUp()) {
// move up
if (mPosIndicator.isInStartPos()) {
if (isControl) {
mBottomHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
} else {
return dispatchTouchEventSupper(ev);
}
} else if (mPosIndicator.isInEndPos()) {
if (mTopView.canScrollVertically(1)) {
if (isControl) {
mTopHandler.scrollContentBy(mTopView, (int) -mPosIndicator.getOffsetY());
} else {
return dispatchTouchEventSupper(ev);
}
} else {
moveChildrenToNewPos(mPosIndicator.getOffsetY());
isControl = true;
}
} else {
moveChildrenToNewPos(mPosIndicator.getOffsetY());
isControl = true;
}
} else {
// move down
if (mPosIndicator.isInStartPos()) {
if (mBottomView.canScrollVertically(-1)) {
if (isControl) {
mBottomHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
} else {
return dispatchTouchEventSupper(ev);
}
} else {
moveChildrenToNewPos(mPosIndicator.getOffsetY());
isControl = true;
}
} else if (mPosIndicator.isInEndPos()) {
if (isControl) {
mTopHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
} else {
return dispatchTouchEventSupper(ev);
}
} else {
moveChildrenToNewPos(mPosIndicator.getOffsetY());
isControl = true;
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mPosIndicator.onRelease(x, y);
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) mVelocityTracker.getYVelocity();
if (isControl) {
isControl = false;
if (Math.abs(velocityY) > mMinimumVelocity) {
if (mPosIndicator.isInStartPos()) {
mBottomHandler.flingContentVertically(mBottomView, -velocityY);
} else {
fling(velocityY);
}
}
} else {
if (Math.abs(velocityY) > mMinimumVelocity) {
// 跟踪子view velocity
trackChildVelocity(velocityY);
}
return dispatchTouchEventSupper(ev);
}
break;
}
return true;
}
/**
* 跟踪子view的velocity
*/
private void trackChildVelocity(int velocityY) {
mTrackScroller.fling(0, 0, 0, Math.abs(velocityY), 0, 0, 0, Integer.MAX_VALUE);
}
/**
* send cancel event to children
*/
private void sendCancelEvent() {
Log.d(TAG, "#sendCancelEvent#");
MotionEvent event = mLastMotionEvent;
MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, event.getX(), event.getY(), event.getMetaState());
dispatchTouchEventSupper(e);
}
/**
* 将子view移到新的position
*
* @param offsetY
*/
private void moveChildrenToNewPos(float offsetY) {
// send cancel event to children
if (!mHreplacedendCancelEvent && mPosIndicator.isUnderTouch() && mPosIndicator.hasMovedAfterPressedDown()) {
mHreplacedendCancelEvent = true;
sendCancelEvent();
}
if (Math.abs(offsetY) <= 0) {
return;
}
int toPos = mPosIndicator.getCurrentPos() + (int) offsetY;
// 边界检查:startPos <= toPos <= endPos
toPos = mPosIndicator.checkPosBoundary(toPos);
mPosIndicator.setCurrentPos(toPos);
int distanceY = toPos - mPosIndicator.getLastPos();
offsetChildren(distanceY);
if (mPosIndicator.hasJustLeftEndPos()) {
if (mLinkageScrollListener.hasHandler()) {
mLinkageScrollListener.onBottomJustIn(mPosIndicator);
}
}
if (mPosIndicator.hasJustLeftStartPos()) {
if (mLinkageScrollListener.hasHandler()) {
mLinkageScrollListener.onTopJustIn(mPosIndicator);
}
}
if (mLinkageScrollListener.hasHandler()) {
mLinkageScrollListener.onPositionChanged(mPosIndicator);
}
if (mPosIndicator.hasJustBackStartPos()) {
if (mLinkageScrollListener.hasHandler()) {
mLinkageScrollListener.onTopJustOut(mPosIndicator);
}
}
if (mPosIndicator.hasJustBackEndPos()) {
if (mLinkageScrollListener.hasHandler()) {
mLinkageScrollListener.onBottomJustOut(mPosIndicator);
}
}
}
/**
* move容器中子view的位置
*
* @param deltaY
*/
private void offsetChildren(int deltaY) {
mTopView.offsetTopAndBottom(deltaY);
mBottomView.offsetTopAndBottom(deltaY);
}
/**
* 注册容器位置信息更新的接口
*/
public void addLinkageScrollListener(LinkageScrollListener handler) {
mLinkageScrollListener.addHandler(mLinkageScrollListener, handler);
}
}
19
Source : HeaderBehavior.java
with Apache License 2.0
from material-components
with Apache License 2.0
from material-components
/**
* The {@link Behavior} for a view that sits vertically above scrolling a view. See {@link
* HeaderScrollingViewBehavior}.
*/
abstract clreplaced HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {
private static final int INVALID_POINTER = -1;
@Nullable
private Runnable flingRunnable;
OverScroller scroller;
private boolean isBeingDragged;
private int activePointerId = INVALID_POINTER;
private int lastMotionY;
private int touchSlop = -1;
@Nullable
private VelocityTracker velocityTracker;
public HeaderBehavior() {
}
public HeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
if (touchSlop < 0) {
touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
// Shortcut since we're being dragged
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && isBeingDragged) {
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
return false;
}
int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
return false;
}
int y = (int) ev.getY(pointerIndex);
int yDiff = Math.abs(y - lastMotionY);
if (yDiff > touchSlop) {
lastMotionY = y;
return true;
}
}
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
activePointerId = INVALID_POINTER;
int x = (int) ev.getX();
int y = (int) ev.getY();
isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
if (isBeingDragged) {
lastMotionY = y;
activePointerId = ev.getPointerId(0);
ensureVelocityTracker();
// There is an animation in progress. Stop it and catch the view.
if (scroller != null && !scroller.isFinished()) {
scroller.abortAnimation();
return true;
}
}
}
if (velocityTracker != null) {
velocityTracker.addMovement(ev);
}
return false;
}
@Override
public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
boolean consumeUp = false;
switch(ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(activePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) ev.getY(activePointerIndex);
int dy = lastMotionY - y;
lastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
break;
case MotionEvent.ACTION_POINTER_UP:
int newIndex = ev.getActionIndex() == 0 ? 1 : 0;
activePointerId = ev.getPointerId(newIndex);
lastMotionY = (int) (ev.getY(newIndex) + 0.5f);
break;
case MotionEvent.ACTION_UP:
if (velocityTracker != null) {
consumeUp = true;
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float yvel = velocityTracker.getYVelocity(activePointerId);
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
// $FALLTHROUGH
case MotionEvent.ACTION_CANCEL:
isBeingDragged = false;
activePointerId = INVALID_POINTER;
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
if (velocityTracker != null) {
velocityTracker.addMovement(ev);
}
return isBeingDragged || consumeUp;
}
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
return setHeaderTopBottomOffset(parent, header, newOffset, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset, int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset();
}
final int scroll(CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header, getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
final boolean fling(CoordinatorLayout coordinatorLayout, @NonNull V layout, int minOffset, int maxOffset, float velocityY) {
if (flingRunnable != null) {
layout.removeCallbacks(flingRunnable);
flingRunnable = null;
}
if (scroller == null) {
scroller = new OverScroller(layout.getContext());
}
scroller.fling(0, // curr
getTopAndBottomOffset(), 0, // velocity.
Math.round(velocityY), 0, // x
0, minOffset, // y
maxOffset);
if (scroller.computeScrollOffset()) {
flingRunnable = new FlingRunnable(coordinatorLayout, layout);
ViewCompat.postOnAnimation(layout, flingRunnable);
return true;
} else {
onFlingFinished(coordinatorLayout, layout);
return false;
}
}
/**
* Called when a fling has finished, or the fling was initiated but there wasn't enough velocity
* to start it.
*/
void onFlingFinished(CoordinatorLayout parent, V layout) {
// no-op
}
/**
* Return true if the view can be dragged.
*/
boolean canDragView(V view) {
return false;
}
/**
* Returns the maximum px offset when {@code view} is being dragged.
*/
int getMaxDragOffset(@NonNull V view) {
return -view.getHeight();
}
int getScrollRangeForDragFling(@NonNull V view) {
return view.getHeight();
}
private void ensureVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
}
private clreplaced FlingRunnable implements Runnable {
private final CoordinatorLayout parent;
private final V layout;
FlingRunnable(CoordinatorLayout parent, V layout) {
this.parent = parent;
this.layout = layout;
}
@Override
public void run() {
if (layout != null && scroller != null) {
if (scroller.computeScrollOffset()) {
setHeaderTopBottomOffset(parent, layout, scroller.getCurrY());
// Post ourselves so that we run on the next animation
ViewCompat.postOnAnimation(layout, this);
} else {
onFlingFinished(parent, layout);
}
}
}
}
}
19
Source : SystemGesturesPointerEventListener.java
with Apache License 2.0
from lulululbj
with Apache License 2.0
from lulululbj
/*
* Listens for system-wide input gestures, firing callbacks when detected.
* @hide
*/
public clreplaced SystemGesturesPointerEventListener implements PointerEventListener {
private static final String TAG = "SystemGestures";
private static final boolean DEBUG = false;
private static final long SWIPE_TIMEOUT_MS = 500;
// max per input system
private static final int MAX_TRACKED_POINTERS = 32;
private static final int UNTRACKED_POINTER = -1;
private static final int MAX_FLING_TIME_MILLIS = 5000;
private static final int SWIPE_NONE = 0;
private static final int SWIPE_FROM_TOP = 1;
private static final int SWIPE_FROM_BOTTOM = 2;
private static final int SWIPE_FROM_RIGHT = 3;
private static final int SWIPE_FROM_LEFT = 4;
private final Context mContext;
private final int mSwipeStartThreshold;
private final int mSwipeDistanceThreshold;
private final Callbacks mCallbacks;
private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];
private final float[] mDownX = new float[MAX_TRACKED_POINTERS];
private final float[] mDownY = new float[MAX_TRACKED_POINTERS];
private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];
private GestureDetector mGestureDetector;
private OverScroller mOverscroller;
int screenHeight;
int screenWidth;
private int mDownPointers;
private boolean mSwipeFireable;
private boolean mDebugFireable;
private boolean mMouseHoveringAtEdge;
private long mLastFlingTime;
public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
mContext = context;
mCallbacks = checkNull("callbacks", callbacks);
mSwipeStartThreshold = checkNull("context", context).getResources().getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
mSwipeDistanceThreshold = mSwipeStartThreshold;
if (DEBUG)
Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
}
private static <T> T checkNull(String name, T arg) {
if (arg == null) {
throw new IllegalArgumentException(name + " must not be null");
}
return arg;
}
public void systemReady() {
Handler h = new Handler(Looper.myLooper());
mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
mOverscroller = new OverScroller(mContext);
}
@Override
public void onPointerEvent(MotionEvent event) {
if (mGestureDetector != null && event.isTouchEvent()) {
mGestureDetector.onTouchEvent(event);
}
switch(event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mSwipeFireable = true;
mDebugFireable = true;
mDownPointers = 0;
captureDown(event, 0);
if (mMouseHoveringAtEdge) {
mMouseHoveringAtEdge = false;
mCallbacks.onMouseLeaveFromEdge();
}
mCallbacks.onDown();
break;
case MotionEvent.ACTION_POINTER_DOWN:
captureDown(event, event.getActionIndex());
if (mDebugFireable) {
mDebugFireable = event.getPointerCount() < 5;
if (!mDebugFireable) {
if (DEBUG)
Slog.d(TAG, "Firing debug");
mCallbacks.onDebug();
}
}
break;
case MotionEvent.ACTION_MOVE:
if (mSwipeFireable) {
final int swipe = detectSwipe(event);
mSwipeFireable = swipe == SWIPE_NONE;
if (swipe == SWIPE_FROM_TOP) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromTop");
mCallbacks.onSwipeFromTop();
} else if (swipe == SWIPE_FROM_BOTTOM) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromBottom");
mCallbacks.onSwipeFromBottom();
} else if (swipe == SWIPE_FROM_RIGHT) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromRight");
mCallbacks.onSwipeFromRight();
} else if (swipe == SWIPE_FROM_LEFT) {
if (DEBUG)
Slog.d(TAG, "Firing onSwipeFromLeft");
mCallbacks.onSwipeFromLeft();
}
}
break;
case MotionEvent.ACTION_HOVER_MOVE:
if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!mMouseHoveringAtEdge && event.getY() == 0) {
mCallbacks.onMouseHoverAtTop();
mMouseHoveringAtEdge = true;
} else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
mCallbacks.onMouseHoverAtBottom();
mMouseHoveringAtEdge = true;
} else if (mMouseHoveringAtEdge && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
mCallbacks.onMouseLeaveFromEdge();
mMouseHoveringAtEdge = false;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mSwipeFireable = false;
mDebugFireable = false;
mCallbacks.onUpOrCancel();
break;
default:
if (DEBUG)
Slog.d(TAG, "Ignoring " + event);
}
}
private void captureDown(MotionEvent event, int pointerIndex) {
final int pointerId = event.getPointerId(pointerIndex);
final int i = findIndex(pointerId);
if (DEBUG)
Slog.d(TAG, "pointer " + pointerId + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
if (i != UNTRACKED_POINTER) {
mDownX[i] = event.getX(pointerIndex);
mDownY[i] = event.getY(pointerIndex);
mDownTime[i] = event.getEventTime();
if (DEBUG)
Slog.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]);
}
}
private int findIndex(int pointerId) {
for (int i = 0; i < mDownPointers; i++) {
if (mDownPointerId[i] == pointerId) {
return i;
}
}
if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
return UNTRACKED_POINTER;
}
mDownPointerId[mDownPointers++] = pointerId;
return mDownPointers - 1;
}
private int detectSwipe(MotionEvent move) {
final int historySize = move.getHistorySize();
final int pointerCount = move.getPointerCount();
for (int p = 0; p < pointerCount; p++) {
final int pointerId = move.getPointerId(p);
final int i = findIndex(pointerId);
if (i != UNTRACKED_POINTER) {
for (int h = 0; h < historySize; h++) {
final long time = move.getHistoricalEventTime(h);
final float x = move.getHistoricalX(p, h);
final float y = move.getHistoricalY(p, h);
final int swipe = detectSwipe(i, time, x, y);
if (swipe != SWIPE_NONE) {
return swipe;
}
}
final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
if (swipe != SWIPE_NONE) {
return swipe;
}
}
}
return SWIPE_NONE;
}
private int detectSwipe(int i, long time, float x, float y) {
final float fromX = mDownX[i];
final float fromY = mDownY[i];
final long elapsed = time - mDownTime[i];
if (DEBUG)
Slog.d(TAG, "pointer " + mDownPointerId[i] + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
if (fromY <= mSwipeStartThreshold && y > fromY + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_TOP;
}
if (fromY >= screenHeight - mSwipeStartThreshold && y < fromY - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_BOTTOM;
}
if (fromX >= screenWidth - mSwipeStartThreshold && x < fromX - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_RIGHT;
}
if (fromX <= mSwipeStartThreshold && x > fromX + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_LEFT;
}
return SWIPE_NONE;
}
private final clreplaced FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (!mOverscroller.isFinished()) {
mOverscroller.forceFinished(true);
}
return true;
}
@Override
public boolean onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY) {
mOverscroller.computeScrollOffset();
long now = SystemClock.uptimeMillis();
if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
mOverscroller.forceFinished(true);
}
mOverscroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
int duration = mOverscroller.getDuration();
if (duration > MAX_FLING_TIME_MILLIS) {
duration = MAX_FLING_TIME_MILLIS;
}
mLastFlingTime = now;
mCallbacks.onFling(duration);
return true;
}
}
interface Callbacks {
void onSwipeFromTop();
void onSwipeFromBottom();
void onSwipeFromRight();
void onSwipeFromLeft();
void onFling(int durationMs);
void onDown();
void onUpOrCancel();
void onMouseHoverAtTop();
void onMouseHoverAtBottom();
void onMouseLeaveFromEdge();
void onDebug();
}
}
19
Source : SwipeHelper.java
with Apache License 2.0
from luckybilly
with Apache License 2.0
from luckybilly
/**
* This clreplaced is copy and modified from ViewDragHelper
* 1. mCapturedView removed. use mClampedDistanceX and mClampedDistanceY instead
* 2. Callback removed. use {@link SwipeConsumer} to consume the motion event
* @author billy.qi
*/
public clreplaced SwipeHelper {
private static final String TAG = "SwipeHelper";
/**
* A null/invalid pointer ID.
*/
public static final int INVALID_POINTER = -1;
public static final int POINTER_NESTED_SCROLL = -2;
public static final int POINTER_NESTED_FLY = -3;
/**
* A view is not currently being dragged or animating as a result of a fling/snap.
*/
public static final int STATE_IDLE = 0;
/**
* A view is currently being dragged. The position is currently changing as a result
* of user input or simulated user input.
*/
public static final int STATE_DRAGGING = 1;
/**
* A view is currently settling into place as a result of a fling or
* predefined non-interactive motion.
*/
public static final int STATE_SETTLING = 2;
public static final int STATE_NONE_TOUCH = 3;
private final ViewConfiguration viewConfiguration;
// ms
private int maxSettleDuration = 600;
// Current drag state; idle, dragging or settling
private int mDragState;
// Distance to travel before a drag may begin
private int mTouchSlop;
// Last known position/pointer tracking
private int mActivePointerId = INVALID_POINTER;
private float[] mInitialMotionX;
private float[] mInitialMotionY;
private float[] mLastMotionX;
private float[] mLastMotionY;
private int mPointersDown;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private float mMinVelocity;
private OverScroller mScroller;
private final SwipeConsumer mSwipeConsumer;
private boolean mReleaseInProgress;
private final ViewGroup mParentView;
private int mClampedDistanceX;
private int mClampedDistanceY;
/**
* Default interpolator defining the animation curve for mScroller
*/
private static final Interpolator sInterpolator = new Interpolator() {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
/**
* Factory method to create a new SwipeHelper.
*
* @param forParent Parent view to monitor
* @param consumer Callback to provide information and receive events
* @param interpolator interpolator for animation
* @return a new SwipeHelper instance
*/
public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer, Interpolator interpolator) {
return new SwipeHelper(forParent.getContext(), forParent, consumer, interpolator);
}
public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer) {
return create(forParent, consumer, null);
}
/**
* Factory method to create a new SwipeHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param consumer Callback to provide information and receive events
* @param interpolator interpolator for animation
* @return a new SwipeHelper instance
*/
public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer consumer, Interpolator interpolator) {
final SwipeHelper helper = create(forParent, consumer, interpolator);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer cb) {
return create(forParent, sensitivity, cb, null);
}
public void setSensitivity(float sensitivity) {
mTouchSlop = (int) (viewConfiguration.getScaledTouchSlop() * (1 / sensitivity));
}
/**
* Apps should use SwipeHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
* @param interpolator interpolator for animation
*/
private SwipeHelper(Context context, ViewGroup forParent, SwipeConsumer cb, Interpolator interpolator) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mSwipeConsumer = cb;
viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledTouchSlop();
mMaxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
mMinVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
setInterpolator(context, interpolator);
}
public void setInterpolator(Context context, Interpolator interpolator) {
if (interpolator == null) {
interpolator = sInterpolator;
}
if (mScroller != null) {
abort();
mScroller = null;
}
mScroller = new OverScroller(context, interpolator);
}
/**
* Set the minimum velocity that will be detected as having a magnitude greater than zero
* in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
*
* @param minVel Minimum velocity to detect
* @return this
*/
public SwipeHelper setMinVelocity(float minVel) {
mMinVelocity = minVel;
return this;
}
/**
* Return the currently configured minimum velocity. Any flings with a magnitude less
* than this value in pixels per second. Callback methods accepting a velocity will receive
* zero as a velocity value if the real detected velocity was below this threshold.
*
* @return the minimum velocity that will be detected
*/
public float getMinVelocity() {
return mMinVelocity;
}
/**
* Retrieve the current drag state of this helper. This will return one of
* {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING} or {@link #STATE_NONE_TOUCH}.
* @return The current drag state
*/
public int getDragState() {
return mDragState;
}
/**
* @return The ID of the pointer currently dragging
* or {@link #INVALID_POINTER}.
*/
public int getActivePointerId() {
return mActivePointerId;
}
/**
* @return The minimum distance in pixels that the user must travel to initiate a drag
*/
public int getTouchSlop() {
return mTouchSlop;
}
/**
* The result of a call to this method is equivalent to
* {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
*/
public void cancel() {
mActivePointerId = INVALID_POINTER;
clearMotionHistory();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* {@link #cancel()}, but also abort all motion in progress and snap to the end of any
* animation.
*/
public void abort() {
cancel();
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
final int oldX = mScroller.getCurrX();
final int oldY = mScroller.getCurrY();
mScroller.abortAnimation();
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
mSwipeConsumer.onSwipeDistanceChanged(newX, newY, newX - oldX, newY - oldY);
}
setDragState(STATE_IDLE);
}
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling()}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param startX start x position
* @param startY start y position
* @param finalX Final x position
* @param finalY Final y position
* @return true if animation should continue through {@link #continueSettling()} calls
*/
public boolean smoothSlideTo(int startX, int startY, int finalX, int finalY) {
mClampedDistanceX = startX;
mClampedDistanceY = startY;
return smoothSlideTo(finalX, finalY);
}
public boolean smoothSlideTo(int finalX, int finalY) {
boolean continueSliding;
if (mVelocityTracker != null) {
continueSliding = smoothSettleCapturedViewTo(finalX, finalY, (int) mVelocityTracker.getXVelocity(mActivePointerId), (int) mVelocityTracker.getYVelocity(mActivePointerId));
} else {
continueSliding = smoothSettleCapturedViewTo(finalX, finalY, 0, 0);
}
mActivePointerId = INVALID_POINTER;
return continueSliding;
}
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling()}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalX Settled left edge position for the captured view
* @param finalY Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling()} calls
*/
public boolean settleCapturedViewAt(int finalX, int finalY) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased");
}
return smoothSettleCapturedViewTo(finalX, finalY, (int) mVelocityTracker.getXVelocity(mActivePointerId), (int) mVelocityTracker.getYVelocity(mActivePointerId));
}
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalX Target left position for the captured view
* @param finalY Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling()} calls
*/
private boolean smoothSettleCapturedViewTo(int finalX, int finalY, int xvel, int yvel) {
final int startX = mClampedDistanceX;
final int startTop = mClampedDistanceY;
final int dx = finalX - startX;
final int dy = finalY - startTop;
mScroller.abortAnimation();
if (dx == 0 && dy == 0) {
setDragState(STATE_SETTLING);
mSwipeConsumer.onSwipeDistanceChanged(finalX, finalY, dx, dy);
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(dx, dy, xvel, yvel);
mScroller.startScroll(startX, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
private int computeSettleDuration(int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;
final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy / addedDistance;
int xduration = computeAxisDuration(dx, xvel, mSwipeConsumer.getHorizontalRange(dx, dy));
int yduration = computeAxisDuration(dy, yvel, mSwipeConsumer.getVerticalRange(dx, dy));
return (int) (xduration * xweight + yduration * yweight);
}
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = mParentView.getWidth();
final int halfWidth = width >> 1;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) (range * maxSettleDuration);
}
return Math.min(duration, maxSettleDuration);
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private int clampMag(int value, int absMin, int absMax) {
final int absValue = Math.abs(value);
if (absValue < absMin) {
return 0;
}
if (absValue > absMax) {
return value > 0 ? absMax : -absMax;
}
return value;
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private float clampMag(float value, float absMin, float absMax) {
final float absValue = Math.abs(value);
if (absValue < absMin) {
return 0;
}
if (absValue > absMax) {
return value > 0 ? absMax : -absMax;
}
return value;
}
private float distanceInfluenceForSnapDuration(float f) {
// center the values about 0.
f -= 0.5f;
f *= 0.3f * (float) Math.PI / 2.0f;
return (float) Math.sin(f);
}
public boolean continueSettling() {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mClampedDistanceX;
final int dy = y - mClampedDistanceY;
if (dx != 0) {
mClampedDistanceX = x;
}
if (dy != 0) {
mClampedDistanceY = y;
}
if (dx != 0 || dy != 0) {
mSwipeConsumer.onSwipeDistanceChanged(x, y, dx, dy);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
if (!keepGoing) {
setDragState(STATE_IDLE);
}
}
return mDragState == STATE_SETTLING;
}
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* @param xvel x velocity
* @param yvel y velocity
*/
public void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mSwipeConsumer.onSwipeReleased(xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
private void clearMotionHistory() {
if (mInitialMotionX == null) {
return;
}
Arrays.fill(mInitialMotionX, 0);
Arrays.fill(mInitialMotionY, 0);
Arrays.fill(mLastMotionX, 0);
Arrays.fill(mLastMotionY, 0);
mPointersDown = 0;
}
private void clearMotionHistory(int pointerId) {
if (mInitialMotionX == null || !isPointerDown(pointerId)) {
return;
}
mInitialMotionX[pointerId] = 0;
mInitialMotionY[pointerId] = 0;
mLastMotionX[pointerId] = 0;
mLastMotionY[pointerId] = 0;
mPointersDown &= ~(1 << pointerId);
}
private void ensureMotionHistorySizeForId(int pointerId) {
if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
float[] imx = new float[pointerId + 1];
float[] imy = new float[pointerId + 1];
float[] lmx = new float[pointerId + 1];
float[] lmy = new float[pointerId + 1];
if (mInitialMotionX != null) {
System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
}
mInitialMotionX = imx;
mInitialMotionY = imy;
mLastMotionX = lmx;
mLastMotionY = lmy;
}
}
private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
mPointersDown |= 1 << pointerId;
}
private void saveLastMotion(MotionEvent ev) {
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip saving on ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
mLastMotionX[pointerId] = x;
mLastMotionY[pointerId] = y;
}
}
/**
* Check if the given pointer ID represents a pointer that is currently down (to the best
* of the SwipeHelper's knowledge).
*
* <p>The state used to report this information is populated by the methods
* {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
* {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
* been called for all relevant MotionEvents to track, the information reported
* by this method may be stale or incorrect.</p>
*
* @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
* @return true if the pointer with the given ID is still down
*/
public boolean isPointerDown(int pointerId) {
return (mPointersDown & 1 << pointerId) != 0;
}
void setDragState(int state) {
if (mDragState != state) {
mDragState = state;
mSwipeConsumer.onStateChanged(state);
// if (mDragState == STATE_IDLE) {
// mClampedDistanceX = mClampedDistanceY = 0;
// }
}
}
/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we've already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/
private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy) {
return trySwipe(pointerId, settling, downX, downY, dx, dy, true);
}
private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy, boolean touchMode) {
if (mActivePointerId == pointerId) {
// Already done!
return true;
}
boolean swipe;
if (settling || mDragState == STATE_SETTLING) {
swipe = mSwipeConsumer.tryAcceptSettling(pointerId, downX, downY);
} else {
swipe = mSwipeConsumer.tryAcceptMoving(pointerId, downX, downY, dx, dy);
}
if (swipe) {
mActivePointerId = pointerId;
float initX = 0;
float initY = 0;
if (pointerId >= 0 && pointerId < mInitialMotionX.length && pointerId < mInitialMotionY.length) {
initX = mInitialMotionX[pointerId];
initY = mInitialMotionY[pointerId];
}
mSwipeConsumer.onSwipeAccepted(pointerId, settling, initX, initY);
mClampedDistanceX = mSwipeConsumer.clampDistanceHorizontal(0, 0);
mClampedDistanceY = mSwipeConsumer.clampDistanceVertical(0, 0);
setDragState(touchMode ? STATE_DRAGGING : STATE_NONE_TOUCH);
return true;
}
return false;
}
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
saveInitialMotion(x, y, pointerId);
// Catch a settling view if possible.
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN:
{
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
saveInitialMotion(x, y, pointerId);
// A SwipeHelper can only manipulate one view at a time.
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
// Catch a settling view if possible.
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_MOVE:
{
if (mInitialMotionX == null || mInitialMotionY == null) {
break;
}
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
float downX = mInitialMotionX[pointerId];
float downY = mInitialMotionY[pointerId];
final float dx = x - downX;
final float dy = y - downY;
final boolean pastSlop = checkTouchSlop(dx, dy);
if (pastSlop) {
final int hDragRange = mSwipeConsumer.getHorizontalRange(dx, dy);
final int vDragRange = mSwipeConsumer.getVerticalRange(dx, dy);
if (hDragRange == 0 && vDragRange == 0) {
continue;
}
}
if (pastSlop && trySwipe(pointerId, false, downX, downY, dx, dy)) {
break;
}
}
saveLastMotion(ev);
break;
}
case MotionEvent.ACTION_POINTER_UP:
{
final int pointerId = ev.getPointerId(actionIndex);
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
{
cancel();
break;
}
default:
}
return mDragState == STATE_DRAGGING;
}
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
if (action == MotionEvent.ACTION_DOWN && mDragState != STATE_DRAGGING) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
if (mDragState != STATE_DRAGGING) {
trySwipe(pointerId, mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN:
{
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
saveInitialMotion(x, y, pointerId);
if (mDragState == STATE_DRAGGING) {
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_MOVE:
{
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) {
break;
}
final int index = ev.findPointerIndex(mActivePointerId);
if (index < 0) {
break;
}
final float x = ev.getX(index);
final float y = ev.getY(index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mClampedDistanceX + idx, mClampedDistanceY + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
float downX = mInitialMotionX[pointerId];
float downY = mInitialMotionY[pointerId];
final float dx = x - downX;
final float dy = y - downY;
if (checkTouchSlop(dx, dy) && trySwipe(pointerId, false, downX, downY, dx, dy)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
{
final int pointerId = ev.getPointerId(actionIndex);
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
int newActivePointer = INVALID_POINTER;
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int id = ev.getPointerId(i);
if (id == mActivePointerId) {
// This one's going away, skip.
continue;
}
if (!isValidPointerForActionMove(id)) {
continue;
}
if (trySwipe(id, true, mInitialMotionX[id], mInitialMotionX[id], 0, 0)) {
newActivePointer = mActivePointerId;
break;
}
}
if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP:
{
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL:
{
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
default:
}
}
/**
* Check if we've crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
*
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/
private boolean checkTouchSlop(float dx, float dy) {
final boolean checkHorizontal = mSwipeConsumer.getHorizontalRange(dx, dy) > 0;
final boolean checkVertical = mSwipeConsumer.getVerticalRange(dx, dy) > 0;
if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(mVelocityTracker.getXVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
final float yvel = clampMag(mVelocityTracker.getYVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}
public boolean nestedScrollingTrySwipe(int dx, int dy, boolean fly) {
return trySwipe(fly ? POINTER_NESTED_FLY : POINTER_NESTED_SCROLL, false, 0, 0, dx, dy, false);
}
public boolean nestedScrollingDrag(int dx, int dy, int[] consumed, boolean fly) {
if (mDragState == STATE_IDLE) {
return nestedScrollingTrySwipe(dx, dy, fly);
}
int clampedX = 0, clampedY = 0;
if (mClampedDistanceX != 0 || dx != 0) {
clampedX = mSwipeConsumer.clampDistanceHorizontal(mClampedDistanceX + dx, dx);
consumed[0] = clampedX - mClampedDistanceX;
}
if (mClampedDistanceY != 0 || dy != 0) {
clampedY = mSwipeConsumer.clampDistanceVertical(mClampedDistanceY + dy, dy);
consumed[1] = clampedY - mClampedDistanceY;
}
if (mClampedDistanceX == 0 && mClampedDistanceY == 0 && consumed[0] == 0 && consumed[1] == 0) {
mActivePointerId = INVALID_POINTER;
setDragState(STATE_IDLE);
return false;
} else {
dragTo(clampedX, clampedY, consumed[0], consumed[1]);
return true;
}
}
public void nestedScrollingRelease() {
if (mDragState == STATE_NONE_TOUCH) {
dispatchViewReleased(0, 0);
}
}
private void dragTo(int x, int y, int dx, int dy) {
int clampedX = x;
int clampedY = y;
final int oldX = mClampedDistanceX;
final int oldY = mClampedDistanceY;
if (dx != 0) {
clampedX = mSwipeConsumer.clampDistanceHorizontal(x, dx);
mClampedDistanceX = clampedX;
}
if (dy != 0) {
clampedY = mSwipeConsumer.clampDistanceVertical(y, dy);
mClampedDistanceY = clampedY;
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldX;
final int clampedDy = clampedY - oldY;
mSwipeConsumer.onSwipeDistanceChanged(clampedX, clampedY, clampedDx, clampedDy);
}
}
public SwipeConsumer getSwipeConsumer() {
return mSwipeConsumer;
}
private boolean isValidPointerForActionMove(int pointerId) {
if (!isPointerDown(pointerId)) {
Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received " + "for this pointer before ACTION_MOVE. It likely happened because " + " SwipeHelper did not receive all the events in the event stream.");
return false;
}
return true;
}
public int getMaxSettleDuration() {
return maxSettleDuration;
}
public void setMaxSettleDuration(int maxSettleDuration) {
this.maxSettleDuration = maxSettleDuration;
}
}
19
Source : StickyNavigationLayout_medlinker.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* <p>
* Note: @attr ref android.R.styleable#stickyLayout_content_id the content view must be the direct child of StickyNavigationLayout.
* or else may cause bug.
* </p>
*
* @author heaven7
* @attr ref com.heaven7.android.sticky_navigation_layout.demo.R.styleable#stickyLayout_content_id
*/
public clreplaced StickyNavigationLayout extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {
private static final String TAG = "StickyNavLayout";
private static final boolean DEBUG = false;
private static final long ANIMATED_SCROLL_GAP = 250;
/**
* The view is not currently scrolling.
*
* @see #getScrollState()
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* The view is currently being dragged by outside input such as user touch input.
*
* @see #getScrollState()
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* The view is currently animating to a final position while not under
* outside control.
*
* @see #getScrollState()
*/
public static final int SCROLL_STATE_SETTLING = 2;
/**
* the top view
*/
private View mTop;
/**
* the navigation view
*/
private View mIndicator;
/**
* the child view which will be intercept
*/
private View mContentView;
private int mTopViewId;
private int mIndicatorId;
private int mContentId;
private int mTopViewHeight;
private boolean mTopHide = false;
private GroupStickyDelegate mGroupStickyDelegate;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private int mLastTouchY, mLastTouchX;
private int mInitialTouchY, mInitialTouchX;
private OnScrollChangeListener mScrollListener;
private boolean mNeedIntercept;
private int mScrollState = SCROLL_STATE_IDLE;
private boolean mEnableStickyTouch = true;
/**
* last scroll time.
*/
private long mLastScroll;
/**
* auto fit the sticky scroll
*/
private final boolean mAutoFitScroll;
/**
* the percent of auto fix.
*/
private float mAutoFitPercent = 0.5f;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private boolean mNestedScrollInProgress;
private int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private final int[] mScrollConsumed = new int[2];
private final int[] mNestedOffsets = new int[2];
private final int[] mScrollOffset = new int[2];
private int mScrollPointerId;
public StickyNavigationLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// setOrientation(LinearLayout.VERTICAL);
mGroupStickyDelegate = new GroupStickyDelegate();
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
mScroller = new OverScroller(context);
// 触摸阙值
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavigationLayout);
// mCodeSet will set stick view from onFinishInflate.
mTopViewId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_top_id, 0);
mIndicatorId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_indicator_id, 0);
mContentId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_content_id, 0);
mAutoFitScroll = a.getBoolean(R.styleable.StickyNavigationLayout_stickyLayout_auto_fit_scroll, false);
a.recycle();
// getWindowVisibleDisplayFrame(mExpectTopRect);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTop = findViewById(mTopViewId);
mIndicator = findViewById(mIndicatorId);
mContentView = findViewById(mContentId);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mEnableStickyTouch && mContentView != null && mIndicator != null) {
// 设置view的高度 (将mViewPager。的高度设置为 整个 Height - 导航的高度) - 被拦截的child view
ViewGroup.LayoutParams params = mContentView.getLayoutParams();
int expect = getMeasuredHeight() - mIndicator.getMeasuredHeight();
// avoid onMeasure all the time
if (params.height != expect) {
params.height = getMeasuredHeight() - mIndicator.getMeasuredHeight();
}
if (DEBUG) {
Logger.i(TAG, "onMeasure", "height = " + params.height + ", snv height = " + getMeasuredHeight());
Logger.i(TAG, "onMeasure", "---> snv bottom= " + getBottom());
}
}
mGroupStickyDelegate.afterOnMeasure(this, mTop, mIndicator, mContentView);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (DEBUG) {
Logger.i(TAG, "onLayout");
}
if (mEnableStickyTouch && mTop != null) {
mTopViewHeight = mTop.getMeasuredHeight();
final ViewGroup.LayoutParams lp = mTop.getLayoutParams();
if (lp instanceof MarginLayoutParams) {
mTopViewHeight += ((MarginLayoutParams) lp).topMargin + ((MarginLayoutParams) lp).bottomMargin;
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (DEBUG) {
Logger.i(TAG, "onSizeChanged");
}
}
/**
* Return the current scrolling state of the RecyclerView.
*
* @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
* {@link #SCROLL_STATE_SETTLING}
*/
public int getScrollState() {
return mScrollState;
}
/**
* set if enable the sticky touch
*
* @param enable true to enable, false to disable.
*/
public void setEnableStickyTouch(boolean enable) {
if (mEnableStickyTouch != enable) {
this.mEnableStickyTouch = enable;
requestLayout();
}
}
/**
* is the sticky touch enabled.
*
* @return true to enable.
*/
public boolean isStickyTouchEnabled() {
return mEnableStickyTouch;
}
/**
* use {@link #addStickyDelegate(IStickyDelegate)} instead.
* set the sticky delegate.
*
* @param delegate the delegate
*/
@Deprecated
public void setStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.addStickyDelegate(delegate);
}
/**
* add a sticky delegate
*
* @param delegate sticky delegate.
*/
public void addStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.addStickyDelegate(delegate);
}
/**
* remove a sticky delegate
*
* @param delegate sticky delegate.
*/
public void removeStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.removeStickyDelegate(delegate);
}
/**
* set the OnScrollChangeListener
*
* @param l the listener
*/
public void setOnScrollChangeListener(OnScrollChangeListener l) {
this.mScrollListener = l;
}
/* @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
boolean canScrollHorizontally = canScrollHorizontally();
boolean canScrollVertically = canScrollVertically();
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
final int y = (int) (ev.getY() + 0.5f);
final int x = (int) (ev.getX() + 0.5f);
switch (action) {
case MotionEvent.ACTION_DOWN:
mScrollPointerId = ev.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (ev.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (ev.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
break;
case MotionEventCompat.ACTION_POINTER_DOWN:
mScrollPointerId = ev.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = x;
mInitialTouchY = mLastTouchY = y;
break;
case MotionEvent.ACTION_MOVE:
final int index = ev.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
break;
case MotionEventCompat.ACTION_POINTER_UP: {
onPointerUp(ev);
}
break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
stopNestedScroll();
}
break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
}
}
return mScrollState == SCROLL_STATE_DRAGGING;
}*/
/* @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Logger.i(TAG, "onInterceptTouchEvent", ev.toString());
if(ev.getAction() == MotionEvent.ACTION_UP){
checkAutoFitScroll();
}else if(ev.getAction() == MotionEvent.ACTION_DOWN){
mTotalDy = 0;
}
return super.onInterceptTouchEvent(ev);
}*/
private void onPointerUp(MotionEvent e) {
final int actionIndex = MotionEventCompat.getActionIndex(e);
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
private boolean canScrollHorizontally() {
return false;
}
private boolean canScrollVertically() {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnableStickyTouch) {
return super.onTouchEvent(event);
}
final boolean canScrollHorizontally = canScrollHorizontally();
final boolean canScrollVertically = canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(event);
final int action = MotionEventCompat.getActionMasked(event);
final int actionIndex = MotionEventCompat.getActionIndex(event);
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch(action) {
case MotionEvent.ACTION_DOWN:
{
if (!mScroller.isFinished())
mScroller.abortAnimation();
mScrollPointerId = event.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN:
{
mScrollPointerId = event.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (event.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);
}
break;
case MotionEvent.ACTION_MOVE:
{
// 遵循Google规范(比如recyclerView源代码)。避免处理嵌套滑动出问题。
final int index = event.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (event.getX(index) + 0.5f);
final int y = (int) (event.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 手向下滑动, dy >0 否则 <0.
if (scrollByInternal(0, dy, vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
}
break;
case MotionEventCompat.ACTION_POINTER_UP:
{
onPointerUp(event);
}
break;
case MotionEvent.ACTION_CANCEL:
cancelTouch();
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final float xvel = (int) mVelocityTracker.getXVelocity();
final float yvel = (int) mVelocityTracker.getYVelocity();
// final float xvel = -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void cancelTouch() {
/* if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}*/
resetTouch();
setScrollState(SCROLL_STATE_IDLE);
}
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll();
// releaseGlows();
}
/**
* Begin a standard fling with an initial velocity along each axis in pixels per second.
* If the velocity given is below the system-defined minimum this method will return false
* and no fling will occur.
*
* @param velocityX Initial horizontal velocity in pixels per second
* @param velocityY Initial vertical velocity in pixels per second
* @return true if the fling was started, false if the velocity was too low to fling or
* LayoutManager does not support scrolling in the axis fling is issued.
*/
public boolean fling(int velocityX, int velocityY) {
// TODO
return false;
}
void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
// stopScrollersInternal();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
}
dispatchOnScrollStateChanged(state);
}
private void dispatchOnScrollStateChanged(int state) {
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(this, state);
}
}
/**
* Does not perform bounds checking. Used by internal methods that have already validated input.
* <p/>
* It also reports any unused scroll request to the related EdgeEffect.
*
* @param dx The amount of horizontal scroll request
* @param dy The amount of vertical scroll request
* @param ev The originating MotionEvent, or null if not from a touch event.
* @return Whether any scroll was consumed in either direction.
*/
boolean scrollByInternal(int dx, int dy, MotionEvent ev) {
mScrollConsumed[0] = 0;
mScrollConsumed[1] = 0;
scrollInternal(dx, dy, mScrollConsumed);
int consumedX = mScrollConsumed[0];
int consumedY = mScrollConsumed[1];
int unconsumedX = dx - mScrollConsumed[0];
int unconsumedY = dy - mScrollConsumed[1];
if (dispatchNestedScroll(mScrollConsumed[0], mScrollConsumed[1], unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return mScrollConsumed[0] != 0 || mScrollConsumed[1] != 0;
}
/**
* Like {@link #scrollTo}, but scroll smoothly instead of immediately.
*
* @param x the position where to scroll on the X axis
* @param y the position where to scroll on the Y axis
*/
public final void smoothScrollTo(int x, int y) {
smoothScrollBy(x - getScrollX(), y - getScrollY());
}
/**
* Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
*
* @param dx the number of pixels to scroll by on the X axis
* @param dy the number of pixels to scroll by on the Y axis
*/
public final void smoothScrollBy(int dx, int dy) {
if (getChildCount() == 0) {
// Nothing to do.
return;
}
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
if (duration > ANIMATED_SCROLL_GAP) {
// design from scrollView
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
ViewCompat.postInvalidateOnAnimation(this);
} else {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollBy(dx, dy);
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
}
private static String getStateString(int state) {
switch(state) {
case SCROLL_STATE_DRAGGING:
return "SCROLL_STATE_DRAGGING";
case SCROLL_STATE_SETTLING:
return "SCROLL_STATE_SETTLING";
case SCROLL_STATE_IDLE:
default:
return "SCROLL_STATE_IDLE";
}
}
public void fling(int velocityY) {
// 使得当前对象只滑动到mTopViewHeight
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void computeScroll() {
// super.computeScroll();
if (mScroller.computeScrollOffset()) {
// true滑动尚未完成
scrollTo(0, mScroller.getCurrY());
// invalidate();
ViewCompat.postInvalidateOnAnimation(this);
}
}
void dispatchOnScrolled(int hresult, int vresult) {
// Preplaced the current scrollX/scrollY values; no actual change in these properties occurred
// but some general-purpose code may choose to respond to changes this way.
final int scrollX = getScrollX();
final int scrollY = getScrollY();
onScrollChanged(scrollX, scrollY, scrollX, scrollY);
// Preplaced the real deltas to onScrolled, the RecyclerView-specific method.
onScrolled(hresult, vresult);
// Invoke listeners last. Subclreplaceded view methods always handle the event first.
// All internal state is consistent by the time listeners are invoked.
if (mScrollListener != null) {
mScrollListener.onScrolled(this, hresult, vresult);
}
}
protected void onScrolled(int dx, int dy) {
// dy > 0 ? gesture up :gesture down
}
@Override
protected Parcelable onSaveInstanceState() {
final SaveState saveState = new SaveState(super.onSaveInstanceState());
saveState.mNeedIntercept = this.mNeedIntercept;
saveState.mTopHide = this.mTopHide;
saveState.mLastScroll = this.mLastScroll;
saveState.mEnableStickyTouch = this.mEnableStickyTouch;
return saveState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
if (state != null) {
SaveState ss = (SaveState) state;
this.mNeedIntercept = ss.mNeedIntercept;
this.mTopHide = ss.mTopHide;
this.mEnableStickyTouch = ss.mEnableStickyTouch;
this.mLastScroll = ss.mLastScroll;
}
}
protected static clreplaced SaveState extends BaseSavedState {
boolean mNeedIntercept;
boolean mTopHide;
boolean mEnableStickyTouch;
long mLastScroll;
public SaveState(Parcel source) {
super(source);
mNeedIntercept = source.readByte() == 1;
mTopHide = source.readByte() == 1;
mEnableStickyTouch = source.readByte() == 1;
mLastScroll = source.readLong();
}
public SaveState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeByte((byte) (mNeedIntercept ? 1 : 0));
out.writeByte((byte) (mTopHide ? 1 : 0));
out.writeByte((byte) (mEnableStickyTouch ? 1 : 0));
out.writeLong(mLastScroll);
}
public static final Parcelable.Creator<SaveState> CREATOR = new Parcelable.Creator<SaveState>() {
@Override
public SaveState createFromParcel(Parcel in) {
return new SaveState(in);
}
@Override
public SaveState[] newArray(int size) {
return new SaveState[size];
}
};
}
/**
* the internal group Sticky Delegate.
*/
private clreplaced GroupStickyDelegate implements IStickyDelegate {
private final ArrayList<IStickyDelegate> mDelegates = new ArrayList<>(5);
public void addStickyDelegate(IStickyDelegate delegate) {
mDelegates.add(delegate);
}
public void removeStickyDelegate(IStickyDelegate delegate) {
mDelegates.remove(delegate);
}
public void clear() {
mDelegates.clear();
}
@Override
public void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View content) {
for (IStickyDelegate delegate : mDelegates) {
delegate.afterOnMeasure(snv, top, indicator, content);
}
}
}
/**
* on scroll change listener.
*/
public interface OnScrollChangeListener {
/**
* called when the scroll state change
*
* @param snl the {@link StickyNavigationLayout}
* @param state the scroll state . see {@link StickyNavigationLayout#SCROLL_STATE_IDLE} and etc.
*/
void onScrollStateChanged(StickyNavigationLayout snl, int state);
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
* <p>
* This callback will also be called if visible item range changes after a layout
* calculation. In that case, dx and dy will be 0.
*
* @param snl the {@link StickyNavigationLayout} which scrolled.
* @param dx The amount of horizontal scroll.
* @param dy The amount of vertical scroll.
*/
void onScrolled(StickyNavigationLayout snl, int dx, int dy);
}
/**
* the sticky delegate
*/
public interface IStickyDelegate {
/**
* called after the {@link StickyNavigationLayout#onMeasure(int, int)}. this is useful used when we want to
* toggle two views visibility in {@link StickyNavigationLayout}(or else may cause bug). see it in demo.
* @param snv the {@link StickyNavigationLayout}
* @param top the top view
* @param indicator the indicator view
* @param contentView the content view
*/
void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View contentView);
}
/**
* a simple implements of {@link IStickyDelegate}
*/
public static clreplaced SimpleStickyDelegate implements IStickyDelegate {
@Override
public void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View contentView) {
}
}
// ======================== NestedScrollingParent begin ========================
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && mEnableStickyTouch && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
// Dispatch up to the nested parent
startNestedScroll(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL);
mNestedScrollInProgress = true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
scrollInternal(dx, dy, consumed);
// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
/**
* do scroll internal.
*
* @param dx the delta x, may be negative
* @param dy the delta y , may be negative
* @param consumed optional , in not null will contains the consumed x and y by this scroll.
* @return the consumed x and y as array by this scroll.
*/
private int[] scrollInternal(int dx, int dy, int[] consumed) {
// 向上滑 dy >0 , 下滑 dy < 0
// Logger.i(TAG, "scrollInternal", "dx = " + dx + ",dy = " + dy + " ,consumed = " + Arrays.toString(consumed));
// >0
final int scrollY = getScrollY();
if (consumed == null) {
consumed = new int[2];
}
int by = 0;
if (dy > 0) {
// gesture up
if (scrollY < mTopViewHeight) {
int maxH = mTopViewHeight - scrollY;
by = consumed[1] = Math.min(dy, maxH);
} else {
// ignore
}
} else {
// gesture down
if (scrollY > 0) {
consumed[1] = -Math.min(Math.abs(dy), scrollY);
by = consumed[1];
}
}
scrollBy(0, by);
return consumed;
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
/* net
inal int myConsumed = moveBy(dyUnconsumed);
final int myUnconsumed = dyUnconsumed - myConsumed;
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);*/
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
// This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
// sometimes between two nested scrolling views, we need a way to be able to know when any
// nested scrolling parent has stopped handling events. We do that by using the
// 'offset in window 'functionality to see if we have been moved from the event.
// This is a decent indication of whether we should take over the event stream or not.
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
// Logger.i(TAG, "onNestedScroll", "mTotalUnconsumed = " + (mTotalUnconsumed + Math.abs(dy)) );
/* if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
// moveSpinner(mTotalUnconsumed);
}*/
}
@Override
public void onStopNestedScroll(View target) {
// Logger.i(TAG, "onStopNestedScroll");
checkAutoFitScroll();
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Dispatch up our nested parent
stopNestedScroll();
}
private void checkAutoFitScroll() {
// check auto fit scroll
if (mAutoFitScroll) {
// check whole gesture.
final float scrollY = getScrollY();
if (scrollY >= mTopViewHeight * mAutoFitPercent) {
smoothScrollTo(0, mTopViewHeight);
} else {
smoothScrollTo(0, 0);
}
/* if (mTotalDy > 0) {
//finger up
if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
smoothScrollTo(0, mTopViewHeight);
} else {
smoothScrollTo(0, 0);
}
} else {
//finger down
//if larger the 1/2 * maxHeight go to maxHeight
if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
smoothScrollTo(0, mTopViewHeight);
} else {
smoothScrollTo(0, 0);
}
}*/
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
// ======================== NestedScrollingParent end ========================
// ======================== NestedScrollingChild begin ========================
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
// ======================== end NestedScrollingChild =====================
}
19
Source : StickyNavigationLayout_backup.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* sticky navigation layout:similar to google+ app.
* <p>
* Note: @attr ref android.R.styleable#stickyLayout_content_id the content view must be the direct child of StickyNavigationLayout.
* or else may cause bug.
* </p>
*
* @author heaven7
* @attr ref com.heaven7.android.sticky_navigation_layout.demo.R.styleable#stickyLayout_content_id
*/
public clreplaced StickyNavigationLayout_backup extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {
private static final String TAG = "StickyNavLayout";
private static final boolean DEBUG = true;
private static final long ANIMATED_SCROLL_GAP = 250;
/**
* indicate the scroll state is just end
*/
public static final int SCROLL_STATE_IDLE = 1;
/**
* indicate the scroll state is just begin.
*/
public static final int SCROLL_STATE_START = 2;
/**
* indicate the scroll state is setting/scrolling.
*/
public static final int SCROLL_STATE_SETTING = 3;
/**
* the view state is shown.
*/
public static final int VIEW_STATE_SHOW = 1;
/**
* the view state is hide
*/
public static final int VIEW_STATE_HIDE = 2;
/**
* the top view
*/
private View mTop;
/**
* the navigation view
*/
private View mIndicator;
/**
* the child view which will be intercept
*/
private View mContentView;
private int mTopViewId;
private int mIndicatorId;
private int mContentId;
private int mTopViewHeight;
private boolean mTopHide = false;
private GroupStickyDelegate mGroupStickyDelegate;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private int mLastY, mLastX;
private int mDownY, mDownX;
private boolean mDragging;
private OnScrollChangeListener mScrollListener;
private boolean mNeedIntercept;
private int mScrollState = SCROLL_STATE_IDLE;
private int mFocusDir;
private boolean mEnableStickyTouch = true;
private int mTotalDy;
/**
* last scroll time.
*/
private long mLastScroll;
/**
* auto fit the sticky scroll
*/
private final boolean mAutoFitScroll;
/**
* the percent of auto fix.
*/
private float mAutoFitPercent = 0.5f;
/**
* code set the sticky view ,not from xml.
*/
private boolean mCodeSet;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private int mTotalUnconsumed;
private int[] mParentScrollConsumed = new int[2];
private boolean mNestedScrollInProgress;
private int[] mParentOffsetInWindow = new int[2];
public StickyNavigationLayout_backup(Context context, AttributeSet attrs) {
super(context, attrs);
// setOrientation(LinearLayout.VERTICAL);
mGroupStickyDelegate = new GroupStickyDelegate();
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
mScroller = new OverScroller(context);
// 触摸阙值
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavigationLayout);
// mCodeSet will set stick view from onFinishInflate.
if (!mCodeSet) {
mTopViewId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_top_id, 0);
mIndicatorId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_indicator_id, 0);
mContentId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_content_id, 0);
}
mAutoFitScroll = a.getBoolean(R.styleable.StickyNavigationLayout_stickyLayout_auto_fit_scroll, false);
mAutoFitPercent = a.getFloat(R.styleable.StickyNavigationLayout_stickyLayout_threshold_percent, 0.5f);
a.recycle();
// getWindowVisibleDisplayFrame(mExpectTopRect);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTop = findViewById(mTopViewId);
mIndicator = findViewById(mIndicatorId);
mContentView = findViewById(mContentId);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mGroupStickyDelegate.afterOnMeasure(this, mTop, mIndicator, mContentView);
if (mEnableStickyTouch && mContentView != null && mIndicator != null) {
// 设置view的高度 (将mViewPager。的高度设置为 整个 Height - 导航的高度) - 被拦截的child view
ViewGroup.LayoutParams params = mContentView.getLayoutParams();
params.height = getMeasuredHeight() - mIndicator.getMeasuredHeight();
if (DEBUG) {
Logger.i(TAG, "onMeasure", "height = " + params.height + ", snv height = " + getMeasuredHeight());
Logger.i(TAG, "onMeasure", "---> snv bottom= " + getBottom());
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (DEBUG) {
Logger.i(TAG, "onLayout");
}
if (mEnableStickyTouch && mTop != null) {
mTopViewHeight = mTop.getMeasuredHeight();
final ViewGroup.LayoutParams lp = mTop.getLayoutParams();
if (lp instanceof MarginLayoutParams) {
mTopViewHeight += ((MarginLayoutParams) lp).topMargin + ((MarginLayoutParams) lp).bottomMargin;
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (DEBUG) {
Logger.i(TAG, "onSizeChanged");
}
}
/**
* set if enable the sticky touch
* @param enable true to enable, false to disable.
*/
public void setEnableStickyTouch(boolean enable) {
if (mEnableStickyTouch != enable) {
this.mEnableStickyTouch = enable;
requestLayout();
}
}
/**
* is the sticky touch enabled.
* @return true to enable.
*/
public boolean isStickyTouchEnabled() {
return mEnableStickyTouch;
}
public int getTopViewState() {
return mTopHide ? VIEW_STATE_HIDE : VIEW_STATE_SHOW;
}
/**
* set the sticky views
*
* @param top the top view
* @param indicator the indicator view
* @param content the content view
*/
/*public*/
void setStickyViews(View top, View indicator, View content) {
if (top == null || indicator == null || content == null) {
throw new NullPointerException();
}
mTopViewId = top.getId();
mIndicatorId = indicator.getId();
mContentId = content.getId();
mTop = top;
mIndicator = indicator;
mContentView = content;
mCodeSet = true;
requestLayout();
}
/**
* use {@link #addStickyDelegate(IStickyDelegate)} instead.
* set the sticky delegate.
* @param delegate the delegate
*/
@Deprecated
public void setStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.addStickyDelegate(delegate);
}
/**
* add a sticky delegate
* @param delegate sticky delegate.
*/
public void addStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.addStickyDelegate(delegate);
}
/**
* remove a sticky delegate
* @param delegate sticky delegate.
*/
public void removeStickyDelegate(IStickyDelegate delegate) {
mGroupStickyDelegate.removeStickyDelegate(delegate);
}
/**
* set the OnScrollChangeListener
*
* @param l the listener
*/
public void setOnScrollChangeListener(OnScrollChangeListener l) {
this.mScrollListener = l;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/* if (!mEnableStickyTouch) {
return super.onInterceptTouchEvent(ev);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);*/
// 1, 上拉的时候,停靠后分发给child滑动. max = mTopViewHeight
// 2, 下拉时,先拉上面的,拉完后分发给child.
/* int action = ev.getAction();
int y = (int) (ev.getY() + 0.5f);
int x = (int) (ev.getY() + 0.5f);
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownY = mLastY = y;
mDownX = mLastX = x;
break;
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
if (Math.abs(dy) > mTouchSlop) {
if (mNeedIntercept) {
return true;
}
if (dy > 0) {
return getScrollY() == mTopViewHeight;
}
if (mGroupStickyDelegate.shouldIntercept(this, dy,
mTopHide ? VIEW_STATE_HIDE : VIEW_STATE_SHOW)) {
mDragging = true;
return true;
}
}
break;
}*/
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnableStickyTouch) {
return super.onInterceptTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
int action = event.getAction();
int y = (int) (event.getY() + 0.5f);
int x = (int) (event.getY() + 0.5f);
switch(action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished())
mScroller.abortAnimation();
// mVelocityTracker.addMovement(event);
mDownY = mLastY = y;
mDownX = mLastX = x;
return true;
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
int dx = x - mLastX;
if (!mDragging && Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (mDragging) {
// 手向下滑动, dy >0 否则 <0.
final int scrollY = getScrollY();
final int totalDy = mTotalDy = y - mDownY;
// Logger.i(TAG, "onTouchEvent", "ScrollY = " + scrollY + " ,dy = " + dy + " , mTopViewHeight = " + mTopViewHeight);
mLastY = y;
mLastX = x;
mFocusDir = dy < 0 ? View.FOCUS_DOWN : View.FOCUS_UP;
setScrollState(SCROLL_STATE_START);
onPreScrollDistanceChange(0, dy, 0, totalDy);
if (dy < 0) {
// 手势向上滑动 ,view down
/**
* called [ onTouchEvent() ]: ScrollY = 666 ,dy = -7.4692383 , mTopViewHeight = 788
* called [ onTouchEvent() ]: ScrollY = 673 ,dy = -3.748291 , mTopViewHeight = 788
*/
if (scrollY == mTopViewHeight) {
// 分发给child
mGroupStickyDelegate.dispatchTouchEventToChild(this, dx, dy, MotionEvent.obtain(event));
} else if (scrollY - dy > mTopViewHeight) {
// top height is the max scroll height
scrollTo(getScrollX(), mTopViewHeight);
} else {
scrollBy(0, -dy);
}
} else {
// 手势向下
if (scrollY == 0) {
// 分发事件给child
// mGroupStickyDelegate.scrollBy(this, dy);
mGroupStickyDelegate.dispatchTouchEventToChild(this, dx, dy, MotionEvent.obtain(event));
} else {
if (scrollY - dy < 0) {
dy = scrollY;
}
scrollBy(0, -dy);
}
}
onAfterScrollDistanceChange(0, dy, 0, totalDy);
}
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
if (!mScroller.isFinished()) {
// 如果手离开了,就终止滑动
mScroller.abortAnimation();
}
setScrollState(SCROLL_STATE_IDLE);
mFocusDir = 0;
break;
case MotionEvent.ACTION_UP:
if (Math.abs(y - mLastY) > mTouchSlop) {
// 1000表示像素/秒
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) mVelocityTracker.getYVelocity();
if (Math.abs(velocityY) > mMinimumVelocity) {
if (DEBUG) {
Logger.i(TAG, "onTouchEvent", "begin fling: velocityY = " + velocityY);
}
// fling(-velocityY);
smoothScrollTo(0, mTotalDy < 0 ? mTopViewHeight : 0);
}
} else {
// check auto fit scroll
if (mAutoFitScroll) {
// check whole gesture.
if (mTotalDy < 0) {
// finger up
// if larger the 1/2 * maxHeight go to maxHeight
if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
smoothScrollTo(0, mTopViewHeight);
} else {
smoothScrollTo(0, 0);
}
} else {
// finger down
if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
smoothScrollTo(0, 0);
} else {
smoothScrollTo(0, mTopViewHeight);
}
}
}
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mDragging = false;
setScrollState(SCROLL_STATE_IDLE);
mFocusDir = 0;
mGroupStickyDelegate.onTouchEventUp(this, event);
break;
}
return super.onTouchEvent(event);
}
/**
* Like {@link #scrollTo}, but scroll smoothly instead of immediately.
*
* @param x the position where to scroll on the X axis
* @param y the position where to scroll on the Y axis
*/
public final void smoothScrollTo(int x, int y) {
smoothScrollBy(x - getScrollX(), y - getScrollY());
}
/**
* Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
*
* @param dx the number of pixels to scroll by on the X axis
* @param dy the number of pixels to scroll by on the Y axis
*/
public final void smoothScrollBy(int dx, int dy) {
if (getChildCount() == 0) {
// Nothing to do.
return;
}
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
if (duration > ANIMATED_SCROLL_GAP) {
// design from scrollView
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
ViewCompat.postInvalidateOnAnimation(this);
} else {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollBy(dx, dy);
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
}
/**
* get the focus direction . 0 or {@link View#FOCUS_DOWN} or {@link View#FOCUS_UP}
*
* @return the focus direction
*/
public int getFocusDirection() {
return mFocusDir;
}
/**
* called pre ths scroll distance change. may be negative .
*
* @param dx the delta x between this touch and last touch
* @param dy the delta y between this touch and last touch
* @param totalDx the delta x between this touch and first touch
* @param totalDy the delta y between this touch and first touch
*/
private void onPreScrollDistanceChange(int dx, int dy, int totalDx, int totalDy) {
if (mScrollListener != null) {
mScrollListener.onPreScrollDistanceChange(this, dx, dy, totalDx, totalDy);
} else {
if (getContext() instanceof OnScrollChangeListener) {
((OnScrollChangeListener) getContext()).onPreScrollDistanceChange(this, dx, dy, totalDx, totalDy);
}
}
}
/**
* called after ths scroll distance change. may be negative .
*
* @param dx the delta x between this touch and last touch
* @param dy the delta y between this touch and last touch
* @param totalDx the delta x between this touch and first touch
* @param totalDy the delta y between this touch and first touch
*/
private void onAfterScrollDistanceChange(int dx, int dy, int totalDx, int totalDy) {
if (mScrollListener != null) {
mScrollListener.onAfterScrollDistanceChange(this, dx, dy, totalDx, totalDy);
} else {
if (getContext() instanceof OnScrollChangeListener) {
((OnScrollChangeListener) getContext()).onAfterScrollDistanceChange(this, dx, dy, totalDx, totalDy);
}
}
}
/**
* called when the scroll state change
*
* @param expectScrollState the expect state.
*/
private void setScrollState(int expectScrollState) {
expectScrollState = adjustState(expectScrollState);
if (mScrollState == expectScrollState) {
// ignore
return;
}
if (DEBUG) {
Logger.i(TAG, "setScrollState", "new state = " + getStateString(expectScrollState));
}
mScrollState = expectScrollState;
if (mScrollListener != null) {
mScrollListener.onScrollStateChange(this, expectScrollState, mFocusDir);
} else {
if (getContext() instanceof OnScrollChangeListener) {
((OnScrollChangeListener) getContext()).onScrollStateChange(this, expectScrollState, mFocusDir);
}
}
}
private static String getStateString(int state) {
switch(state) {
case SCROLL_STATE_START:
return "SCROLL_STATE_START";
case SCROLL_STATE_SETTING:
return "SCROLL_STATE_SETTING";
case SCROLL_STATE_IDLE:
default:
return "SCROLL_STATE_IDLE";
}
}
private int adjustState(int expectScrollState) {
switch(expectScrollState) {
case SCROLL_STATE_START:
{
if (mScrollState == SCROLL_STATE_START) {
return SCROLL_STATE_SETTING;
} else if (mScrollState == SCROLL_STATE_IDLE) {
return expectScrollState;
} else {
return SCROLL_STATE_SETTING;
}
}
case SCROLL_STATE_SETTING:
case SCROLL_STATE_IDLE:
default:
return expectScrollState;
}
}
public void fling(int velocityY) {
// 使得当前对象只滑动到mTopViewHeight
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
ViewCompat.postInvalidateOnAnimation(this);
}
// 限定滑动的y范围
@Override
public void scrollTo(int x, int y) {
// Logger.i(TAG, "scrollTo", "x = " + x + ", y = " + y);
if (y < 0) {
y = 0;
}
// maxY = mTopViewHeight
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
if (y == 0 || y == mTopViewHeight) {
mNeedIntercept = false;
} else {
mNeedIntercept = true;
}
super.scrollTo(x, y);
mTopHide = getScrollY() == mTopViewHeight;
}
@Override
public void computeScroll() {
// super.computeScroll();
if (mScroller.computeScrollOffset()) {
// true滑动尚未完成
scrollTo(0, mScroller.getCurrY());
// invalidate();
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected Parcelable onSaveInstanceState() {
final SaveState saveState = new SaveState(super.onSaveInstanceState());
saveState.mNeedIntercept = this.mNeedIntercept;
saveState.mTopHide = this.mTopHide;
saveState.mLastScroll = this.mLastScroll;
saveState.mEnableStickyTouch = this.mEnableStickyTouch;
saveState.mCodeSet = this.mCodeSet;
saveState.mTopViewId = this.mTopViewId;
saveState.mIndicatorId = this.mIndicatorId;
saveState.mContentId = this.mContentId;
return saveState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
if (state != null) {
SaveState ss = (SaveState) state;
this.mNeedIntercept = ss.mNeedIntercept;
this.mTopHide = ss.mTopHide;
this.mEnableStickyTouch = ss.mEnableStickyTouch;
this.mLastScroll = ss.mLastScroll;
this.mCodeSet = ss.mCodeSet;
this.mTopViewId = ss.mTopViewId;
this.mIndicatorId = ss.mIndicatorId;
this.mContentId = ss.mContentId;
}
}
protected static clreplaced SaveState extends BaseSavedState {
boolean mNeedIntercept;
boolean mTopHide;
boolean mEnableStickyTouch;
long mLastScroll;
boolean mCodeSet;
int mTopViewId;
int mIndicatorId;
int mContentId;
public SaveState(Parcel source) {
super(source);
mNeedIntercept = source.readByte() == 1;
mTopHide = source.readByte() == 1;
mEnableStickyTouch = source.readByte() == 1;
mLastScroll = source.readLong();
mCodeSet = source.readByte() == 1;
mTopViewId = source.readInt();
mIndicatorId = source.readInt();
mContentId = source.readInt();
}
public SaveState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeByte((byte) (mNeedIntercept ? 1 : 0));
out.writeByte((byte) (mTopHide ? 1 : 0));
out.writeByte((byte) (mEnableStickyTouch ? 1 : 0));
out.writeLong(mLastScroll);
out.writeByte((byte) (mCodeSet ? 1 : 0));
out.writeInt(mTopViewId);
out.writeInt(mIndicatorId);
out.writeInt(mContentId);
}
public static final Creator<SaveState> CREATOR = new Creator<SaveState>() {
@Override
public SaveState createFromParcel(Parcel in) {
return new SaveState(in);
}
@Override
public SaveState[] newArray(int size) {
return new SaveState[size];
}
};
}
/**
* the internal group Sticky Delegate.
*/
private clreplaced GroupStickyDelegate implements IStickyDelegate {
private final ArrayList<IStickyDelegate> mDelegates = new ArrayList<>(5);
public void addStickyDelegate(IStickyDelegate delegate) {
mDelegates.add(delegate);
}
public void removeStickyDelegate(IStickyDelegate delegate) {
mDelegates.remove(delegate);
}
public void clear() {
mDelegates.clear();
}
@Override
public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
for (IStickyDelegate delegate : mDelegates) {
if (delegate.shouldIntercept(snv, dy, topViewState)) {
return true;
}
}
return false;
}
@Override
public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
for (IStickyDelegate delegate : mDelegates) {
delegate.afterOnMeasure(snv, top, indicator, contentView);
}
}
@Override
public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
for (IStickyDelegate delegate : mDelegates) {
delegate.dispatchTouchEventToChild(snv, dx, dy, event);
}
}
@Override
public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
for (IStickyDelegate delegate : mDelegates) {
delegate.onTouchEventUp(snv, event);
}
}
}
/**
* on scroll change listener.
*/
public interface OnScrollChangeListener {
/**
* called when ths scroll distance change. may be negative .
*
* @param snl the {@link StickyNavigationLayout_backup}
* @param dx the delta x between this touch and last touch
* @param dy the delta y between this touch and last touch
* @param totalDx the delta x between this touch and first touch
* @param totalDy the delta y between this touch and first touch
*/
void onPreScrollDistanceChange(StickyNavigationLayout_backup snl, int dx, int dy, int totalDx, int totalDy);
/**
* called when ths scroll distance change. may be negative .
*
* @param snl the {@link StickyNavigationLayout_backup}
* @param dx the delta x between this touch and last touch
* @param dy the delta y between this touch and last touch
* @param totalDx the delta x between this touch and first touch
* @param totalDy the delta y between this touch and first touch
*/
void onAfterScrollDistanceChange(StickyNavigationLayout_backup snl, int dx, int dy, int totalDx, int totalDy);
/**
* called when the scroll state change
*
* @param snl the {@link StickyNavigationLayout_backup}
* @param state the scroll state . see {@link StickyNavigationLayout_backup#SCROLL_STATE_START} and etc.
* @param focusDirection {@link View#FOCUS_UP} means finger down or {@link View#FOCUS_DOWN} means finger up.
*/
void onScrollStateChange(StickyNavigationLayout_backup snl, int state, int focusDirection);
}
/**
* the sticky delegate
*/
public interface IStickyDelegate {
/**
* called when you should intercept child's touch event.
*
* @param snv the {@link StickyNavigationLayout_backup}
* @param dy the delta y distance
* @param topViewState the view state of top view. {@link #VIEW_STATE_SHOW} or {@link #VIEW_STATE_HIDE}
* @return true to intercept
*/
boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState);
/**
* called after the {@link StickyNavigationLayout_backup#onMeasure(int, int)}. this is useful used when we want to
* toggle two views visibility in {@link StickyNavigationLayout_backup}(or else may cause bug). see it in demo.
* @param snv the {@link StickyNavigationLayout_backup}
* @param top the top view
* @param indicator the indicator view
* @param contentView the content view
*/
void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView);
/**
* dispatch the touch event
* @param snv the {@link StickyNavigationLayout_backup}
* @param dx the delta x
* @param dy the delta y
* @param event the event.
*/
void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event);
void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event);
}
/**
* a simple implements of {@link IStickyDelegate}
*/
public static clreplaced SimpleStickyDelegate implements IStickyDelegate {
@Override
public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
return false;
}
@Override
public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
}
@Override
public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
}
@Override
public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
}
}
public static clreplaced RecyclerViewStickyDelegate implements IStickyDelegate {
private final WeakReference<RecyclerView> mWeakRecyclerView;
private boolean mParentReceived;
public RecyclerViewStickyDelegate(RecyclerView mRv) {
this.mWeakRecyclerView = new WeakReference<>(mRv);
}
@Override
public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
final RecyclerView view = mWeakRecyclerView.get();
if (view == null)
return false;
final int position = findFirstVisibleItemPosition(view);
if (position == -1)
return false;
final View child = view.getChildAt(position);
boolean isTopHidden = topViewState == StickyNavigationLayout_backup.VIEW_STATE_HIDE;
if (!isTopHidden || (child != null && child.getTop() == 0 && dy > 0)) {
// 滑动到顶部,并且要继续向下滑动时,拦截触摸
return true;
}
return false;
}
@Override
public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
}
@Override
public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
final RecyclerView view = mWeakRecyclerView.get();
if (view != null) {
/* final int position = findFirstVisibleItemPosition(view);
if (position == -1){
return;
}
final View child = view.getChildAt(position);
if(child != null && child.getTop() == 0 && dy > 0){
if(snv.getTopViewState() == VIEW_STATE_SHOW){
ViewGroup vg = (ViewGroup) view.getParent();
vg.dispatchTouchEvent(event);
mParentReceived = true;
return;
}
}*/
view.scrollBy(0, -dy);
if (DEBUG) {
Logger.i(TAG, "dispatchTouchEventToChild", "dy = " + dy + " ,can scroll: " + view.getLayoutManager().canScrollVertically());
}
}
}
@Override
public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
/*if( mParentReceived ) {
mParentReceived = false;
final RecyclerView view = mWeakRecyclerView.get();
if (view != null) {
ViewGroup vg = (ViewGroup) view.getParent();
vg.dispatchTouchEvent(event);
}
}*/
}
public static int findFirstVisibleItemPosition(RecyclerView rv) {
RecyclerView.LayoutManager lm = rv.getLayoutManager();
int firstPos = RecyclerView.NO_POSITION;
if (lm instanceof GridLayoutManager) {
firstPos = ((GridLayoutManager) lm).findFirstVisibleItemPosition();
} else if (lm instanceof LinearLayoutManager) {
firstPos = ((LinearLayoutManager) lm).findFirstVisibleItemPosition();
} else if (lm instanceof StaggeredGridLayoutManager) {
int[] positions = ((StaggeredGridLayoutManager) lm).findFirstVisibleItemPositions(null);
for (int pos : positions) {
if (pos < firstPos) {
firstPos = pos;
}
}
}
return firstPos;
}
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
/* public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}*/
// ======================== NestedScrollingParent begin ========================
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && mEnableStickyTouch && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
// Dispatch up to the nested parent
startNestedScroll(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
/**
* 滑动的最大高度:
*/
final int scrollY = getScrollY();
int by = 0;
if (dy > 0) {
// 手势向下,view向上
if (scrollY != 0) {
consumed[1] = Math.min(dy, scrollY);
by = -consumed[1];
}
} else {
// 手势向上,view向下
if (scrollY < mTopViewHeight) {
int maxH = mTopViewHeight - scrollY;
consumed[1] = -Math.min(Math.abs(dy), maxH);
by = -consumed[1];
} else {
// ignore
}
}
scrollBy(0, by);
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
/* if (dy > 0 && mTotalUnconsumed > 0) {
if ( dy > mTotalUnconsumed ) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
Logger.i(TAG, "onNestedPreScroll", "mTotalUnconsumed = " + mTotalUnconsumed);
// moveSpinner(mTotalUnconsumed);
}*/
// If a client layout is using a custom start position for the circle
// view, they mean to hide it again before scrolling the child view
// If we get back to mTotalUnconsumed == 0 and there is more to go, hide
// the circle so it isn't exposed if its blocking content is moved
/* if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
&& Math.abs(dy - consumed[1]) > 0) {
mCircleView.setVisibility(View.GONE);
}*/
// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
/* net
inal int myConsumed = moveBy(dyUnconsumed);
final int myUnconsumed = dyUnconsumed - myConsumed;
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);*/
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
// This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
// sometimes between two nested scrolling views, we need a way to be able to know when any
// nested scrolling parent has stopped handling events. We do that by using the
// 'offset in window 'functionality to see if we have been moved from the event.
// This is a decent indication of whether we should take over the event stream or not.
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
Logger.i(TAG, "onNestedScroll", "mTotalUnconsumed = " + (mTotalUnconsumed + Math.abs(dy)));
/* if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
// moveSpinner(mTotalUnconsumed);
}*/
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Finish the spinner for nested scrolling if we ever consumed any
// unconsumed nested scroll
if (mTotalUnconsumed > 0) {
// finishSpinner(mTotalUnconsumed);
mTotalUnconsumed = 0;
}
// Dispatch up our nested parent
stopNestedScroll();
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
// ======================== NestedScrollingParent end ========================
// ======================== NestedScrollingChild begin ========================
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
// ======================== end NestedScrollingChild =====================
}
19
Source : ScrollHelper.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* <p>
* this clreplaced is a simple implement of {@link IScrollHelper}. it can do most work of scroller.
* such as {@link IScrollHelper#smoothScrollTo(int, int)}, {@link IScrollHelper#smoothScrollBy(int, int)} and etc.
* </p>
* Created by heaven7 on 2016/11/14.
*/
public clreplaced ScrollHelper implements IScrollHelper {
/*protected*/
static final boolean DEBUG = Util.sDEBUG;
private static final long ANIMATED_SCROLL_GAP = 250;
private CopyOnWriteArrayList<OnScrollChangeListener> mScrollListeners;
private final OverScroller mScroller;
protected final ScrollCallback mCallback;
protected final String mTag;
private final View mTarget;
private final int mTouchSlop;
private final float mMinFlingVelocity;
private final float mMaxFlingVelocity;
private long mLastScroll;
private int mScrollState = SCROLL_STATE_IDLE;
/**
* create a ScrollHelper.
*
* @param target the target view
* @param scroller the over Scroller
* @param callback the callback
*/
public ScrollHelper(View target, OverScroller scroller, ScrollCallback callback) {
this(target, 1, scroller, callback);
}
/**
* create a ScrollHelper.
*
* @param target the target view
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param scroller the over Scroller
* @param callback the callback
*/
public ScrollHelper(View target, float sensitivity, OverScroller scroller, ScrollCallback callback) {
Util.check(target, "target view can't be null.");
Util.check(scroller, null);
Util.check(callback, "ScrollCallback can't be null");
final ViewConfiguration vc = ViewConfiguration.get(target.getContext());
this.mTag = target.getClreplaced().getSimpleName();
this.mTarget = target;
this.mCallback = callback;
this.mScroller = scroller;
this.mTouchSlop = (int) (vc.getScaledTouchSlop() * (1 / sensitivity));
this.mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
this.mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
}
public OverScroller getScroller() {
return mScroller;
}
public int getTouchSlop() {
return mTouchSlop;
}
public float getMinFlingVelocity() {
return mMinFlingVelocity;
}
public float getMaxFlingVelocity() {
return mMaxFlingVelocity;
}
public View getTarget() {
return mTarget;
}
@Override
public void dispatchOnScrolled(int dx, int dy) {
// Preplaced the current scrollX/scrollY values; no actual change in these properties occurred
// but some general-purpose code may choose to respond to changes this way.
/* final int scrollX = mTarget.getScrollX();
final int scrollY = mTarget.getScrollY();
mTarget.onScrollChanged(scrollX, scrollY, scrollX, scrollY);*/
// Invoke listeners last. Subclreplaceded view methods always handle the event first.
// All internal state is consistent by the time listeners are invoked.
if (mScrollListeners != null && mScrollListeners.size() > 0) {
for (OnScrollChangeListener l : mScrollListeners) {
if (l != null) {
l.onScrolled(mTarget, dx, dy);
}
}
}
// Preplaced the real deltas to onScrolled, the RecyclerView-specific method.
onScrolled(dx, dy);
}
/**
* Called when the scroll position of this view changes. Subclreplacedes should use
* this method to respond to scrolling within the adapter's data set instead of an explicit
* listener. this is called in {@link #dispatchOnScrolled(int, int)}.
* <p/>
* <p>This method will always be invoked before listeners. If a subclreplaced needs to perform
* any additional upkeep or bookkeeping after scrolling but before listeners run,
* this is a good place to do so.</p>
*
* @param dx horizontal distance scrolled in pixels
* @param dy vertical distance scrolled in pixels
*/
protected void onScrolled(int dx, int dy) {
mCallback.onScrolled(dx, dy);
}
@Override
public int getScrollState() {
return mScrollState;
}
@Override
public void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(mTag, "setting scroll state to " + state + " from " + mScrollState, new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollerInternal();
}
dispatchOnScrollStateChanged(state);
}
protected void stopScrollerInternal() {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
}
/**
* dispatch the scroll state change, this is called in {@link #setScrollState(int)}.
*
* @param state the target scroll state.
*/
protected void dispatchOnScrollStateChanged(int state) {
if (mScrollListeners != null && mScrollListeners.size() > 0) {
for (OnScrollChangeListener l : mScrollListeners) {
if (l != null) {
l.onScrollStateChanged(mTarget, state);
}
}
}
}
@Override
public void scrollBy(int dx, int dy) {
scrollTo(mTarget.getScrollX() + dx, mTarget.getScrollY() + dy);
}
/**
* {@inheritDoc}. Note: this is similar to {@link View#scrollTo(int, int)}, but limit the range of scroll,
* which is indicate by {@link ScrollCallback#getMaximumXScrollDistance(View)} with {@link ScrollCallback#getMaximumYScrollDistance(View)}.
*
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
@Override
public void scrollTo(int x, int y) {
mTarget.scrollTo(Math.min(x, mCallback.getMaximumXScrollDistance(mTarget)), Math.min(y, mCallback.getMaximumYScrollDistance(mTarget)));
}
@Override
public void smoothScrollBy(int dx, int dy) {
if (mTarget instanceof ViewGroup && ((ViewGroup) mTarget).getChildCount() == 0) {
// Nothing to do.
return;
}
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
if (duration > ANIMATED_SCROLL_GAP) {
// design from scrollView
final int scrollX = mTarget.getScrollX();
final int scrollY = mTarget.getScrollY();
final int maxX = mCallback.getMaximumXScrollDistance(mTarget);
final int maxY = mCallback.getMaximumYScrollDistance(mTarget);
if ((scrollX + dx) > maxX) {
dx -= scrollX + dx - maxX;
}
if ((scrollY + dy) > maxY) {
dy -= scrollY + dy - maxY;
}
setScrollState(SCROLL_STATE_SETTLING);
mScroller.startScroll(scrollX, scrollY, dx, dy);
ViewCompat.postInvalidateOnAnimation(mTarget);
} else {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mTarget.scrollBy(dx, dy);
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
}
@Override
public final void smoothScrollTo(int x, int y) {
smoothScrollBy(x - mTarget.getScrollX(), y - mTarget.getScrollY());
}
@Override
public void stopScroll() {
setScrollState(SCROLL_STATE_IDLE);
stopScrollerInternal();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
// true if not finish
if (DEBUG) {
Log.i(mTag, "computeScroll: scroll not finished: currX = " + mScroller.getCurrX() + " ,currY = " + mScroller.getCurrY());
}
mTarget.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
ViewCompat.postInvalidateOnAnimation(mTarget);
}
}
@Override
public boolean isScrollFinish() {
return mScroller.isFinished();
}
@Override
public boolean fling(float velocityX, float velocityY) {
final boolean canScrollHorizontal = mCallback.canScrollHorizontally(mTarget);
final boolean canScrollVertical = mCallback.canScrollVertically(mTarget);
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
return onFling(canScrollHorizontal, canScrollVertical, velocityX, velocityY);
}
@Override
public void addOnScrollChangeListener(OnScrollChangeListener l) {
if (mScrollListeners == null) {
mScrollListeners = new CopyOnWriteArrayList<>();
}
mScrollListeners.add(l);
}
@Override
public void removeOnScrollChangeListener(OnScrollChangeListener l) {
if (mScrollListeners != null) {
mScrollListeners.remove(l);
}
}
@Override
public boolean hasOnScrollChangeListener(OnScrollChangeListener l) {
return mScrollListeners != null && mScrollListeners.contains(l);
}
/**
* do fling , this method is called in {@link #fling(float, float)}
*
* @param canScrollHorizontal if can scroll in Horizontal
* @param canScrollVertical if can scroll in Vertical
* @param velocityX the velocity of X
* @param velocityY the velocity of y
* @return true if the fling was started.
*/
protected boolean onFling(boolean canScrollHorizontal, boolean canScrollVertical, float velocityX, float velocityY) {
if (canScrollHorizontal || canScrollVertical) {
setScrollState(SCROLL_STATE_SETTLING);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mScroller.fling(mTarget.getScrollX(), mTarget.getScrollY(), (int) velocityX, (int) velocityY, 0, canScrollHorizontal ? mCallback.getMaximumXScrollDistance(mTarget) : 0, 0, canScrollVertical ? mCallback.getMaximumYScrollDistance(mTarget) : 0);
// TODO why recyclerView use mScroller.fling(0, 0, (int)velocityX, (int)velocityY,Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
ViewCompat.postInvalidateOnAnimation(mTarget);
return true;
}
return false;
}
/**
* get the scroll state as string log.
* @param state the scroll state.
* @return the state as string
*/
public static String getScrollStateString(int state) {
switch(state) {
case SCROLL_STATE_DRAGGING:
return "SCROLL_STATE_DRAGGING";
case SCROLL_STATE_SETTLING:
return "SCROLL_STATE_SETTLING";
case SCROLL_STATE_IDLE:
return "SCROLL_STATE_IDLE";
default:
return "unknown state";
}
}
/**
* the scroll callback of {@link ScrollHelper}.
*/
public static abstract clreplaced ScrollCallback {
/**
* if can scroll in Horizontal
*
* @param target the target view.
* @return true if can scroll in Horizontal
*/
public abstract boolean canScrollHorizontally(View target);
/**
* if can scroll in Vertical
*
* @param target the target view.
* @return true if can scroll in Vertical
*/
public abstract boolean canScrollVertically(View target);
/**
* get the maximum x scroll distance of the target view.
*
* @param target the target view.
* @return the maximum x scroll distance
*/
public int getMaximumXScrollDistance(View target) {
return target.getWidth();
}
/**
* get the maximum y scroll distance of the target view.
*
* @param target the target view.
* @return the maximum y scroll distance
*/
public int getMaximumYScrollDistance(View target) {
return target.getHeight();
}
/**
* called in {@link ScrollHelper#dispatchOnScrolled(int, int)}.
*
* @param dx the delta x
* @param dy the delta y
*/
public void onScrolled(int dx, int dy) {
}
}
}
19
Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* create a ScrollHelper.
* @param target the target view
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param scroller the over Scroller
* @param callback the callback
*/
public static ScrollHelper create(View target, float sensitivity, OverScroller scroller, ScrollHelper.ScrollCallback callback) {
return new ScrollHelper(target, sensitivity, scroller, callback);
}
19
Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* create a ScrollHelper.
* @param target the target view
* @param scroller the over Scroller
* @param callback the callback
*/
public static ScrollHelper create(View target, OverScroller scroller, ScrollHelper.ScrollCallback callback) {
return new ScrollHelper(target, 1, scroller, callback);
}
19
Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* create the nested scroll helper, but the target view must implements {@link NestedScrollingChild}.
* @param target the target view
* @param scroller the scroller
* @param callback the callback
*/
public static NestedScrollHelper create(View target, OverScroller scroller, NestedScrollHelper.NestedScrollCallback callback) {
return new NestedScrollHelper(target, 1, scroller, (NestedScrollingChild) target, callback);
}
19
Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun
with Apache License 2.0
from LightSun
/**
* create the nested scroll helper.
* @param target the target view
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param scroller the scroller
* @param child the NestedScrollingChild.
* @param callback the callback
*/
public static NestedScrollHelper create(View target, float sensitivity, OverScroller scroller, NestedScrollingChild child, NestedScrollHelper.NestedScrollCallback callback) {
return new NestedScrollHelper(target, sensitivity, scroller, child, callback);
}
19
Source : SmartDragLayout.java
with Apache License 2.0
from li-xiaojun
with Apache License 2.0
from li-xiaojun
/**
* Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
* Create by dance, at 2018/12/23
*/
public clreplaced SmartDragLayout extends LinearLayout implements NestedScrollingParent {
private View child;
OverScroller scroller;
VelocityTracker tracker;
// 是否启用手势拖拽
boolean enableDrag = true;
boolean dismissOnTouchOutside = true;
boolean isUserClose = false;
// 是否开启三段拖拽
boolean isThreeDrag = false;
LayoutStatus status = LayoutStatus.Close;
public SmartDragLayout(Context context) {
this(context, null);
}
public SmartDragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (enableDrag) {
scroller = new OverScroller(context);
}
}
int maxY;
int minY;
@Override
public void onViewAdded(View c) {
super.onViewAdded(c);
child = c;
}
int lastHeight;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
maxY = child.getMeasuredHeight();
minY = 0;
int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
if (status == LayoutStatus.Open) {
if (isThreeDrag) {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
} else {
// 通过scroll上移
scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
}
}
lastHeight = maxY;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
isUserClose = true;
return super.dispatchTouchEvent(ev);
}
float touchX, touchY;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (enableDrag && scroller.computeScrollOffset()) {
touchX = 0;
touchY = 0;
return true;
}
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (enableDrag) {
if (tracker != null)
tracker.clear();
tracker = VelocityTracker.obtain();
}
touchX = event.getX();
touchY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (enableDrag && tracker != null) {
tracker.addMovement(event);
tracker.computeCurrentVelocity(1000);
int dy = (int) (event.getY() - touchY);
scrollTo(getScrollX(), getScrollY() - dy);
touchY = event.getY();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// click in child rect
Rect rect = new Rect();
child.getGlobalVisibleRect(rect);
if (!XPopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
performClick();
}
} else {
}
if (enableDrag && tracker != null) {
float yVelocity = tracker.getYVelocity();
if (yVelocity > 1500 && !isThreeDrag) {
close();
} else {
finishScroll();
}
// tracker.recycle();
tracker = null;
}
break;
}
return true;
}
private void finishScroll() {
if (enableDrag) {
int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
if (isThreeDrag) {
int per = maxY / 3;
if (getScrollY() > per * 2.5f) {
dy = maxY - getScrollY();
} else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
dy = per * 2 - getScrollY();
} else if (getScrollY() > per) {
dy = per - getScrollY();
} else {
dy = minY - getScrollY();
}
}
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, XPopup.getAnimationDuration());
ViewCompat.postInvalidateOnAnimation(this);
}
}
boolean isScrollUp;
@Override
public void scrollTo(int x, int y) {
if (y > maxY)
y = maxY;
if (y < minY)
y = minY;
float fraction = (y - minY) * 1f / (maxY - minY);
isScrollUp = y > getScrollY();
if (listener != null) {
if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
status = LayoutStatus.Close;
listener.onClose();
} else if (fraction == 1f && status != LayoutStatus.Open) {
status = LayoutStatus.Open;
listener.onOpen();
}
listener.onDrag(y, fraction, isScrollUp);
}
super.scrollTo(x, y);
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isScrollUp = false;
isUserClose = false;
setTranslationY(0);
}
public void open() {
post(new Runnable() {
@Override
public void run() {
int dy = maxY - getScrollY();
smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
status = LayoutStatus.Opening;
}
});
}
public void close() {
isUserClose = true;
post(new Runnable() {
@Override
public void run() {
scroller.abortAnimation();
smoothScroll(minY - getScrollY(), false);
status = LayoutStatus.Closing;
}
});
}
public void smoothScroll(final int dy, final boolean isOpen) {
post(new Runnable() {
@Override
public void run() {
scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? XPopup.getAnimationDuration() : XPopup.getAnimationDuration() * 0.8f));
ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
}
});
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// 必须要取消,否则会导致滑动初次延迟
scroller.abortAnimation();
}
@Override
public void onStopNestedScroll(View target) {
finishScroll();
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0) {
// scroll up
int newY = getScrollY() + dy;
if (newY < maxY) {
// dy不一定能消费完
consumed[1] = dy;
}
scrollTo(getScrollX(), newY);
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
if (isDragging && velocityY < -1500 && !isThreeDrag) {
close();
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return ViewCompat.SCROLL_AXIS_VERTICAL;
}
public void isThreeDrag(boolean isThreeDrag) {
this.isThreeDrag = isThreeDrag;
}
public void enableDrag(boolean enableDrag) {
this.enableDrag = enableDrag;
}
public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
this.dismissOnTouchOutside = dismissOnTouchOutside;
}
private OnCloseListener listener;
public void setOnCloseListener(OnCloseListener listener) {
this.listener = listener;
}
public interface OnCloseListener {
void onClose();
void onDrag(int y, float percent, boolean isScrollUp);
void onOpen();
}
}
19
Source : HorizontalPageScrollView.java
with Apache License 2.0
from Launcher3-dev
with Apache License 2.0
from Launcher3-dev
/**
* Created by yuchuan
* DATE 16/4/5
* TIME 09:37
*/
public clreplaced HorizontalPageScrollView<T extends MenuItem> extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener {
private Context mContext;
private Launcher mLauncher;
private OverScroller mScroller;
public static boolean startTouch = true;
private static final String TAG = "HorizontalScrollView";
/*
* 速度追踪器,主要是为了通过当前滑动速度判断当前滑动是否为fling
*/
private VelocityTracker mVelocityTracker;
/*
* 记录当前屏幕下标,取值范围是:0 到 getMenuItemCount()-1
*/
private int mCurScreen = 0;
/*
* Touch状态值 0:静止 1:滑动
*/
private static final int TOUCH_STATE_REST = 0;
private static final int TOUCH_STATE_SCROLLING = 1;
/*
* 记录当前touch事件状态--滑动(TOUCH_STATE_SCROLLING)、静止(TOUCH_STATE_REST 默认)
*/
private int mTouchState = TOUCH_STATE_REST;
private static final int SNAP_VELOCITY = 300;
/*
* 记录滑动时上次手指所处的位置
*/
private float mLastMotionX;
private float mLastMotionY;
private int mPageCount;
private OnScrollChangedListener mScrollChangedListener = null;
private int mCellViewWidth;
private int mCellViewHeight;
private int mCountX;
/**
* 是否以页滑动
*/
private boolean mPageScroll;
// 是自由滑动还是分页滑动
private boolean mFreeScroll;
// 如果自由滑动失效,如果不是自由滑动,不满一屏的是否居中显示
private boolean mCenterLayout;
public HorizontalPageScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.mContext = context;
mScroller = new OverScroller(mContext);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MenuDeviceProfile, defStyle, 0);
mFreeScroll = ta.getBoolean(R.styleable.MenuDeviceProfile_free_scroll, false);
mCenterLayout = ta.getBoolean(R.styleable.MenuDeviceProfile_center_layout, true);
mCountX = ta.getInteger(R.styleable.MenuDeviceProfile_menu_numColumns, 4);
ta.recycle();
mLauncher = Launcher.getLauncher(context);
}
public HorizontalPageScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalPageScrollView(Context context) {
this(context, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight());
int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
mCellViewWidth = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
mCellViewHeight = DeviceProfile.calculateCellHeight(childHeightSize, 1);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
measureChild(child);
}
}
private void measureChild(View child) {
final DeviceProfile profile = Launcher.getLauncher(mContext).getDeviceProfile();
LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.width = mCellViewWidth;
lp.height = mCellViewHeight;
int cHeight = getCellContentHeight(profile);
int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
int cellPaddingX = (int) (profile.edgeMarginPx / 2f);
child.setPadding(cellPaddingX, cellPaddingY, cellPaddingX, 0);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
int getCellContentHeight(DeviceProfile profile) {
return Math.min(getMeasuredHeight(), profile.getCellHeight(CellLayout.HOTSEAT));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int top = getPaddingTop();
mPageCount = getPageCount();
if (!mFreeScroll) {
if (mPageCount > 1) {
int left = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
if (i % mCountX == 0) {
left += getPaddingLeft();
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(left, top, left + lp.width, top + lp.height);
left += mCellViewWidth;
if (i % mCountX == (mCountX - 1)) {
left += getPaddingRight();
}
}
} else {
int left = getPaddingLeft();
if (mCenterLayout) {
left = getWidth() / 2 - mCellViewWidth * childCount / 2;
}
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(left, top, left + lp.width, top + lp.height);
left += lp.width;
}
}
} else {
int left = getPaddingLeft();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(left, top, left + lp.width, top + lp.width);
left += lp.width;
}
}
}
public void setAdapter(IMenuAdapter adapter) {
removeAllViews();
resetPage();
adapter.setContainer(this);
int N = adapter.getMenuItemCount();
for (int i = 0; i < N; i++) {
View view = adapter.getChildView(i, null, this);
addView(view, i);
startLayoutAnimation();
}
}
private int getPageColumn() {
int maxWidth = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
maxWidth = Math.max(mCellViewWidth, maxWidth);
}
return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / (maxWidth);
}
/**
* 是否满屏
*/
private boolean childIsFull() {
return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() < getAllChildLength();
}
private int getAllChildLength() {
int column = mCountX;
int childVisibleCount = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
childVisibleCount++;
}
return (int) Math.ceil(childVisibleCount * (1.0f * getWidth() / column));
}
// 计算最左边的child的左边位置,如果没有满一屏则所有居中
private int computeLayoutLeftPoint() {
boolean isFull = childIsFull();
if (isFull) {
return getPaddingLeft();
} else {
int width = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
width += child.getPaddingLeft() + child.getPaddingRight() + child.getMeasuredWidth();
}
if (getChildCount() > 0) {
width += (getChildCount() - 1);
}
return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - width) / 2;
}
}
// 获取页数
public int getPageCount() {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams params = (LayoutParams) child.getLayoutParams();
if (params.width == LayoutParams.MATCH_PARENT) {
return childCount;
}
}
return (int) Math.ceil(1.0 * getAllChildLength() / getWidth());
}
public int getAllPageCount() {
return mPageCount;
}
public void setSupportPageScroll(boolean pageScroll) {
this.mPageScroll = pageScroll;
}
public boolean getSupportPageScroll() {
return mPageScroll;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
final int action = event.getAction();
final float x = event.getX();
switch(action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionX = x;
break;
case MotionEvent.ACTION_MOVE:
// 横向滑动距离
int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
scrollBy(deltaX, 0);
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
int velocityX = (int) velocityTracker.getXVelocity();
if (!mFreeScroll) {
if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
// Fling enough to move left
int page = mCurScreen - 1;
page = page > 0 ? page : 0;
snapToScreen(page);
} else if (velocityX < -SNAP_VELOCITY && mCurScreen < mPageCount) {
// Fling enough to move right
int page = mCurScreen + 1;
page = page < mPageCount ? page : mPageCount - 1;
snapToScreen(page);
} else {
snapToDestination();
}
} else {
View view = getChildAt(getChildCount() - 1);
if (view == null)
return true;
int end = view.getRight() - getWidth();
end = Math.max(0, end);
float minVelocity = ViewConfiguration.getMinimumFlingVelocity();
float maxVelocity = ViewConfiguration.getMaximumFlingVelocity();
final float step = 0.2f;
if (velocityX > SNAP_VELOCITY) {
// F;ling enough to move left
int delta;
if (getScrollX() > -getWidth() && getScrollX() <= 0) {
delta = -getScrollX();
} else {
float velocity = (velocityX - minVelocity);
float moveLength = velocity * step;
if (moveLength > getScrollX()) {
delta = -getScrollX();
} else {
delta = -(int) moveLength;
}
}
mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
// Redraw the layout
invalidate();
} else if (velocityX < -SNAP_VELOCITY) {
int delta;
if (end - getScrollX() < getWidth()) {
delta = end - getScrollX();
} else {
float velocity = (minVelocity - velocityX);
float moveLength = velocity * step;
if (moveLength > (end - getScrollX())) {
delta = end - getScrollX();
} else {
delta = (int) moveLength;
}
}
mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
// Redraw the layout
invalidate();
} else {
int length = 0;
if (getScrollX() > end) {
length = end - getScrollX();
} else if (getScrollX() < 0) {
length = -getScrollX();
}
mScroller.startScroll(getScrollX(), 0, length, 0, 300);
// Redraw the layout
invalidate();
}
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
}
return true;
}
/**
* 方法名称:snapToDestination 方法描述:根据当前位置滑动到相应界面
*/
public void snapToDestination() {
final int screenWidth = getWidth();
final int destScreen = (int) Math.ceil(1.0f * getScrollX() / screenWidth);
snapToScreen(destScreen < mPageCount ? destScreen : mPageCount - 1);
}
/**
* 方法名称:snapToScreen 方法描述:滑动到到第whichScreen(从0开始)个界面,有过渡效果
*
* @param whichScreen
*/
public void snapToScreen(int whichScreen) {
// get the valid layout effect_page
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
if (getScrollX() != (whichScreen * getWidth())) {
final int delta = whichScreen * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
notifyScrollChanged(whichScreen, mCurScreen);
mCurScreen = whichScreen;
// Redraw the layout
invalidate();
}
}
/**
* 当滑动切换界面时执行相应操作
*
* @param newPage 目标页面
* @param oldPage 要离开的页面
*/
private void notifyScrollChanged(int newPage, int oldPage) {
if (mScrollChangedListener != null) {
mScrollChangedListener.onScrollChanged(newPage, oldPage);
int allPageCount = getPageCount();
mScrollChangedListener.onPagePositionChanged(newPage, allPageCount);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch(action) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(mLastMotionX - x);
if (xDiff > ViewConfiguration.getTouchSlop()) {
if (Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1) {
mTouchState = TOUCH_STATE_SCROLLING;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchState = TOUCH_STATE_REST;
break;
}
return mTouchState != TOUCH_STATE_REST;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
int x = mScroller.getCurrX();
scrollTo(x, 0);
postInvalidate();
}
}
/**
* 是否滑动到了结尾
*
* @param distanceX 滑动距离
*
* @return
*/
private boolean isEnd(float distanceX) {
int fastLeft = getPaddingLeft();
View view = getChildAt(getChildCount() - 1);
int lastRight = view.getRight() - getWidth();
int mScrollX = 0;
return (distanceX > 0 && lastRight < mScrollX) || (distanceX < 0 && fastLeft > mScrollX);
}
public int getCurrentPage() {
return mCurScreen;
}
public void setOnScrollChangeListener(OnScrollChangedListener onScrollChangeListener) {
this.mScrollChangedListener = onScrollChangeListener;
}
@Override
public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target, LauncherLogProto.Target targetParent) {
}
@Override
public void onClick(View v) {
}
@Override
public boolean onLongClick(View v) {
return false;
}
@Override
public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
// if (!success || (target != mLauncher.getWorkspace() &&
// !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
// // Exit spring loaded mode if we have not successfully dropped or have not handled the
// // drop in Workspace
// mLauncher.exitSpringLoadedDragModeDelayed(true,
// Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
// }
// mLauncher.unlockScreenOrientation(false);
if (!success) {
d.deferDragViewCleanupPostAnimation = false;
}
}
public interface OnScrollChangedListener {
void onScrollChanged(int newPage, int oldPage);
void onPagePositionChanged(int currentPage, int allPage);
}
public void resetPage() {
scrollTo(0, 0);
mCurScreen = 0;
}
/**
* 设置等距布局
*
* @param equidistant 是否等距
*/
public void setEquidistantLayout(boolean equidistant) {
mFreeScroll = equidistant;
requestLayout();
}
/**
* 是否等距布局
*
* @return
*/
public boolean hasEquidistantLayout() {
return mFreeScroll;
}
}
19
Source : NestedScrollWebView.java
with GNU General Public License v3.0
from KnIfER
with GNU General Public License v3.0
from KnIfER
public clreplaced NestedScrollWebView extends WebViewmy implements NestedScrollingChild3 {
private static final String TAG = NestedScrollWebView.clreplaced.getSimpleName();
private final int[] mScrollConsumed = new int[2];
private final int[] mScrollOffset = new int[2];
private int mLastMotionY;
private VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
private int mMaximumVelocity;
private OverScroller mScroller;
private int mLastScrollerY;
private final NestedScrollingChildHelper mChildHelper;
public NestedScrollWebView(Context context) {
this(context, null);
}
public NestedScrollWebView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
mScroller = new OverScroller(getContext());
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
public void fling(int velocityY) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
// start
mScroller.fling(// start
getScrollX(), // start
getScrollY(), // velocities
0, // velocities
velocityY, // x
0, // x
0, // y
Integer.MIN_VALUE, // y
Integer.MAX_VALUE, 0, // overscroll
0);
mLastScrollerY = getScrollY();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
Log.d(TAG, "computeScroll: y : " + y);
int dy = y - mLastScrollerY;
if (dy != 0) {
int scrollY = getScrollY();
int dyUnConsumed = 0;
int consumedY = dy;
if (scrollY == 0) {
dyUnConsumed = dy;
consumedY = 0;
} else if (scrollY + dy < 0) {
dyUnConsumed = dy + scrollY;
consumedY = -scrollY;
}
dispatchNestedScroll(0, consumedY, 0, dyUnConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
}
// Finally update the scroll positions and post an invalidation
mLastScrollerY = y;
ViewCompat.postInvalidateOnAnimation(this);
} else {
// We can't scroll any more, so stop any indirect scrolling
if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
// and reset the scroller y
mLastScrollerY = 0;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(event);
final int actionMasked = event.getAction();
switch(actionMasked) {
case MotionEvent.ACTION_DOWN:
mLastMotionY = (int) event.getRawY();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
mVelocityTracker.addMovement(vtev);
mScroller.computeScrollOffset();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity();
if (Math.abs(initialVelocity) > mMinimumVelocity) {
fling(-initialVelocity);
}
case MotionEvent.ACTION_CANCEL:
stopNestedScroll();
recycleVelocityTracker();
break;
case MotionEvent.ACTION_MOVE:
final int y = (int) event.getRawY();
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
Log.d(TAG, "onTouchEvent: deltaY : " + deltaY + " , mScrollConsumedY : " + mScrollConsumed[1] + " , mScrollOffset : " + mScrollOffset[1]);
vtev.offsetLocation(0, mScrollConsumed[1]);
}
mLastMotionY = y;
int scrollY = getScrollY();
int dyUnconsumed = 0;
if (scrollY == 0) {
dyUnconsumed = deltaY;
} else if (scrollY + deltaY < 0) {
dyUnconsumed = deltaY + scrollY;
vtev.offsetLocation(0, -dyUnconsumed);
}
mVelocityTracker.addMovement(vtev);
dispatchNestedScroll(0, deltaY - dyUnconsumed, 0, dyUnconsumed, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
default:
break;
}
super.onTouchEvent(vtev);
return true;
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean hasNestedScrollingParent(int type) {
return mChildHelper.hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
}
See More Examples