自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

1. 这里我们继承已有ViewGroup实现自定义控件,模拟出来ViewPager的效果,如下:

(1)实现的效果图如下:

(2)实现步骤:

• 自定义view继承viewGroup

• 重写onLayout方法,为每一个子View确定位置

• 重写onTouchEvent方法,监听touch事件,并用scrollTo()或scrollBy()方法移动view

• 监听UP事件,当手指抬起时候,判断应显示的页面位置,并计算距离、滑动页面。

• 添加页面切换的监听事件

2. 具体实现过程,如下:

(1)新建一个Android工程,命名为"仿ViewPager",如下:

(2)拷贝(美工设计好的)图片资源文件到res/drawable/,如下:

(3)代码首先是activity_main.xml,如下:

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:orientation="vertical"
 6     android:gravity="center_horizontal"
 7     tools:context="com.himi.myscrollview.MainActivity" >
 8     
 9     <RadioGroup 
10         android:id="@+id/radioGroup"
11         android:orientation="horizontal"
12         android:layout_width="match_parent"
13            android:layout_height="wrap_content"
14         />
15 
16     <com.himi.myscrollview.MyScrollView
17         android:layout_width="match_parent"
18         android:layout_height="match_parent"
19         android:id="@+id/myscroll_view" />
20 
21 </LinearLayout>

