自定义ScollerView,理解滑动动画与自定义view的原理

来源:互联网 发布:js基本数据类型 编辑:程序博客网 时间:2024/06/05 11:06
package com.itheima.myscrollview28;
 
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* 自定义一个MyScrollView继承ViewGroup类,可以实现视图滚动的效果,只不过系统控件中ViewPager类也可以实现该效果
* 这里只是要理解系统中滚动视图动画的原理
* @author 丁空华
* 2015-3-5下午3:39:16
*/
public class MyScrollView extends ViewGroup{
private Context ctx;//上下文
//private MyScroller myScroller;//计算位移的工具类,这是自己定义的,帮助理解滑动动画的原理
private Scroller myScroller;//系统计算位移的工具类
private GestureDetector detector;//手势识别的工具类
private int firstX = 0;//down 事件时的x坐标
private int currId = 0;//当前的ID值,显示在屏幕上的子View的下标
private MyPageChangedListener pageChangedListener;
protected boolean isFling;//判断是否发生快速滑动的变量,true表示快速滑动,反之相反
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
this.ctx = context;
initView();
}
/**
* 初始化控件,实例化变量
*/
private void initView() {
//myScroller = new MyScroller(ctx);
myScroller = new Scroller(ctx);
//给手势识别器定义手势识别的监听器
detector = new GestureDetector(ctx, new OnGestureListener() {
/**
* 手指离开屏幕的事件
*/
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/**
* 手指在屏幕上按下的事件
*/
@Override
public void onShowPress(MotionEvent e) {
}
/**
* 手指在屏幕上滑动的事件
* @param e1 滑动事件开始时的事件对象
* @param e2 滑动时的事件对象
* @param distanceX 横坐标滑动的距离
* @param distanceY 纵坐标滑动的距离
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//设置滑动的距离,该方法会调用scrollTo(mScrollX + x, mScrollY + y)方法,设置滚动的坐标
scrollBy((int) distanceX, 0);
return false;
}
/**
* 手指在屏幕上长按下的事件
*/
@Override
public void onLongPress(MotionEvent e) {
}
/**
* 手指在屏幕上发生快速滑动的事件
* @param e1 滑动事件开始时的事件对象
* @param e2 滑动时的事件对象
* @param velocityX 横坐标滑动的距离
* @param velocityY 纵坐标滑动的距离
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
//将控制是否快速滑动的变量至为true
isFling = true;
if(velocityX>0 && currId>0){ // 快速向右滑动,要将显示的子View对象的表示值currId减1
currId--;
}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑动,要将显示的子View对象的表示值currId加1
currId++;
}
moveToDest(currId);
return false;
}
/**
* 手指在屏幕上按下的事件
*/
@Override
public boolean onDown(MotionEvent e) {
return false;
}
});
}
/**
* 计算 控件大小,
* 做为viewGroup 还有一个责任,计算 子view的大小,否则只会显示该父组件而不会显示其子控件
* 若是在该控件中显示出一个ViewGroup对象,那么该ViewGroup还有责任计算该ViewGroup中的子View控件的宽和高
*
* 由于每一个子View的宽高是由父View决定的,会根据子View的宽高模式来给定子View的宽高
* MeasureSpec.UNSPECIFIED 该模式父类不会设置子类的宽高,根据onMeasure(widthMeasureSpec, heightMeasureSpec)方法传递的值来确定
* MeasureSpec.AT_MOST 和MeasureSpec.EXACTLY 这两种模式会限制子View的宽高,分别是最大和精确
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
v.measure(widthMeasureSpec, heightMeasureSpec);
//v.getMeasuredWidth() // 得到测量的大小
}
}
@Override
/**
* 对子view进行布局,确定子view的位置
* changed变量 若为true ,说明布局发生了变化
* l\t\r\b\ 是指当前viewgroup 在其父view中的位置
*/
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
// 取得下标为i的子view
View view = getChildAt(i);
//父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小)
//指定子view的位置 , 左,上,右,下,是指在viewGroup坐标系中的位置
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());
 
}
}
/**
* 给该控件设置触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//将事件对象传递给手势识别器,手势识别器会走动解析该事件
detector.onTouchEvent(event);
//添加自己的事件解析
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if(!isFling){// 在没有发生快速滑动的时候,才执行按位置判断currid
int nextId = 0;
if(event.getX()-firstX>getWidth()/2){ // 手指向右滑动,超过屏幕的1/2 当前的currid - 1
nextId = currId-1;
}else if(firstX - event.getX()>getWidth()/2){ // 手指向左滑动,超过屏幕的1/2 当前的currid + 1
nextId = currId+1;
}else{
nextId = currId;
}
//移动到指定的屏幕上
moveToDest(nextId);
}
//将快速滑动的变量至为false
isFling = false;
break;
}
return true;
}
 
/**
* 移动到指定的屏幕上
* @param nextId屏幕 的下标
*/
public void moveToDest(int nextId) {
//对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1
currId = (nextId>=0)?nextId:0;
currId = (nextId<=getChildCount()-1)?nextId:(getChildCount()-1);
//瞬间移动
//scrollTo(currId*getWidth(), 0);
//触发listener事件
if(pageChangedListener!=null){
pageChangedListener.moveToDest(currId);
}
//计算出移动的距离 要移动的距离 = 最终的位置 - 现在的位置
int distance = currId*getWidth() - getScrollX();
//调用系统图的Scoller设置滑动运行的时间
myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance));
//刷新当前view,该方法会调用onDraw()方法 的执行
invalidate();
}
/**
* invalidate(); 会导致 computeScroll()这个方法的执行,执行完这个方法之后才会执行onDraw()方法
* 计算滚动的坐标
*/
@Override
public void computeScroll() {
if(myScroller.computeScrollOffset()){//计算动画偏移量,若是返回true的话动画会一直执行
//获取需要滑动的距离
int newX = (int) myScroller.getCurrX();
//设置滑动的坐标
scrollTo(newX, 0);
//滑动完之后再重新调用invalidate()方法,invalidate()方法又继续调用该方法,又去computeScrollOffset()计算偏移量,
//一直循环下去直到偏移量computeScrollOffset()返回为false,此时偏移量应该是0
invalidate();
};
}
 
 
/**
* 页面更改时的监听接口
*/
public interface MyPageChangedListener{
void moveToDest(int currid);
}
public MyPageChangedListener getPageChangedListener() {
return pageChangedListener;
}
public void setPageChangedListener(MyPageChangedListener pageChangedListener) {
this.pageChangedListener = pageChangedListener;
}
}

