自定义控件---------ViewPager的一个小案例

今天做了一个关于VIewPager的小demo        感觉有些难以理解所有在这里分享出来,大家需要的就带走吧大笑


首先说下具体的功能吧

1、几张图片滑动切换   调用系统的api按系统默认的250毫秒滑动切换到下一张,自己写一个匀速切换到下一张(两种)

2、添加导航按钮,滑动到下一张的时候,导航的按钮自动切换到相应的位置上

3、在几张图片当中添加页面(这里添加一个自定义的测试页面)里面用ScrollView来模拟ListView实现效果,下面用listview来解释


大体的功能就是上面的几个吧,但是详细的细节却很重要哦,这个小demo搞了我一天了。。。。。有些地方还不是很理解,这里只代表是我个人的理解,也欢迎各位多提意见,thank!


下面先来看一张最终的效果图:


好了,现在先来说下具体怎么实现的:

这是一个自定的ViewPager:

1、写一个类继承ViewGroup,并实现其两个参数的构造方法

public class MyScorllView extends ViewGroup {

	public MyScorllView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {

	}
  }

2、引入到布局文件中

<com.ittest.MyScorllView
        android:id="@+id/msv"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

3、到对应的Activity类中实例化id,并引入图片资源数组,添加到MyScorllView

public class MainActivity extends Activity {
	private MyScorllView msv;
	
	// 图片资源ID 数组
	private int[] ids = new int[] { R.drawable.a1, R.drawable.a2,
				R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6 };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        msv = (MyScorllView) findViewById(R.id.msv);
        
        //添加6张图片
        for (int i = 0; i < ids.length; i++) {
			ImageView image = new ImageView(this);
			//设置背景资源才能完美的填充到布局中,而src则不能
			image.setBackgroundResource(ids[i]);
			//添加给MyScorllView
			msv.addView(image);
		}
        }
  }


4、指定子view的位置(onLayout()方法)

通过第三步,到这里MyScorllView中已获得了图片的资源

@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		//用for循环动态获取位置
		for (int i = 0; i < getChildCount(); i++) {
			View view = getChildAt(i);
			//给当前view设置布局(左、上、右、下)
			view.layout(0+(i*getWidth()), 0, getWidth()+(i*getWidth()), getHeight());
		}
	}

5、给图片添加滑动事件

detector = new GestureDetector(ctx, new GestureDetector.OnGestureListener() {
			
			@Override
			public boolean onSingleTapUp(MotionEvent e) {
				// TODO Auto-generated method stub
				return false;
			}
			
			@Override
			public void onShowPress(MotionEvent e) {
				// TODO Auto-generated method stub
				
			}
			/**
			 * 正在滑动时回调该方法
			 */
			@Override
			public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
					float distanceY) {
				/**
				 * scrollBy(int i ,int j)方法让当前view的内容发生移动
				 * distanceX x方向移动的距离
				 * distanceY y方向移动的距离
				 */
				scrollBy((int) distanceX, 0);//只关心x方向上的移动
				/**
				 * scrollTo(x,y) 让当前的view的内容移动到某点上
				 * x 目标点的x坐标
				 * y 目标点的y坐标
				 */
				return false;
			}
			
			@Override
			public void onLongPress(MotionEvent e) {
				// TODO Auto-generated method stub
				
			}
			/**
			 * 发生快速滑动时回调此方法
			 */
			@Override
			public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
					float velocityY) {
				isFling = true;
				System.out.println("velocityX:"+velocityX);
				if(velocityX > 0 && currIndex > 0){
					currIndex--;
				}else if(velocityX < 0 && currIndex < getChildCount()-1){
					currIndex++;
				}
				//让view移动到相应的位置上去
				moveToDest(currIndex);
				return false;
			}
			
			@Override
			public boolean onDown(MotionEvent e) {
				// TODO Auto-generated method stub
				return false;
			}
		});
#
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		//用detector来解析正常的滑动点击事件
		detector.onTouchEvent(event);
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			firstX = lastX = (int) event.getX();
			isFling = false;
			break;
		case MotionEvent.ACTION_MOVE:
			break;
		case MotionEvent.ACTION_UP:
			//没有快速滑动的时候才执行以下操作
			if(!isFling){
				//当前x的坐标upX
				int upX = (int) event.getX();
				//tempIndex临时下标
				int tempIndex = currIndex;
				if(upX - firstX > getWidth()/2){//滑动到上一页面
					tempIndex--;
				}else if(firstX - upX> getWidth()/2){//滑动到下一页面
					tempIndex++;
				}
				//让view的内容移动到子view上面去
				moveToDest(tempIndex);
			}
			break;
		}
		return true;
	}

