有了之前自定义View的理论基础,有了ViewPage、事件分发机制、滑动冲突、Scroller使用等相关知识的铺垫,今天纯手动打造一款ViewPage。
1、完成基本的显示:
在MainActivity中:
public class MainActivity extends AppCompatActivity { private MyViewPage mViewPage; int[] imageIds = new int[]{ R.drawable.pic_0, R.drawable.pic_1, R.drawable.pic_2, R.drawable.pic_3, R.drawable.pic_4, R.drawable.pic_5 }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewPage = (MyViewPage) findViewById(R.id.myviewpage); //给自定义ViewPage添加孩子组件 for (int i = 0; i < imageIds.length; i++) { ImageView imageView = new ImageView(this); imageView.setBackgroundResource(imageIds[i]); mViewPage.addView(imageView); } } }在MyViewPage中:
public class MyViewPage extends ViewGroup { public MyViewPage(Context context) { this(context,null); } public MyViewPage(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); for (int i = 0; i < count; i++) { //遍历所有孩子,手动安放每个孩子控件的位置 getChildAt(i).layout(i*getWidth(),0,(i+1)*getWidth(),getHeight()); } } }
首先往自定义view里面添加了6张图片,在view的onLayout方法中,给每个孩子组件进行布局安放位置,因为位置都确定了,因而不用去进行测量和绘制也可以显示。
给每个孩子布局位置的算法如下:
此时运行:
2、实现可滑动效果
运行后,按照添加的顺序显示,第一张肯定显示的是第一个孩子控件对象的图片。但是此时是无法进行滑动的,我们使用手势识别器GestureDetector,让自定义的控件可以滑动:
private void init() { mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { //手势识别器移动的监听回调。每次移动,都会回调该方法 @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //参数1:起点动作封装;参数2:终点动作封装;参数3:x方向的移动距离;参数4:y方向滑动距离 scrollBy((int) distanceX,0); return true; } }); }初始化手势识别器,重写手势滑动监听回调。每当滑动的时候,都会调用这里的方法,我们在这里直接调用ScrollBy()方法,更新当前控件(MyViewPage)内部孩子控件(图片)的位置。在这里需要知道ScrollBy()和ScrollTo()的区别:
scrollBy来移动一段相对的距离,属于温柔性质的。表示在原来坐标的基础上再改变参数大小的距离。对于x:大于零表示往左移,小于零表示往右移。对于y:大于零表示往上移,小于零表示往下移。
scrollBy(10, 0);从右往左移动10个像素
scrollBy(-10, 0);从左往右动10个像素
scrollTo就是把View移动到屏幕的X和Y位置,属于强迫性质的。参数代表我一次性跳跃到该坐标位置。
我擦,中国语言真是博大精深啊~
瞬间移动视图的内容: 利用View的scroll方法
1). scrollBy(int x, int y) : 滑动指定的偏移量(从当前位置瞬间)
x: x轴上的偏移量, x>0内容向左滑动, x<0内容向右滑动, x=0水平方向不滑动
y: y轴上的偏移量, y>0内容向上滑动, y<0内容向下滑动, y=0垂直方向不滑动
2). scrollTo(int x, int y) : 滑动到指定的偏移量(从当前位置瞬间)
x: 目标位置x轴上的偏移量, x>0移动到原始位置的左侧, x<0移动到原始位置的右侧,x=0移动到水平原始位置,
y: 目标位置y轴上的偏移量, y>0移动到原始位置的上侧, y<0移动到原始位置的下侧, y=0移动到垂直原始位置
View类的源代码如下所示,mScrollX记录的是当前View针对屏幕坐标在水平方向上的偏移量(getScrollX();),而mScrollY则是记录的时当前View针对屏幕在竖值方向上的偏移量(getScrollY();)。
scrollTo就是把View移动到屏幕的X和Y位置,也就是绝对位置。而scrollBy其实就是调用的scrollTo,但是参数是当前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy调用的是相对于mScrollX和mScrollY的位置。
手势识别器要想使用,需要把touch事件委托给手势识别器来处理:
@Override public boolean onTouchEvent(MotionEvent event) { //事件委托交手势识别器 mGestureDetector.onTouchEvent(event); return true; }运行程序效果:
可以看到,此时我们实现了滑动效果。
3、滑动到某个位置后自动到合适位置下标停留
此时的滑动显然是不可行的,我们需要跟ViewPage那样,滑动小于屏幕一半时,跳转到当前页面,滑动距离大于一半时,跳转到下一页。让我们来实现利逻辑吧:
switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: //松开手的时候,根据当前位置,来确定下一个页面 int scrollX = getScrollX(); //当前页的索引值 int pageIndex = scrollX / getWidth(); int offset = scrollX % getWidth(); if(offset > getWidth()/2){ pageIndex++; } //处理越界问题 if(pageIndex > getChildCount()-1){ pageIndex = getChildCount()-1; } goCurrentPage(pageIndex); break; default: break; }
我们重写了touch事件,因为监听到底是否在页面一半位置,是手指离开屏幕时候决定的,因而逻辑写在UP事件里面即可。相信这个小算法难不倒你。
当前的pageIndex就是对应的页码,而且处理了越界问题。最后再生成一个方法,专门用于跳转页面功能。具体代码如下:
/** * 根据当前 * @param pageIndex * 当前的page页面 */ private void goCurrentPage(int pageIndex) { scrollTo(pageIndex*getWidth(),0); }是的,只需要一行代码就可以了!运行起来看看效果吧:
此时跟原生的ViewPgae挺像了,但是还是稍有区别的,即滑动跳转很是生硬。这是由于使用了ScrollTo()进行了强制跳转的缘故。为了与ViewPage更贴近,我们使用系统提供的类:Scoller来解决生硬问题。
Scoller的具体用法可以参考博客:Android Scroller完全解析
4、回弹过程解决办法
修改ScrollTo(),被Scoller取代:
private void goCurrentPage(int pageIndex) { //scrollTo(pageIndex*getWidth(),0); int dx = pageIndex*getWidth() - getScrollX(); Log.e("YDL",dx+"-------"); //参数1:x的起始值;参数2:y的起始值;参数3:x的偏移量;参数4:y的偏移量 //对于参数3:dx>0往左移动;dx<0往右移动 mScroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx));//dx绝对值作为时间值,按比例可以实现了匀速移动 //使用Scroller必须重新刷新界面,不刷新的话不会滑动 invalidate(); }在这里需要借助Scroller来完成后续的滚动操作。接下来我们就调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。
在这里比较难理解的可能是dx值的计算方式。我也通过两张图片来分析里面的算法:
图一:移动距离超过半个屏幕,应该执行跳转下一页的功能。因此dx=i*getWidth()-getScrollX();可以自行测试。
图二:移动的距离小于屏幕一半,执行跳转上一页的功能:
现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法:
//Scroller使用调用invalidate();后,会同步调用computeScroll()方法 @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ int currX = mScroller.getCurrX(); Log.e("YDL",currX+""); scrollTo(mScroller.getCurrX(),0); //也要刷新界面 invalidate(); } }
并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的currenX和currentY坐标传入,然后刷新界面从而完成平滑滚动的操作。
那么我们运行程序看看效果吧:
可以看到,效果跟系统自带的ViewPage几乎一模一样了。那么最后,再分析一下Scroller进行滚动的原理吧。
平滑移动视图的内容: 利用Scoller和View的scroll方法
1). Scoller是实现View平滑移动的帮助类, 它本身并不能实现对View的移动
2). 平滑移动的基本原理: 将整个从起始位置到结束位置的移动分解成多个小的距离, 循环调用scrollTo()实现平滑移动
3). 相关API:
a. Scoller类:
-->Scoller(Context context) : 创建对象的构造方法
-->startScroll(int startX, int startY, int dx, int dy, int duration) : 开始平滑移动视图(这个方法本身不会产生滑动)
startX : 起始位置的X偏移量
startY : 起始位置的Y偏移量
dx: 滑动多大的X偏移量(如果是0,X方向不会滑动)
dy: 滑动多大的Y偏移量(如果是0,Y方向不会滑动)
duration : 整个过程持续的时间(ms)
-->startScroll(int startX, int startY, int dx, int dy): 开始平滑移动视图(时间为250ms)
-->boolean computeScrollOffset() : 计算当前移动的偏移量, 并将其保存到Scoller对象中, 如果滑动还没有完成返回true
-->int getCurrX() : 得到计算出的X偏移量
-->int getCurrY() : 得到计算出的Y偏移量
b. View类
-->invalidate() : 强制重绘, 导致draw()-->computeScroll()
在scoller.startScroll()后必须执行此方法
-->computeScroll() : 需要重写此方法, 用于计算移动, 此方法在draw()中调用
调用scoller计算移动偏移量
调用view对象scrollTo()到计算出的偏移量
调用View对象invalidate()强制重绘, 导致computeScroll()再次执行
我们在上面的代码中可以看到当我们手指不段移动屏幕时,就会调用scrollBy来移动一段相对的距离。而当我们手指松开后,会调用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);来产生一段动画来移动到相应的页面,在这个过程中系统会不断调用computeScroll(),我们再使用scrollTo来把View移动到当前Scroller所在的绝对位置。
到目前为止,该自定义ViewPage控件算是讲完了。
打开微信搜索公众号 Android程序员开发指南 或者手机扫描下方二维码 在公众号阅读更多Android文章。
微信公众号图片: