使用自定义的ScrollView实现带弹性和刷新功能
定义接口实现刷新的功能
1、OverScrollView类
import android.content.Context;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
/**
* 具有上下弹性滚动的ScrollView<br><br>
* <strong>策略:</strong> 获取ScrollView的子视图并添加到自定义的{@link OverScrollWarpLayout}滚动视图中
* 将滚动视图添加到ScrollView作为子视图,所有的弹性滚动都由{@link OverScrollWarpLayout}来完成
*
* @author King
*/
public class OverScrollView extends ScrollView {
/**
* 滚动系数, 视图滚动距离与手指滑动距离的比值
*/
private static final float ELASTICITY_COEFFICIENT = 0.25f;
/**
* 无弹性滚动状态
*/
private static final int NO_OVERSCROLL_STATE = 0;
/**
* 上方弹性滚动状态
*/
private static final int TOP_OVERSCROLL_STATE = 1;
/**
* 下方弹性滚动状态
*/
private static final int BOTTOM_OVERSCROLL_STATE = 2;
/**
* 滚动最大高度,超过此高度不再滚动
*/
private static final int OVERSCROLL_MAX_HEIGHT = 1200;
/**
* Sentinel value for no current active pointer.
* Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;
/**
* 触发事件的高度默认阀值
*/
private static final int TRIGGER_HEIGHT = 120;
/**
* 弹性滚动状态
*/
private int overScrollSate;
/**
* 属性标志位:是否可以弹性滚动
*/
private boolean mIsUseOverScroll = true;
/**
* 是否标记可以滚动
*/
private boolean isRecord;
/**
* 自定义的弹性滚动视图
*/
private OverScrollWarpLayout mContentLayout;
/**
* OverScroll监听器
*/
private OverScrollListener mOverScrollListener;
/**
* OverScroll细致监听器
*/
private OverScrollTinyListener mOverScrollTinyListener;
/**
* Scroll细致监听器
*/
private OnScrollListener mScrollListener;
/**
* 最新一次的手指触摸位置
*/
private float mLastMotionY;
/**
* 弹性滚动总距离,向下为负,向上为正
*/
private int overScrollDistance;
/**
* 按在屏幕上的手指的id
*/
private int mActivePointerId = INVALID_POINTER;
/**
* 是否触摸
*/
private boolean isOnTouch;
/**
* 是否具有惯性
*/
private boolean isInertance;
/**
* 是否使用惯性
*/
private boolean mIsUseInertance = true;
/**
* 是否禁用快速滚动
*/
private boolean mIsBanQuickScroll;
/**
* 惯性距离(与滑动速率有关)
*/
private int inertanceY;
/**
* 触发事件的高度阀值,最小值为30
*/
private int mOverScrollTrigger = TRIGGER_HEIGHT;
public OverScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initScrollView();
}
public OverScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public OverScrollView(Context context) {
this(context, null);
}
private void initScrollView(){
//设置滚动无阴影( API Level 9 )
if(Build.VERSION.SDK_INT >= 9){
setOverScrollMode(View.OVER_SCROLL_NEVER);
}else{
ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_NEVER);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果禁用,不做任何处理
if(!mIsUseOverScroll){
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isOverScrolled()) {
isRecord = true;
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
}
break;
case MotionEvent.ACTION_MOVE:
if(isRecord && Math.abs(ev.getY() - mLastMotionY) > 20){
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
if(isRecord){
isRecord = false;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
isOnTouch = true;
if(ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL){
if(mOverScrollTinyListener != null){
mOverScrollTinyListener.scrollLoosen();
}
isOnTouch = false;
}
// 如果禁用,不做任何处理
if(!mIsUseOverScroll){
return super.onTouchEvent(ev);
}
if(!isOverScrolled()){
mLastMotionY = (int) ev.getY();
return super.onTouchEvent(ev);
}
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
mLastMotionY = (int) ev.getY();
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);
if(mActivePointerId != INVALID_POINTER){
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
}
break;
case MotionEvent.ACTION_MOVE:
if (isRecord) {
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
break;
}
final float y = ev.getY(activePointerIndex);
// 滚动距离
int deltaY = (int) (mLastMotionY - y);
// 记录新的触摸位置
mLastMotionY = y;
if(Math.abs(overScrollDistance) >= OVERSCROLL_MAX_HEIGHT && overScrollDistance * deltaY > 0){
deltaY = 0;
}
//如果滚动到ScrollView自身滚动边界,直接调用自身滚动
if(overScrollDistance *(overScrollDistance + deltaY) < 0){
mContentLayout.smoothScrollToNormal();
overScrollDistance = 0;
break;
}
// 如果处于ScrollView滚动状态,直接调用ScrollView自身滚动
if((!isOnBottom() && overScrollDistance > 0) || (!isOnTop() && overScrollDistance < 0)){
mContentLayout.smoothScrollToNormal();
overScrollDistance = 0;
break;
}
if(overScrollDistance * deltaY > 0){
deltaY = (int) (deltaY * ELASTICITY_COEFFICIENT);
}
if(overScrollDistance == 0){
deltaY = (int) (deltaY * ELASTICITY_COEFFICIENT * 0.5f);
}
if(overScrollDistance == 0 && deltaY == 0){
break;
}
//检测最终滚动距离,最大为20
if(Math.abs(deltaY) > 20){
deltaY = deltaY > 0 ? 20 : -20;
}
// 记录滚动总距离
overScrollDistance += deltaY;
if(isOnTop() && overScrollDistance > 0 && !isOnBottom()){
overScrollDistance = 0;
break;
}
if(isOnBottom() && overScrollDistance < 0 && !isOnTop()){
overScrollDistance = 0;
break;
}
// 滚动视图
mContentLayout.smoothScrollBy(0, deltaY);
if(mOverScrollTinyListener != null){
mOverScrollTinyListener.scrollDistance(deltaY, overScrollDistance);
}
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mContentLayout.smoothScrollToNormal();
overScrollTrigger();
// 重置滑动总距离
overScrollDistance = 0;
// 重置标记
isRecord = false;
// 重置手指触摸id
mActivePointerId = INVALID_POINTER;
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 功能描述: 防止出现pointerIndex out of range异常<br>
*
* @param ev
*/
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) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
public boolean isOverScrolled() {
return isOnTop() || isOnBottom();
}
private boolean isOnTop(){
return getScrollY() == 0;
}
private boolean isOnBottom(){
return getScrollY() + getHeight() == mContentLayout.getHeight();
}
/**
* 功能描述:初始化滚动视图 <br>
* <br>
* <strong>策略:</strong> 获取ScrollView的子视图并添加到自定义的{@link OverScrollWarpLayout}滚动视图中
* 将滚动视图添加到ScrollView作为子视图
*/
private void initOverScrollLayout() {
//必须设置为true,否则添加子视图时高度不会填充到整个ScrollView的高度
setFillViewport(true);
if(mContentLayout == null){
// 获取ScrollView的子视图
View child = getChildAt(0);
// 初始化弹性滚动视图
mContentLayout = new OverScrollWarpLayout(getContext());
// 移除ScrollView所有视图
this.removeAllViews();
// 将原先ScrollView子视图加入到弹性滚动视图中
mContentLayout.addView(child);
// 添加弹性滚动视图,作为ScrollView子视图
this.addView(mContentLayout,new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
// mIsUseOverScroll = true;
}
/**
* 功能描述:设置是否可以弹性滚动 <br>
*
* @param isOverScroll
*/
public void setOverScroll(boolean isOverScroll){
mIsUseOverScroll = isOverScroll;
}
/**
* 功能描述: 设置是否使用惯性<br>
*
* @param isInertance
*/
public void setUseInertance(boolean isInertance){
mIsUseInertance = isInertance;
}
@Override
protected void onAttachedToWindow() {
initOverScrollLayout();
super.onAttachedToWindow();
}
/**
* 功能描述: 获取弹性状态<br>
*
* @return
*/
public int getScrollState(){
invalidateState();
return overScrollSate;
}
/**
* 功能描述: 刷新弹性滚动状态<br>
*/
private void invalidateState(){
if(mContentLayout.getScrollerCurrY() == 0){
overScrollSate = NO_OVERSCROLL_STATE;
}
if(mContentLayout.getScrollerCurrY() < 0){
overScrollSate = TOP_OVERSCROLL_STATE;
}
if(mContentLayout.getScrollerCurrY() > 0){
overScrollSate = BOTTOM_OVERSCROLL_STATE;
}
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX,
int scrollY, int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
// Log.v("test", "deltaY "+deltaY+" scrollY "+scrollY);
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if(mScrollListener != null && overScrollDistance == 0){
mScrollListener.onScroll(l, t, oldl, oldt);
}
super.onScrollChanged(l, t, oldl, oldt);
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
if(mIsUseInertance && !isInertance && scrollY != 0){
isInertance = true;
}
if(clampedY && !isOnTouch && isInertance){
mContentLayout.smoothScrollBy(0, inertanceY);
mContentLayout.smoothScrollToNormal();
inertanceY = 0;
}
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
/**
* 功能描述: 设置OverScroll滚动监听器<br>
*
* @param listener
*/
public void setOverScrollListener(OverScrollListener listener){
mOverScrollListener = listener;
}
/**
* 功能描述: 设置OverScroll滚动监听器<br>
*
* @param listener
*/
public void setOverScrollTinyListener(OverScrollTinyListener listener){
mOverScrollTinyListener = listener;
}
/**
* 功能描述: 设置Scroll滚动监听器<br>
*
* @param listener
*/
public void setOnScrollListener(OnScrollListener listener){
mScrollListener = listener;
}
/**
* 设置OverScrollListener出发阀值
* @param height
*/
public void setOverScrollTrigger(int height){
if(height >= 30){
mOverScrollTrigger = height;
}
}
private void overScrollTrigger(){
if(mOverScrollListener == null){
return;
}
if(overScrollDistance > mOverScrollTrigger && isOnBottom()){
mOverScrollListener.footerScroll();
}
if(overScrollDistance < -mOverScrollTrigger && isOnTop()){
mOverScrollListener.headerScroll();
}
}
public void setQuickScroll(boolean isEnable){
mIsBanQuickScroll = !isEnable;
}
@Override
public void computeScroll() {
if(!mIsBanQuickScroll){
super.computeScroll();
}
}
/**
* 获取ScrollView可滚动高度
* @return
*/
public int getScrollHeight(){
return mContentLayout.getHeight() - getHeight();
}
@Override
public void fling(int velocityY) {
inertanceY = 50 * velocityY / 5000;
super.fling(velocityY);
}
/**
* 当OverScroll超出一定值时,调用此监听
*
* @author King
* @since 2014-4-9 下午4:36:29
*/
public interface OverScrollListener {
/**
* 顶部
*/
void headerScroll();
/**
* 底部
*/
void footerScroll();
}
/**
* 每当OverScroll时,都能触发的监听
* @author King
* @since 2014-4-9 下午4:39:06
*/
public interface OverScrollTinyListener{
/**
* 滚动距离
* @param tinyDistance 当前滚动的细小距离
* @param totalDistance 滚动的总距离
*/
void scrollDistance(int tinyDistance, int totalDistance);
/**
* 滚动松开
*/
void scrollLoosen();
}
/**
* 普通滚动监听器<br>
* overScroll距离为0切无惯性时调用
*
* @author king
*
*/
public interface OnScrollListener{
void onScroll(int l, int t, int oldl, int oldt);
}
}
2、OverScrollWarpLayout类
import android.content.Context;
import android.util.AttributeSet;
import android.view.animation.OvershootInterpolator;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class OverScrollWarpLayout extends LinearLayout {
/**
* OvershootInterpolator的弹性系数
*/
private static final float OVERSHOOT_TENSION = 0.75f;
/**
* 平滑滚动器
*/
private Scroller mScroller;
public OverScrollWarpLayout(Context context, AttributeSet attr) {
super(context, attr);
this.setOrientation(LinearLayout.VERTICAL);
// 初始化平滑滚动器
mScroller = new Scroller(getContext(), new OvershootInterpolator(OVERSHOOT_TENSION));
}
public OverScrollWarpLayout(Context context) {
super(context);
this.setOrientation(LinearLayout.VERTICAL);
// 初始化平滑滚动器
mScroller = new Scroller(getContext(), new OvershootInterpolator(OVERSHOOT_TENSION));
}
// 调用此方法滚动到目标位置
public void smoothScrollTo(int fx, int fy) {
int dx = fx - mScroller.getFinalX();
int dy = fy - mScroller.getFinalY();
smoothScrollBy(dx, dy);
}
// 调用此方法设置滚动的相对偏移
public void smoothScrollBy(int dx, int dy) {
// 设置mScroller的滚动偏移量
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
// 这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
invalidate();
}
@Override
public void computeScroll() {
// 先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
// 这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
super.computeScroll();
}
public final void smoothScrollToNormal() {
smoothScrollTo(0, 0);
}
public final int getScrollerCurrY() {
return mScroller.getCurrY();
}
}