今天做了一个关于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方面的东西!