6、让view的内容移动到子view上面去

public void moveToDest(int tempIndex) {
		//防止左右滑动超出边界
		if(tempIndex < 0){
			tempIndex = 0;
		}
		if(tempIndex > getChildCount()-1){
			tempIndex = getChildCount()-1;
		}
		//将临时的下标给当前子view的下标
		currIndex = tempIndex;
		/**
		 * 触发改变页面的监听
		 */
		if(pageChangeListener != null){
			pageChangeListener.moveToDest(currIndex);
		}
		//scrollTo(currIndex*getWidth(), 0);
		//移动的距离  = 终点坐标 - 当前坐标
		int distanceX = currIndex*getWidth() - getScrollX();
		//开始执行动画
		scroller.startScroll(getScrollX(), getScrollY(), distanceX, 0,Math.abs(distanceX));
		/**
		 * 刷新会导致computeScroll()的执行
		 */
		invalidate();
	}

@Override
	public void computeScroll() {
		if(scroller.computeScrollOffset()){
			int curX = scroller.getCurrX();//获得当前滑动的位置
			//滑动到当前位置上
			scrollTo(curX, 0);
			//再刷新
			invalidate();
		}
	}

自定义MyScroller类(攻城狮)

/**
	 * 用于计算速度和位移
	 * @author Administrator
	 *
	 */
	public class MyScroller {
	private int startX;
	private int startY;
	private int distanX;
	private int distanY;
	//开始执行动画的时间
	private long startTime;
	//判断动画是否执行完成
	private boolean isFished;

	public MyScroller(Context ctx){
		
	}
	/**
	 * 开始滑动
	 * @param startX 基准点的x的坐标
	 * @param startY 基准点的y的坐标
	 * @param distanX x方向上要移动的距离
	 * @param distanY y方向上要移动的距离
	 */
	public void startScroll(int startX,int startY,int distanX,int distanY){
		this.startX = startX;
		this.startY = startY;
		this.distanX = distanX;
		this.distanY = distanY;
		//从手机开机到现在的毫秒数 但不包括手机休眠的时间
		this.startTime = SystemClock.uptimeMillis();
		//判断动画是否执行完成
		this.isFished = false;
	}
	/**
	 * 系统默认运行动画执行的的总时间 3秒
	 */
	private int totalTime = 3000;
	private int currX;
	private int currY;
	
	
	public int getCurrX() {
		return currX;
	}
	public void setCurrX(int currX) {
		this.currX = currX;
	}
	public int getCurrY() {
		return currY;
	}
	public void setCurrY(int currY) {
		this.currY = currY;
	}
	/**
	 * 计算当前的位移量
	 * @return  true 还在执行动画     false 动画已经结束
	 */
	public boolean computeScrollOffset(){
		//判断下动画是否已经结束----如果动画已经结束了就开始计算
		if(isFished){
			return false;
		}
		//获得已经运行的时间(当前时间-开始时间)
		long passTime = SystemClock.uptimeMillis()-startTime;
		//给它设置一个默认执行动画的总时间  来判断动画是否还在执行中  进行相应的计算
		if(passTime < totalTime){//说明动画还在执行当中
			//这段时间的位置 = 这段时间的差  * 速度
			//速度 = 总位移 / 总时间
			currX = (int)(startX + passTime*distanX/totalTime);
			currY = (int)(startY + passTime*distanY/totalTime);
		}else{//动画已经执行完成
			//计算当前x、y的位置
			currX = startX + distanX;
			currY = startY + distanY;
			//将isFished设置为true  表示动画已经执行完成了
			isFished = true;
		}
		return true;
		}
	}

7、在布局文件中RadioGroup,并在代码中动态添加RadioButton

<RadioGroup
    android:id="@+id/my_radio_group"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >
</RadioGroup>

//根据msv中的view的数量   动态添加RadioButton
    for (int i = 0; i < msv.getChildCount(); i++) {
        RadioButton button = new RadioButton(this);
        //如果是第一个的位置就默认选中
        if(i == 0){
            button.setChecked(true);
        }else{
            button.setChecked(false);
        }
        //将button的下标值设置为他的id的值
        button.setId(i);
        //将button添加到radioGroup中
        radioGroup.addView(button);
    }

8、在MyScrollView中添加页面改变的监听

/**
 * 定义接口,当页面发生改变时  触发该事件
 */
