利用ScrollView实现阻尼弹簧自定义控件

应该注意的问题:

(1)继承ScrollView来实现自定义滚动动画(实际是对自定义ScrollView内部的子View或子ViewGroup实现动画效果,因此不能破坏掉ScrollView本身已经有的滚动动画)时,应注意不能使用Scroller,也不能使用scrollTo()/scrollBy()等实现动画,因为ScrollView中已经自己实现了computeScroll(),为了不破坏ScrollView本身的滚动行为,所以,最好不要使用Scroller,而且也不要重写computeScroll()函数,并且不适用scrollTo()/scrollBy()函数;可以直接对要做动画的View/ViewGroup及其子类使用布局函数xxx.layout(changed, left, top, right, bottom)和Animation来实现动画。

(2)直接继承View 或ViewGroup等一般的View及其子类来实现动画时,一般是使用Scroller配合重写View的computeScroll(),并且调用scrollTo()或scrollBy()来实现动画;除此之外,都可以使用Animation和view的参数来实现动画;必须都要重写View的onTouchEvent()方法来捕捉手势。

(3)源代码

package com.xunlei.downloadprovider.member.login.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;

import com.xunlei.downloadprovider.androidutil.AndroidConfig;
import com.xunlei.downloadprovider.androidutil.XLLog;

/**
 * 增加ScrollView控件的弹簧效果,用于个人空间背景
 * @author GeXianglin
 * @since 手雷2.7
 */
public class SpringScrollView extends ScrollView {
	private final String TAG = getClass().getSimpleName();
	
	private Context mContext;
	private View mChild = null;
	
	private int mInitTop = 0;	// 第一个动画View初始top位置
	private int mInitBottom;	// 第一个动画View初始bottom位置
	private int mCurrentTop;	// 第一个动画View当前top位置
	private int mCurrentBottom;	// 第一个动画View当前bottom位置
	
	private int mInitTop2 = 0;
	private int mInitBottom2 = 0;
	private int mCurrentTop2;
	private int mCurrentBottom2;
	
	private float mLastDownY = 0f;	// 触摸DOWN时记录Y坐标
	
	/** 
	 * 标记第一次进入OnTouchEvent,为了处理ScrollView手指滑动的时候
	 * 并不进入ACTION_DOWN,为了获取ACTION_MOVE之前手指按下时的坐标
	 */
	private boolean isFirst = true;			// 标记:ScrollView是否处于最顶部
	private boolean mIsTop = false;			// 标记:ScrollView是否在最顶端
	private boolean mIsDownSlide = false;	// 标记:向下滑动手势
	private final int MOVE_FACTOR = 6;		// 影响滑动速度的自定义除数因子(为了用户体验)
	
	// 移动的View
	private View mNeedMoveUpView;	//第一个移动的View
	private View mNeedMoveDownView;	// 第二个移动的View,第二个View的移动速度是第一个View速度的2倍(用户体验)
	
	public SpringScrollView(Context context) {
		super(context);
		mContext = context;
	}
	
