您的位置:首页 > 移动开发 > Android开发

Android 使用ViewGroup实现ViewPager的效果

2016-05-21 14:52 309 查看
ViewPager控件可以让我们做出很多漂亮的界面,例如导航, 页面菜单等. 那么我们自己能否去实现ViewPager的效果呢? 本文将介绍如何使用ViewGroup + scrollTo + scroller实现ViewPager控件, 并且会简单地实现一个自己的scroller, 来了解学习系统提供的scroller类滑屏功能的实现思想.

首先看一下实现的效果:



定义自己的ViewPager类–MyViewPager继承ViewGroup

public class MyViewPager extends ViewGroup {
private Context context;
//手势识别工具类
private GestureDetector detector;

public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView();
}

private void initView() {

detector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}

//处理移动事件
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//将当前视图内容偏移(x , y)个单位,可视区域也跟着偏移(x,y)个单位
//也就是说让视图跟着鼠标移动, distanceX为鼠标在屏幕上移动的距离
scrollBy((int) distanceX, 0);
return false;
}

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置每个子view的位置
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);

return true;
}

}


(1) 在MyViewPager中, 重写OnLayout方法, 在OnLayout方法中去确定子view在ViewPager中的位置:



通过getChildAt获得所有的子view, 调用子view的layout方法设置每个子view的位置, layout接收4个参数(就是左上角坐标与右下角坐标), 来确定view的大小, 如上图可以分析出每个view在MyViewPager中的位置, 例如第2个view的位置是:

[getWidth(), 0, 2 * getWidth(), getHeight()] , getWidth 与 getHeight为MyViewPager的宽高. 我们可以看到变化的只有横坐标.

(2) 重写onTouchEvent方法, 此方法处理屏幕触摸事件, 在这里使用用户手势识别工具类GestureDetector来处理action.move事件, 在OnGestureListener的onScroll中处理move事件, onScroll方法的参数distanceX, 就是工具类帮我们计算好的手指在屏幕上x轴方向移动的距离, 然后就可以很方便的使用此参数, 调用scrollBy(x, y)方法移动视图, 让视图偏移(x, y). 这样就实现了MyViewPager随手指移动而移动.

2.使用MyViewPager. 并给MyViewPager设置子view

public class MainActivity extends AppCompatActivity {

private MyViewPager myViewPager;
private int[] imgIds = new int[] {
R.mipmap.a, R.mipmap.b, R.mipmap.c,
R.mipmap.d, R.mipmap.e, R.mipmap.f
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myViewPager = (MyViewPager) findViewById(R.id.my_viewpager);

for (int i = 0; i < imgIds.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setImageResource(imgIds[i]);
myViewPager.addView(imageView);
}
}
}


布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.myviewpager.MainActivity">

<com.myviewpager.MyViewPager
android:id="@+id/my_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</RelativeLayout>


给MyViewPager设置6张图片. 现在效果图如下:



3.现在图片可以跟随手指滑动而滑动, 但是还不能自动地切换, 图片只能停在你移动到的地方, 如果想实现切换的效果, 还需要我们自己去处理touch事件, 在onTouchEvent中继续添加代码:

//标记当前显示在屏幕上的图片
private int currPos = 0;
//记录按下时的横坐标
private int firstX = 0;

@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);
//添加下面的代码
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = getScrollX();    //获得按下去时的横坐标
break;
case MotionEvent.ACTION_UP:   //判断显示哪个子view, 如果滑动大于父控件的一半切换子view
int tmpPos = currPos;
if ((getScrollX() - firstX) > getWidth() / 2) {    //向左滑动
tmpPos++;
} else if ((firstX - getScrollX()) > getWidth() / 2) {  //向右滑动
tmpPos--;
}
MoveToDest(tmpPos);  //切换到指定的图片
break;
default:
break;
}
return true;
}

public void MoveToDest(int tmpPos) {
//确定currPos的值, 保证currPos的范围在[0, getChildCount() - 1]
currPos = tmpPos > 0 ? tmpPos : 0;
currPos = currPos < getChildCount() - 1 ? currPos : (getChildCount() - 1);
//将视图内容偏移至(x , y)坐标处,可视区域位于(x , y)坐标处
scrollTo(currPos * getWidth(), 0);
}


定义两个成员变量, firstX, currPos来记录点击屏幕时的点与当前显示在屏幕上的子view. 在touch_up事件中处理: 当在屏幕上滑动的距离大于屏幕的一半切换视图, 否则留在当前视图. 在MoveToDest方法中调用scrollTo来实现.

现在的效果为:



4.在ViewPager中, 我们去切换视图时, 并不是瞬间完成, 而是有个过程.

我们实现MyScroller类来实现滑动过程,新建MyScroller类:

public class MyScroller {

private Context context;
private int disX;
private int startY;
private int startX;
private int disY;
private long startTime; //开始动画时间
private boolean isFinish; //标志是否结束动画
//默认运行时间,500ms
private int duration = 500;
//当前绘制所在的X位置
private long currX;
//当前绘制所在的Y位置
private long currY;

public MyScroller(Context context) {
this.context = context;
}

public long getCurrY() {
return currY;
}

public void setCurrY(long currY) {
this.currY = currY;
}

public long getCurrX() {
return currX;
}

public void setCurrX(long currX) {
this.currX = currX;
}

/**
* 开始移动
* @param startX  开始时的x坐标
* @param startY  开始时的y坐标
* @param disX    x方向要移动的距离
* @param disY    y方向要移动的距离
*/
public void startScroll(int startX, int startY, int disX, int disY) {
this.startX = startX;
this.startY = startY;
this.disX = disX;
this.disY = disY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}

/**
* 计算当前的运行状况
* @return
* true 还在运行
* false 运行结束
*/
public boolean computeScrollOffset() {
if (isFinish) {
return false;
}
//获得绘制时的时间
long passTime = SystemClock.uptimeMillis() - startTime;
//如果时间还在允许的范围内
if (passTime <= duration) {
currX = startX + disX * passTime / duration;
currY = startY + disY * passTime / duration;
} else { //绘制运行结束
currX = startX + disX;
currY = startY + disY;
isFinish = true;
}
return true;
}

}