然后是MyScrollView.java,如下:

  1 package com.himi.myscrollview;
  2 
  3 import android.content.Context;
  4 import android.util.AttributeSet;
  5 import android.view.GestureDetector;
  6 import android.view.GestureDetector.OnGestureListener;
  7 import android.view.MotionEvent;
  8 import android.view.View;
  9 import android.view.ViewGroup;
 10 import android.widget.Scroller;
 11 
 12 public class MyScrollView extends ViewGroup {
 13 
 14     private Context ctx;
 15     /**
 16      * 判断是否发生快速滑动
 17      */
 18     private boolean isFling;
 19     public MyScrollView(Context context, AttributeSet attrs) {
 20         super(context, attrs);
 21         this.ctx = context;
 22         initView();
 23     }
 24 
 25     private void initView() {
 26         //myScroller = new MyScroller(ctx);
 27         myScroller = new Scroller(ctx);
 28         /**
 29          * GestureDetector根据ACTION_DOWN,ACTION_UP,ACTION_MOVE感受不同到手势
 30          * 然后提供一些API接口给程序员进行开发编程
 31          */
 32         detector = new GestureDetector(ctx, new OnGestureListener() {
 33             
 34             /**
 35              * 用户轻触触摸屏,由1个MotionEvent ACTION_DOWN触发 
 36              * Touch down时触发,不论是touch (包括long) ,scroll
 37              */
 38             private void onDow() {
 39 
 40             }
 41 
 42             /**
 43              * 用户(轻触触摸屏后)松开,由一个1个MotionEvent ACTION_UP触发  
 44              */
 45             public boolean onSingleTapUp(MotionEvent e) {
 46                 return false;
 47             }
 48             
 49             /**
 50              * 用户轻触触摸屏,尚未松开或拖动,由一个1个MotionEvent ACTION_DOWN触发  
 51              * 注意和onDown()的区别,强调的是没有松开或者拖动的状态 (单击没有松开或者移动时候就触发此事件,再触发onLongPress事件) 
 52              * 
 53              * onShowPress在Touch了还没有滑动时触发
 54              * onShowPress与onDown,onLongPress比较,onDown只要Touch down一定立刻触发。
 55              */
 56             public void onShowPress(MotionEvent e) {
 57                 
 58             }
 59             
 60             /**
 61              * 响应手指在屏幕上的滑动事件
 62              */
 63             public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
 64                     float distanceY) {
 65                 //移动屏幕
 66                 /**
 67                  * 移动当前的view内容 移动一段距离
 68                  * disX x方向移动的距离  设置为正值(图片向左移动)   为负值(图片向右移动)
 69                  * disY y方向移动的距离
 70                  */
 71                 scrollBy((int)distanceX, 0);
 72                 
 73                 /**
 74                  * 将当前视图的基准点移动到某个点     坐标点原点移动
 75                  * 初始状态基准点是左上角(0,0)
 76                  * x  水平方向x坐标
 77                  * y  竖直方向y坐标
 78                  * 
 79                  * scrollTo(x,y)
 80                  */
 81                 
 82                 return false;
 83             }
 84             
 85             
 86             /**
 87              * 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发 
 88              * 
 89              * Touch了不移动一直Touch down时触发
 90              * Touchdown后过一会没有滑动先触发onShowPress再是onLongPress。
 91              */
 92             public void onLongPress(MotionEvent e) {
 93                 
 94             }
 95             
 96             
 97             /**
 98              * 发生快速滑动的回调方法
 99              * e1:第1个ACTION_DOWN MotionEvent   
100              * e2:最后一个ACTION_MOVE MotionEvent 
101              * velocityX:X轴上的移动速度,像素/秒  
102              * velocityY:Y轴上的移动速度,像素/秒  
103              */
104             public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
105                     float velocityY) {
106                 
107                 isFling = true;
108                 if(velocityX>0 && currId>0) {//快速向右滑动
109                     currId--;
110                 }else if(velocityX<0 && currId< getChildCount()-1){//快速向右滑动
111                     currId++;
112                 }
113                 moveToDest(currId);
114                 
115                 return false;
116             }
117             
118             public boolean onDown(MotionEvent e) {
119                 return false;
120             }
121         });
122         
123     }
124 
125     
126     /**
127      * 计算控件大小
128      * 做为一个ViewGroup还有一个责任,就是计算每一个子View的大小
129      * 注意:这里如果不重写这个方法,我们自定义的temp.xml就不会显示布局的内容
130      * 这里的temp.xml内部也是一个ViewGroup,必须要测量这个大小尺寸,Android系统只有根据得到的尺寸才能安排显示
131      */
132     
133     @Override
134     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
135         // TODO 自动生成的方法存根
136         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
137         
138         int size = MeasureSpec.getSize(widthMeasureSpec);
139         int mode = MeasureSpec.getMode(widthMeasureSpec);
140         
141         for(int i = 0; i<getChildCount(); i++) {
142             View v = getChildAt(i);
143             v.measure(widthMeasureSpec, heightMeasureSpec);
144             
145             //v.getMeasuredWidth();得到测量的大小
146         }
147     }
148 
149     
150     /**
151      * 确定子view进行布局,确定子view的位置
152      * 参数changed :判断当前布局是否发生改变(true--改变,  false--没有改变)
153      * 参数 l	
  是指当前viewgroup(MyScrollView)在其父view中的位置
154      */
155     @Override
156     protected void onLayout(boolean changed, int l, int t, int r, int b) {
157         for(int i=0; i<getChildCount(); i++) {
158             //取得小标为i的子view
159             View view = getChildAt(i); 
160             
161             /**
162              * 父View会根据子View的需求,和自身的情况,来综合确实子View的位置(确定他的大小)
163              */
164             //指定子view的位置, 左 , 上, 右, 下 ,是指在viewgroup坐标系中的位置
165             view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());  
166             //view.getWidth();//得到View真实的大小
167         }
168         
169     }
170     /**
171      * 手势识别的工具类
172      */
173     private GestureDetector detector;
174     
175     
176     /**
177      * 当前的ID值
178      * 显示在屏幕上的子View的下标
179      */
180     private int currId = 0;
181     
182     
183 
184     /**
185      *down 事件时的y坐标
186      */
187     private int firstY = 0;
188     /**
189      * 
190      *是否中断事件的传递
191      *返回true的时候中断事件,执行自己的onTouchEvent方法
192      *返回false的时候,默认处理,不中断,也不会执行自己的onTouchEvent方法
193      */
194     @Override
195     public boolean onInterceptTouchEvent(MotionEvent ev) {
196         
197         boolean result = false;
198          System.out.println("onInterceptTouchEvent::"+ev.getAction());
199         
200         switch (ev.getAction()) {
201         case MotionEvent.ACTION_DOWN:
202                 firstX = (int) ev.getX();
203                 firstY= (int) ev.getY();
204                 break;
205         case MotionEvent.ACTION_MOVE:
206                  //手指在屏幕上水平移动的绝对值
207                  int disx = (int) Math.abs(ev.getX() -firstX);
208                  
209                  //手指在屏幕上竖直移动的绝对值
210                  int disy = (int) Math.abs(ev.getY() -firstY);
211                  
212                  if(disx > disy && disx >10) {
213                      result = true;
214                  }else {
215                      result = false;
216                  }
217                      
218                 break;
219         case MotionEvent.ACTION_UP:
220             
221             break;
222 
223         default:
224             break;
225         }
226         
227         return result;
228     }
229     
230     
231     
232     
233     
234     
235     /**
236      * down 事件时的x坐标
237      */
238     private int firstX = 0;
239     
240     @Override
241     public boolean onTouchEvent(MotionEvent event) {
242          super.onTouchEvent(event);
243          
244          System.out.println("onTouchEvent::"+event.getAction());
245          
246          detector.onTouchEvent(event);
247          
248          //添加自己的事件解析
249          switch (event.getAction()) {
250         case MotionEvent.ACTION_DOWN:
251             firstX = (int)event.getX();
252             break;
253         case MotionEvent.ACTION_MOVE:
254             
255             break;
256         case MotionEvent.ACTION_UP:
257             if (!isFling) {//在没有发生快速滑动的时候,才执行按位置判断currId
258                 int nextId = 0;
259                 //firstX---ACTION_DOWN的点(x坐标),event.getX()此时是---ACTION_UP的点(x坐标)
260                 if (event.getX() - firstX > getWidth() / 2) {// 手指向右滑动,超过屏幕的1/2,当前的currId
261                                                                 // -1
262                     nextId = currId - 1;
263                 } else if (firstX - event.getX() > getWidth() / 2) {// 手指向左滑动,超过屏幕的1/2,当前的currId
264                                                                     // +1
265                     nextId = currId + 1;
266                 } else {
267                     nextId = currId;
268                 }
269 
270                 moveToDest(nextId);
271             }
272             isFling = false;//防止快速滑动干扰页面正常翻转
273             //scrollTo(0,0);
274             break;
275         }
276          return true;
277     }
278     
279     /**
280      * 计算位移的工具类
281      */
282     //private MyScroller myScroller;//自定义的Scroller
283     private Scroller myScroller;//调用系统的Scroller(更加强大),注意修改上面的构造方法
284     /**
285      * 移动到指定的屏幕上
286      * @param nextId  屏幕的下标
287      */
288     public void moveToDest(int nextId) {
289         /**
290          * 首先对nextId进行判断,确保是在合理的范围
291          * 即 nextId >=0 && nextId <=getChildCount()-1
292          */
293         //确保currId >=0
294         currId = (nextId >=0)?nextId:0;
295         
296         //确保currId <=getChildCount()-1
297         currId = (nextId <=getChildCount()-1)?nextId:(getChildCount()-1);
298         
299         //用户体验是立即跳转的下一个页面,太过迅速,不自然,用户体验不好。要修改,改为稍缓移动到下一个页面
300         //瞬间移动
301         //scrollTo(currId*getWidth(),0);
302         
303         
304         //触发MyPageChangedListener事件
305         if(pageChangedListener != null) {
306             pageChangedListener.moveToDest(currId);
307         }
308         
309         //最终的位置 - 现在的位置 = 要移动的距离
310         int distance = currId*getWidth()-getScrollX();
311         
312         
313         //myScroller.startScroll(getScrollX(), 0, distance,0);//自定义的
314        /**
315         * 记录下开始时候:
316         * startScroll方法参数,如下:
317         * 参数1:x坐标 
318         * 参数2:y坐标 
319         * 参数3: x方向移动距离  
320         * 参数4:y方向移动的距离 
321         * 参数5: 设置运行的时间
322         */
323         myScroller.startScroll(getScrollX(), 0, distance,0,Math.abs(distance));
324         
325         /**
326          * invalidate()会刷新当前View,会导致onDraw方法的执行
327          */
328         invalidate();
329     }
330     
331     /**
332      * invalidate()会导致computeScroll()方法的执行
333      */
334     
335     @Override
336     public void computeScroll() {
337         if(myScroller.computeScrollOffset()) {//移动图片页面还在进行中,没有结束
338             int newX = (int) myScroller.getCurrX();
339             System.out.println("newX::"+newX);
340             scrollTo(newX, 0);
341             
342             //前面设置改变了View视图,但是必须刷新才能显示
343             //invalidate()方法调用,又会启用computeScroll()方法,如此反复,直至View不再移动(View移动到目的位置,终止移动)
344             invalidate();
345         }
346     }
347     
348     
349     public MyPageChangedListener getPageChangedListener() {
350         return pageChangedListener;
351     }
352 
353     public void setPageChangedListener(MyPageChangedListener pageChangedListener) {
354         this.pageChangedListener = pageChangedListener;
355     }
356     private MyPageChangedListener pageChangedListener;
357     
358     
359     /**
360      * 页面改变时候的监听接口
361      */
362     
363     public interface MyPageChangedListener {
364         void moveToDest(int currid);
365     }
366 }