	public SpringScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;
	}
	
	public SpringScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		mContext = context;
	}
	
	/**
     * Finalize inflating a view from XML. This is called as the last phase of 
     * inflation, after all child views have been added.
     */
	@Override
	public void onFinishInflate() {
		init();
    }
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// 这是因为ACTION_DOWN和子View的OnClick有冲突,ACTION_DOWN进入不了
		if (getScrollY() == 0 && isFirst) {	// 当前滚动条在最顶部且是首次进入TouchEvent事件循环
			mInitTop = mNeedMoveUpView.getTop();
			mInitBottom = mNeedMoveUpView.getBottom();
			
			mLastDownY = event.getY();	// 获得手指按下的Y坐标
			
			if (mNeedMoveDownView != null) {
				mInitTop2 = mNeedMoveDownView.getTop();
				mInitBottom2 = mNeedMoveDownView.getBottom();
			}
		}
		
		int action = event.getAction();
		switch (action & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
			mIsTop = false;
			if (getScrollY() == 0) {	// 滑动到顶部了
				mLastDownY = event.getY();
				/*mLastDownX = event.getX();*/
				mInitTop = mNeedMoveUpView.getTop();
				mInitBottom = mNeedMoveUpView.getBottom();
				mIsTop = true;
				
				if (mNeedMoveDownView != null) {
					mInitTop2 = mNeedMoveDownView.getTop();
					mInitBottom2 = mNeedMoveDownView.getBottom();
				}
			}
			
			isFirst = false;
			break;
		
		case MotionEvent.ACTION_MOVE:
			int distanceY = (int) (event.getY() - mLastDownY);
			
			if (distanceY > 0 && mIsTop) {
				mIsDownSlide = true;	// 标记是向下滑动手势
				
				mCurrentTop = mInitTop + distanceY / MOVE_FACTOR;
				mCurrentBottom = mInitBottom + distanceY / MOVE_FACTOR;
				mNeedMoveUpView.layout(mNeedMoveUpView.getLeft(), mCurrentTop, 
						mNeedMoveUpView.getRight(), mCurrentBottom);
				
				mCurrentTop2 = mInitTop2 + 2 * distanceY / MOVE_FACTOR;
				mCurrentBottom2 = mInitBottom2 + 2 * distanceY / MOVE_FACTOR;
				mNeedMoveDownView.layout(mNeedMoveDownView.getLeft(), mCurrentTop2, 
						mNeedMoveDownView.getRight(), mCurrentBottom2);
			} else if (distanceY > 0 && !mIsTop) {
				mIsTop = true;
				mIsDownSlide = true;	// 标记是向下滑动手势
				
				mCurrentTop = mInitTop + distanceY / MOVE_FACTOR;
				mCurrentBottom = mInitBottom + distanceY / MOVE_FACTOR;
				mNeedMoveUpView.layout(mNeedMoveUpView.getLeft(), mCurrentTop, 
						mNeedMoveUpView.getRight(), mCurrentBottom);
				
				mCurrentTop2 = mInitTop2 + 2 * distanceY / MOVE_FACTOR;
				mCurrentBottom2 = mInitBottom2 + 2 * distanceY / MOVE_FACTOR;
				mNeedMoveDownView.layout(mNeedMoveDownView.getLeft(), mCurrentTop2, 
						mNeedMoveDownView.getRight(), mCurrentBottom2);
			} else if (distanceY <= 0 && !canScrollToUp()) {
				/**
				 * 处理由于第1个View隐藏的部分,导致ScrollView滚动。
				 * 手指向上滑动,且如果不能向上滑动,因为弹簧效果的原理是,让第一个View的顶部坐标
				 * (android:marginTop是负值)超出屏幕的顶部,且让第一个View的底部坐标(
				 * android:layout_marginBottom为负值)超过第二个View。因此有可能导致在界面上看起来
				 * 并没有超过一屏幕的View,但是却可以向上滚动,这里就是为了处理这个的,重写了判断滚动的
				 * 函数让其不滚动。
				 */
				isFirst = false;
				mIsDownSlide = false;
				return true;	//
			}
			
			isFirst = false;
			break;
		
		/*case MotionEvent.ACTION_CANCEL:*/
		case MotionEvent.ACTION_UP:
			if (mIsTop && mIsDownSlide) {
				mIsTop = false;
				mIsDownSlide = false;
				bounceToBack();
			} else if (mIsTop && !mIsDownSlide) {	// 处理超出滑动
				mIsTop = false;
				mIsDownSlide = false;
				isFirst = true;
				return true;
			}
			
			isFirst = true;
			break;
		
		default:
			break;
		}
		
		return super.onTouchEvent(event);
	}
	
	/**
	 * 获得唯一的子View,因为是继承ScrollView(ScrollView只能有一个子View或ViewGroup)
	 */
	private void init() {
		if (getChildCount() > 0) {
			mChild = getChildAt(0);	// 获得唯一的子View(一般是一个XxxLayout)
		}
	}
	
	/**
	 * 指定第二个动画View
	 */
	public void setMoveDownView(View v) {
		mNeedMoveDownView = v;
	}
	
	/**
	 * 指定第一个动画View
	 */
	public void setMoveUpView(View v) {
		mNeedMoveUpView = v;
	}
	
	/**
	 * 判断是否可以向上滚动
	 * 只有在当前View的高度大于屏幕高度的时候,才允许滚动。
	 */
	private boolean canScrollToUp() {
		if (mChild != null) {
			if (mChild.getHeight() > AndroidConfig.getScreenHeight()) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 回弹动画
	 */
	private void bounceToBack() {
		/**
		 * TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
		 * float fromXDelta,这个参数表示动画开始的点离当前View X坐标上的差值;
		 * float toXDelta,这个参数表示动画结束的点离当前View X坐标上的差值;
		 * float fromYDelta,这个参数表示动画开始的点离当前View Y坐标上的差值;
		 * float toYDelta,这个参数表示动画结束的点离当前View Y坐标上的差值;
		 */
		TranslateAnimation animation = new TranslateAnimation(0, 0, mCurrentTop - mInitTop, 0);
		animation.setInterpolator(mContext, android.R.anim.decelerate_interpolator);
		animation.setDuration(200);
		
		TranslateAnimation animation2 = new TranslateAnimation(0, 0, mCurrentTop2 - mInitTop2, 0);
		animation2.setInterpolator(mContext, android.R.anim.decelerate_interpolator);
		animation2.setDuration(200);
		
		mNeedMoveUpView.startAnimation(animation);
		
		// 这是因为Animation的动画并不真正的改变View的实际坐标,仅仅是一个动画,所以需要最后设定下布局
		mNeedMoveUpView.layout(mNeedMoveUpView.getLeft(), mInitTop, mNeedMoveUpView.getRight(), mInitBottom);
		
		mNeedMoveDownView.startAnimation(animation2);
		mNeedMoveDownView.layout(mNeedMoveDownView.getLeft(), mInitTop2, mNeedMoveDownView.getRight(),
				mInitBottom2);
	}
}




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值