在startScroll方法中记录下当前滑动点坐标与目标点坐标, 并记录下当前时间. 然后在computeScrollOffset中开始去更新当前的滑动的坐标.

接下来使用MyScroller来实现滑动过程,在MyViewPager类中定义MyScroller类成员变量并去使用它来实现滑动过程:

private MyScroller myScroller;

//在initView中初始化
private void initView() {
...............
myScroller = new MyScroller(context);
...............
}


然后修改moveToDest方法,不去使用ScrollTo来实现移动,使用MyScroller来实现:

public void moveToDest(int nextId) {
..................
//不使用scrollTo来实现移动
//      scrollTo(currId * getWidth(), 0);
//获得移动的距离,移动距离等于最终位置-当前位置
int distance = currId * getWidth() - getScrollX();
myScroller.startScroll(getScrollX(), 0, distance, 0);
//会导致computeScroll方法执行
invalidate();
}

@Override
public void computeScroll() {
//计算当前绘制状况,进行绘制
if (myScroller.computeScrollOffset()) {
int nowX = (int) myScroller.getCurrX();
scrollTo(nowX, 0);
invalidate();
}
}


现在的效果你会看到视图切换时就会有个过程而不是很快就完成了:



现在视图的切换是匀速的,如果想达到ViewPager那种加速效果,将MyScroller改成系统提供的Scroller类即可,只需要将private MyScroller myScroller; 改为private Scroller myScroller; 其他都不需要动就可以实现加速效果, 因为我们的MyScroller类的接口和系统Scroller接口一样。

5.现在MyViewPager中的View全为ImageView, 那么向MyViewPager中添加一个布局(包括一个button和listView)来看一下是否同样的支持滑动效果.

(1)布局文件list_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_layout"
android:orientation="vertical" >

<Button
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_margin="10dp"
android:text="button"/>
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"/>

</LinearLayout>


(2)我们将布局文件加载到MyViewPager的第四个view

private LinearLayout listLayout;
private ListView listview;
private String[] datas = { "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };

@Override
protected void onCreate(Bundle savedInstanceState) {
............
//添加下面代码
listLayout = (LinearLayout) LayoutInflater.from(getApplicationContext())
.inflate(R.layout.list_view, null);
listview = (ListView) listLayout.findViewById(R.id.listview);

ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, datas);
listview.setAdapter(adapter);

for (int i = 0; i < ids.length; i++) {
if (i == 3) {  //将布局文件添加到第四个位置
msv.addView(listLayout);
} else {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(imgIds[i]);
msv.addView(imageView);
}
}

}


这个时候你运行会发现这个布局不会显示出来, 因为这个时候我们没有在MyViewPager中去调用布局控件的OnMeasure方法, 让其去测量子View的大小, 导致布局view没有显示出来,所以我们需要在MyViewPager中去重载OnMeasure方法,去调用每个子view的measure,绘制子view的大小。

//绘制每个子控件的尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
}


现在的效果为:



现在我们发现,当滑动到布局文件中,在listview上可以上下滑,但是此时左右滑动失效了,不能左右滑动。

原因因为Touch事件的传递导致, 我们知道touch事件是从父view到子view一层一层传递,当我们左右滑动时,此事件由MyViewPager一层层传递到listview,但是listview并不支持(消费)此事件,所以导致左右滑动不起作用了, 现在我们重写onInterceptTouchEvent,来去判断touch事件,如果是左右滑动事件那么就去中断此事件,不让其再往下传递,我们去处理消费它。

/**
* 返回true, 中断事件,执行自己的onTouchEvent方法
* 返回false, 默认处理,不中断事件, 也不会执行自己的onTouchEvent方法
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//解决点击图片时跳动的bug
detector.onTouchEvent(ev);

firstX = (int) ev.getX();
firstY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int disx = (int) Math.abs(ev.getX() - firstX); //不管是左右移,只判断是否左右移动
int disy = (int) Math.abs(ev.getY() - firstY); //竖直方向移动距离
//判断是否为水平方向移动,disx > 10 防止手指按住屏幕抖动
if(disx > disy && disx > 10) {
result = true;
} else {
result = false;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return result;
}


现在效果如下:



此时还缺少一点就是开放一个接口让外部来使用, 比如导航界面,点击某一个点,会直接跳到那个点指定的页面。

在MyViewPager中添加接口:

//监听器对象
private MyPagerChangedListener listener;

public MyPagerChangedListener getListener() {
return listener;
}

public void setListener(MyPagerChangedListener listener) {
this.listener = listener;
}
//页面改变时的监听接口
public interface MyPagerChangedListener{
void moveToDest(int currId);
}


然后在moveToDest方法中去判断此监听器是否为空,不为空就调用其接口。

//移动到指定的子控件上
public void moveToDest(int nextId) {
..................

//触发listener事件
if (listener != null) {
listener.moveToDest(currId);
}
..............
}


至此, 使用ViewGroup实现ViewPager效果完成, 如有问题可以留言。

源码下载地址:

http://download.csdn.net/detail/lbcab/9536961
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息