还有就是计算位移距离的工具类MyScroller,如下:

  1 package com.himi.myscrollview;
  2 
  3 import android.content.Context;
  4 import android.os.SystemClock;
  5 
  6 /**
  7  * 计算位移距离的工具类
  8  * @author Administrator
  9  *
 10  */
 11 
 12 public class MyScroller {
 13     private int startX;
 14     private int startY;
 15     private int disX;
 16     private int disY;
 17     
 18     /**
 19      * 开始执行动画的时间
 20      */
 21     private long startTime;
 22     /**
 23      * 判断是否正在执行动画
 24      * true 还在运行
 25      * false 已经停止
 26      */
 27     private boolean isFinish;
 28 
 29     public MyScroller(Context context) {
 30         
 31     }
 32 
 33     /**
 34      * 开始移动
 35      * @param startX  开始时的X坐标
 36      * @param startY  开始时的Y坐标
 37      * @param disX    X方向要移动的距离
 38      * @param disY    Y方向要移动的距离
 39      */
 40     public void startScroll(int startX, int startY, int disX, int disY) {
 41             this.startX = startX;
 42             this.startY = startY;
 43             this.disX = disX;
 44             this.disY = disY;
 45             this.startTime = SystemClock.uptimeMillis();//手机开机时开始计算的毫秒值
 46             
 47             this.isFinish = false;
 48         
 49     }
 50     /**
 51      * 默认运行的时间
 52      * 毫秒值
 53      */
 54     private int duration = 500;
 55     /**
 56      * 当前的X的值
 57      */
 58     
 59     private long currX;
 60     /**
 61      * 当前的Y的值
 62      */
 63     private long currY;
 64 
 65     public long getCurrX() {
 66         return currX;
 67     }
 68 
 69     public void setCurrX(long currX) {
 70         this.currX = currX;
 71     }
 72 
 73     /**
 74      * 计算一下当前的运行状况
 75      *返回值 :
 76      * true 还在运行
 77      * false 运行结束
 78      */
 79     public boolean computeScrollOffset() {
 80         if(isFinish) {
 81             return false;
 82         }
 83         
 84         //获得所用的时间
 85         long passTime = SystemClock.uptimeMillis() - startTime;
 86         
 87         //如果时间还在允许的范围内
 88         if(passTime<duration) {
 89             //当前的位置 = 开始的位置 +移动的距离 (距离 = 速度*时间)
 90             currX = startX+disX*passTime/duration;
 91             currY= startY+disY*passTime/duration;
 92         }else  {
 93             currX = startX + disX;
 94             currY = startY + disY;
 95             isFinish = true;
 96         }
 97         
 98         
 99         return true;
100     }
101 
102 }