自定义MyScrolle类,理解系统类Scoller和动画滑动的原理
package com.itheima.myscrollview28;
 
import android.content.Context;
import android.os.SystemClock;
/**
* 计算位移的工具类
* @author 丁空华
* 2015-3-5下午3:40:56
*/
public class MyScroller {
 
private int startX;
private int startY;
private int distanceX;
private int distanceY;
//开始执行动画的时间
private long startTime;
//判断是否正在执行动画,true 是还在运行,false 已经停止
private boolean isFinish;
//默认动画运行的时长,单位为毫秒值
private int duration = 500;
//当前的横坐标
private long currX;
//当前的纵坐标
private long currY;
/**
* 无参数构造方法
* @param ctx 上下文
*/
public MyScroller(Context ctx){
}
 
/**
* 初始化下面的值,并设置isFinish变量为false
* @param startX开始时的X坐标
* @param startY开始时的Y坐标
* @param disXX方向 要移动的距离
* @param disYY方向 要移动的距离
*/
public void startScroll(int startX, int startY, int disX, int disY) {
this.startX = startX;
this.startY = startY;
this.distanceX = disX;
this.distanceY = disY;
this.startTime = SystemClock.uptimeMillis();//获取动画开始的时间
this.isFinish = false;
}
 
/**
* 计算一下当前的运行状况,也就是计算动画的偏移量
* 根据滑动的快慢计算出动画的滑动效果,若是滑动时长在duration设置的范围之内则为快速滑动,否则为慢速滑动
* return true 还在运行动画
* return false 运行结束动画
*/
public boolean computeScrollOffset() {
//判断动画是否在运行
if (isFinish) {
return false;
}
 
// 获得动画运行所用的时间
long passTime = SystemClock.uptimeMillis() - startTime;
 
if (passTime < duration) {// 如果滑动时间还在duration允许的范围内则定义为慢速滑动
// 当前的坐标位置 = 开始的位置 + 移动的距离(距离 = 速度*时间)
currX = startX + distanceX * passTime / duration;
currY = startY + distanceY * passTime / duration;
 
} else {//快速滑动
currX = startX + distanceX;
currY = startY + distanceY;
//设置决定滑动快慢的变量为true
isFinish = true;
}
 
return true;
}
 
public long getCurrX() {
return currX;
}
 
public void setCurrX(long currX) {
this.currX = currX;
}
}
0 0