interface IPageChangeListener{
    void moveToDest(int destIndex);
}
private IPageChangeListener pageChangeListener;
public IPageChangeListener getPageChangeListener() {
    return pageChangeListener;
}
public void setPageChangeListener(IPageChangeListener pageChangeListener) {
    this.pageChangeListener = pageChangeListener;
}
    //给msv设置改变监听
    msv.setPageChangeListener(new MyScorllView.IPageChangeListener() {

        @Override
        public void moveToDest(int destIndex) {
            //根据切换的页面下标  找到对应的值选中
            //((RadioButton)radioGroup.getChildAt(destIndex)).setChecked(true);
            radioGroup.check(destIndex);
        }
    });

9、给RadioButton添加改变时监听

//给RadioButton添加改变时监听
    radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {

        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
            //checkedId是选中的button的ID值
            //在这里id是和下标一样  的数字
            msv.moveToDest(checkedId);
        }
    });

10、添加测试页面test.xml(添加一个ScrollView滚动测试)这里用ScrollVIew模拟ListView

这里test.xml就不列出了  大家只需模拟在这个测试页面上有滑动点击事件即可想ScrollView和ListView都有滑动点击事件

/**
     * 添加测试页面
     */
    View view = getLayoutInflater().inflate(R.layout.test, null);
    msv.addView(view,2);
    //根据msv中的view的数量   动态添加RadioButton
    for (int i = 0; i < msv.getChildCount(); i++) {
        RadioButton button = new RadioButton(this);
        //如果是第一个的位置就默认选中
        if(i == 0){
            button.setChecked(true);
        }else{
            button.setChecked(false);
        }
        //将button的下标值设置为他的id的值
        button.setId(i);
        //将button添加到radioGroup中
        radioGroup.addView(button);
    }

在测量view的大小的时候,也需要测量子view的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //当ViewGroup在测量大小的时候  也应该为每个子view测量大小  否则就可能会出现问题
    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        view.measure(widthMeasureSpec, heightMeasureSpec);
    }
}

11、给自定义ViewGroup添加事件分发

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    return super.dispatchTouchEvent(ev);
}

12、自定义ViewGroup的事件中断机制

/**
 * 该方法默认返回false  即不中断事件的传递
 * 如果返回true  即中断事件的传递   就需要我们自己来处理事件的传递  即执行onTouchEvent()方法
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean result = false;
    //如果水平滑动的距离大于竖直滑动的距离就中断竖直的子view滑动事件  否则就不中断
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        downInterX = (int) ev.getX();
        downInterY = (int) ev.getY();
        //解决detector不能收到down的事件  在这里将down的事件传递给它
        //如果不将down时间传给它,那么onTouchEvent事件将不能正常执行
        detector.onTouchEvent(ev);
        firstX = lastX = (int) ev.getX();
        //将是否正在快速滑动设置为false
        isFling = false;
        break;

    case MotionEvent.ACTION_MOVE:
        int distanceX = (int) Math.abs(ev.getX()-downInterX);
        int distanceY = (int) Math.abs(ev.getY()-downInterY);

        //为了防止手机抖动distanceX设置大于10
        if(distanceX > distanceY && distanceX >10){
            //水平移动大于竖直移动
            result = true;
        }else{
            result = false;
        }
        break;
    case MotionEvent.ACTION_UP:
        break;
    }
    return result;
};

好了 ,到这里,这个小demo就已经结束了,代码中都有详细的注释,大家看注释把,如果还是不理解的欢迎大家留言给我。。。。。

附上个人的一点难点理解:(这点是关于在测试页面点击事件触发的理解)

当点击view中的listview时,view先收到点击滑动的事件,再一层一层的往里面传递,最终传递到ListView上,而在此过程中外面层的事件,可随时消耗掉事件,这样外面层消耗了事件,里层就收不到滑动的事件,这样就被中断事件了。 当水平方向上的距离大于竖直方向上的距离时,竖直方向的滑动点击事件将被中断,也就是listview的事件被中断,这时就执行外层的滑动点击事件,反之,当滑动点击事件传递到里层的listview时,listview消耗了这个事件,这时外层的view滑动点击事件将被中断。 另外需要注意的就是,当滑动点击事件执行时,需要将down事件传递系统的手势滑动解析的工具GestureDetector,这样就可以防止滑动点击时跳动的问题。

最后给大家附上一张事件中断传递机制的图解,能更好的帮助大家理解后面的测试页面点击的部分。

Touch事件传递机制流程图

附上测试页面:

最后欢迎大家,关注我的博客,不定期分享一些有用的android方面的东西!



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值