其中测试布局xml文件temp.xml,如下:

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:gravity="center_horizontal"
 6     android:background="@android:color/darker_gray"
 7     android:orientation="vertical"
 8     tools:context="com.himi.myscrollview.MainActivity" >
 9 
10     <Button
11         android:id="@+id/button1"
12         android:layout_width="wrap_content"
13         android:layout_height="wrap_content"
14         android:text="Button" />
15 
16     <TextView
17         android:id="@+id/textView1"
18         android:layout_width="wrap_content"
19         android:layout_height="wrap_content"
20         android:text="Large Text"
21         android:textAppearance="?android:attr/textAppearanceLarge" />
22 
23     <ScrollView
24         android:id="@+id/scrollView1"
25         android:layout_width="match_parent"
26         android:layout_height="wrap_content" >
27 
28         <LinearLayout
29             android:layout_width="match_parent"
30             android:layout_height="match_parent"
31             android:orientation="vertical" >
32 
33             <TextView
34                 android:id="@+id/textView0"
35                 android:layout_width="wrap_content"
36                 android:layout_height="wrap_content"
37                 android:text="Large Text"
38                 android:textAppearance="?android:attr/textAppearanceLarge" />
39 
40             <TextView
41                 android:id="@+id/textView2"
42                 android:layout_width="wrap_content"
43                 android:layout_height="wrap_content"
44                 android:text="Large Text"
45                 android:textAppearance="?android:attr/textAppearanceLarge" />
46 
47             <TextView
48                 android:id="@+id/textView3"
49                 android:layout_width="wrap_content"
50                 android:layout_height="wrap_content"
51                 android:text="Large Text"
52                 android:textAppearance="?android:attr/textAppearanceLarge" />
53 
54             <TextView
55                 android:id="@+id/textView4"
56                 android:layout_width="wrap_content"
57                 android:layout_height="wrap_content"
58                 android:text="Large Text"
59                 android:textAppearance="?android:attr/textAppearanceLarge" />
60 
61             <TextView
62                 android:id="@+id/textView5"
63                 android:layout_width="wrap_content"
64                 android:layout_height="wrap_content"
65                 android:text="Large Text"
66                 android:textAppearance="?android:attr/textAppearanceLarge" />
67 
68             <TextView
69                 android:id="@+id/textView6"
70                 android:layout_width="wrap_content"
71                 android:layout_height="wrap_content"
72                 android:text="Large Text"
73                 android:textAppearance="?android:attr/textAppearanceLarge" />
74         </LinearLayout>
75     </ScrollView>
76 
77 </LinearLayout>

这个temp.xml布局效果如下:

(4)MainActivity,如下:

 1 package com.himi.myscrollview;
 2 
 3 import android.app.Activity;
 4 import android.os.Bundle;
 5 import android.view.View;
 6 import android.widget.ImageView;
 7 import android.widget.RadioButton;
 8 import android.widget.RadioGroup;
 9 import android.widget.RadioGroup.OnCheckedChangeListener;
10 
11 import com.himi.myscrollview.MyScrollView.MyPageChangedListener;
12 
13 public class MainActivity extends Activity {
14     private MyScrollView msv;
15     //图片的资源ID数组
16     private int[] ids = new int[] { R.drawable.a1,R.drawable.a2,R.drawable.a3,
17             R.drawable.a4,R.drawable.a5,R.drawable.a6
18     };
19     
20     private RadioGroup radioGroup;
21     @Override
22     protected void onCreate(Bundle savedInstanceState) {
23         super.onCreate(savedInstanceState);
24         setContentView(R.layout.activity_main);
25         
26         msv =  (MyScrollView) findViewById(R.id.myscroll_view);
27         radioGroup = (RadioGroup) findViewById(R.id.radioGroup);
28         
29         for(int i=0; i<ids.length; i++) {
30             ImageView image = new ImageView(this);
31             image.setBackgroundResource(ids[i]);
32             //添加image图片资源  到  自定义的MyScrollView(ViewGroup是父容器)
33             msv.addView(image);
34         
35         }
36         
37         msv.setPageChangedListener(new MyPageChangedListener() {
38             
39             public void moveToDest(int currid) {
40                 ((RadioButton)radioGroup.getChildAt(currid)).setChecked(true);
41                 
42             }
43         });
44         
45         radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
46             
47             public void onCheckedChanged(RadioGroup group, int checkedId) {
48                 msv.moveToDest(checkedId);
49                 
50             }
51         });
52         
53         //给自定义的viewGroup添加测试的布局
54         View temp  = getLayoutInflater().inflate(R.layout.temp, null);
55         msv.addView(temp,2);
56         
57         for (int i = 0; i < msv.getChildCount(); i++) {
58 
59             // 添加radioButton
60             RadioButton rbtn = new RadioButton(this);
61             rbtn.setId(i);
62             radioGroup.addView(rbtn);
63             if (i == 0) {
64                 rbtn.setChecked(true);
65             }
66         }
67     }
68 
69 }

2. 附件理解图:

(1)起始状态,多张页面pager位置布局图,在上面的Layout方法中,如下:

(2)当我们手指水平向右滑动,页面标号id减少

                 手指水平向左滑动,页面标号id增加

原文地址:https://www.cnblogs.com/hebao0514/p/4849658.html