android-自定义viewGroup-支持滑动

引子

自定义ViewGroup,用于实现复杂的控件特效。凡是见到的很是花哨牛逼的效果,大多能够分解为若干个 小的效果,而后经过自定义ViewGroup进行组合。可是,在组合的过程当中,明明两个牛逼控件各自运行好好的,组合起来就浑身毛病,比较多见的就是滑动冲突。java

今天,提供一个可横向滑动的ViewGroup,内部能够放置多个子View,并且子View能够带竖向滑动效果。android

本文只提供一个基础控件,重在提供一个写控件的思路,也让我本身往后温故知新。app

 

注意:如下控件并无考虑ViewGroup的padding和margin,因此,若是放到真实场景下,必然要作修改。框架

 

效果图

(每个子view都是listView,纵向的滑动效果我没有录,相信你们都能看明白)

 

 关键类或方法:

1)重写自定义layout的onMeasure,onLayout,让某一个子view占满layout,其余的都在屏幕以外ide

2)View基类自己自带的scrollBy方法,配合自定义layout的onTouchEvent截取的触摸事件,实现滑动布局

3)重写自定义layout的onInterceptTouchEvent方法,解决滑动冲突this

4)Scroller类,实现layout的平滑回滚,用于当你滑到layout边界以外时回滚到界内,或者你想滚到某一个子viewspa

5)VelocityTracker类,实现滑动速率的监听,当滑动速率超过临界值时,就算没有滑到下一个子view的临界点,也要用Scroller来平滑滚动到下一个子viewcode

5)最后提一下,上面几个都是基于android框架的内容,可是仅仅有他们还不够,最后须要咱们用本身的计算方式,结合1,2,3,4,5的原理,实现咱们本身想要的效果。xml

 

我观察过网上不少人写的博客,发现每一个人实现这个效果的计算方式各不相同。android框架的原理也许咱们都能理解,可是可以写出来的控件质量有高有低,就看我的的数学修为了。

不得不说,数学思惟逻辑仍是颇有用的。

 

源代码(拷贝到项目内能够直接使用)

HorizontalScrollViewEx.java 这个是自定义控件的源码
 1 package tt.zhou;  2  3 import android.content.Context;  4 import android.util.AttributeSet;  5 import android.util.Log;  6 import android.view.MotionEvent;  7 import android.view.VelocityTracker;  8 import android.view.ViewConfiguration;  9 import android.view.ViewGroup;  10 import android.widget.Scroller;  11  12 /**  13  * 能够横向滚动的viewGroup,兼容纵向滚动的子view  14 */  15 public class HorizontalScrollViewEx extends ViewGroup {  16  17 //第一步,定义一个追踪器引用  18 private VelocityTracker mVelocityTracker;//滑动速度追踪器  19  20  21 public HorizontalScrollViewEx(Context context) {  22 this(context, null);  23  }  24  25 public HorizontalScrollViewEx(Context context, AttributeSet attrs) {  26 this(context, attrs, 0);  27  }  28  29 public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {  30 super(context, attrs, defStyleAttr);  31  init(context);  32  }  33  34 private void init(Context context) {  35 mScroller = new Scroller(context);  36 //初始化追踪器  37 mVelocityTracker = VelocityTracker.obtain();//得到追踪器对象,这里用obtain,按照谷歌的尿性,应该是考虑了对象重用  38  }  39  40 int childCount;  41  42 /**  43  * 肯定每个子view的宽高  44  * <p>  45  * 若是是逐个去测量子view的话,必须在测量以后,调用setMeasuredDimension来设置宽高  46  * <p>  47  * 这里测量出来的宽高,会在onLayout中用来做为参考  48  *  49  * @param widthMeasureSpec  50  * @param heightMeasureSpec  51 */  52  @Override  53 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//spec 测量模式,  54  55 int width = MeasureSpec.getSize(widthMeasureSpec);  56 int height = MeasureSpec.getSize(heightMeasureSpec);  57 int widthMode = MeasureSpec.getMode(widthMeasureSpec);  58 int heightMode = MeasureSpec.getMode(heightMeasureSpec);  59  60 childCount = getChildCount();  61 measureChildren(widthMeasureSpec, heightMeasureSpec);//逐个测量全部的子view  62  63 if (childCount == 0) {//若是子view数量为0,  64 setMeasuredDimension(0, 0);//那么整个viewGroup宽高也就是0  65 } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {//若是viewGroup的宽高都是matchParent  66 width = childCount * getChildAt(0).getMeasuredWidth();// 那么,本viewGroup的宽,就是index为0的子view的测量宽度 乘以 子view的个数  67 height = getChildAt(0).getMeasuredHeight();//高,就是子view的高  68 setMeasuredDimension(width, height);//用子view的宽高,来设定  69 } else if (widthMode == MeasureSpec.AT_MOST) {  70 width = childCount * getChildAt(0).getMeasuredWidth();  71  setMeasuredDimension(width, height);  72 } else {  73 height = getChildAt(0).getMeasuredHeight();  74  setMeasuredDimension(width, height);  75 Log.d("setMeasuredDimension", "" + width);  76  }  77  }  78  79 /**  80  * 这个方法用于,处理布局全部的子view,让他们按照代码写的规则去排布  81  *  82  * @param changed  83  * @param l left,当前viewGroup的左边线距离父组件左边线的距离  84  * @param t top,当前viewGroup的上边线距离父组件上边线的距离  85  * @param r right,当前viewGroup的左边线距离父组件右边线的距离  86  * @param b bottom,当前viewGroup的上边线距离父组件下边线的距离  87 */  88  @Override  89 protected void onLayout(boolean changed, int l, int t, int r, int b) {  90 Log.d("onLayout", ":" + l + "-" + t + "-" + r + "-" + b);  91 int count = getChildCount();  92 int offsetX = 0;  93 for (int i = 0; i < count; i++) {  94 int w = getChildAt(i).getMeasuredWidth();  95 int h = getChildAt(i).getMeasuredHeight();  96 Log.d("onLayout", "w:" + w + " - h:" + h);  97  98 getChildAt(i).layout(offsetX + l, t, offsetX + l + w, b);//保证每次都最多只完整显示一个子view,由于在onMeasure中,已经将子view的宽度设置为了 本viewGroup的宽度  99 offsetX += w;//每次的偏移量都递增 100  } 101  } 102 103 104 private float lastInterceptX, lastInterceptY; 105 106 /** 107  * 事件的拦截, 108  * 109  * @param event 110  * @return 111 */ 112  @Override 113 public boolean onInterceptTouchEvent(MotionEvent event) { 114 boolean ifIntercept = false; 115 switch (event.getAction()) { 116 case MotionEvent.ACTION_DOWN: 117 lastInterceptX = event.getRawX(); 118 lastInterceptY = event.getRawY(); 119 break; 120 case MotionEvent.ACTION_MOVE: 121 //检查是横向移动的距离大,仍是纵向 122 float xDistance = Math.abs(lastInterceptX - event.getRawX()); 123 float yDistance = Math.abs(lastInterceptY - event.getRawY()); 124 if (xDistance > yDistance) { 125 ifIntercept = true; 126 } else { 127 ifIntercept = false; 128  } 129 break; 130 case MotionEvent.ACTION_UP: 131 break; 132 case MotionEvent.ACTION_CANCEL: 133 break; 134  } 135 return ifIntercept; 136  } 137 138 private float downX; 139 private float distanceX; 140 private boolean isFirstTouch = true; 141 private int childIndex = -1; 142 143  @Override 144 public boolean onTouchEvent(MotionEvent event) { 145 int scrollX = getScrollX();//控件的左边界,与屏幕原点的X轴坐标 146 int scrollXMax = (getChildCount() - 1) * getChildAt(1).getMeasuredWidth(); 147 final int childWidth = getChildAt(0).getWidth(); 148 mVelocityTracker.addMovement(event);//在onTouchEvent这里,截取event对象 149 ViewConfiguration configuration = ViewConfiguration.get(getContext()); 150 switch (event.getAction()) { 151 case MotionEvent.ACTION_DOWN: 152 break; 153 case MotionEvent.ACTION_MOVE: 154 //先让你滑动起来 155 float moveX = event.getRawX(); 156 if (isFirstTouch) {//一次事件序列,只会赋值一次? 157 downX = moveX; 158 isFirstTouch = false; 159  } 160 Log.d("distanceX", "" + downX + "|" + moveX + "|" + distanceX); 161 distanceX = downX - moveX; 162 163 //断定是否能够滑动 164 //这里有一个隐患,因为不知道Move事件,会以什么频率来分发,因此,这里多少都会出现一点偏差 165 if (getChildCount() >= 2) {//子控件在2个或者2个以上时,才有下面的效果 166 //若是命令是向左滑动,distanceX>0 ,那么判断命令是否能够执行 167 //若是命令是向右滑动,distanceX<0 ,那么判断命令是否能够执行 168 Log.d("scrollX", "scrollX:" + scrollX); 169 if (distanceX <= 0) { 170 if (scrollX >= 0) 171 scrollBy((int) distanceX, 0);//滑动 172 } else { 173 if (scrollX <= scrollXMax) 174 scrollBy((int) distanceX, 0);//滑动 175  } 176 }//若是只有一个,则不容许滑动,防止bug 177 break; 178 case MotionEvent.ACTION_UP:// 当手指松开的时候,要显示某一个完整的子view 179 mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());//计算,最近的event到up之间的速率 180 float xVelocity = mVelocityTracker.getXVelocity();//当前横向的移动速率 181 float edgeXVelocity = configuration.getScaledMinimumFlingVelocity();//临界点 182 childIndex = (scrollX + childWidth / 2) / childWidth;//整除的方式,来肯定X轴应该所在的单元,将每个item的竖向中间线定为滑动的临界线 183 if (Math.abs(xVelocity) > edgeXVelocity) {//若是当前横向的速率大于零界点, 184 childIndex = xVelocity > 0 ? (childIndex - 1) : (childIndex + 1);//xVelocity正数,表示从左往右滑,因此child应该是要显示前面一个 185  } 186 // childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));//不能够超出左右边界,这种写法可能很难一眼看懂,那就替换成下面的写法 187 if (childIndex < 0)//计算出的childIndex多是负数。那就赋值为0 188 childIndex = 0; 189 else if (childIndex >= getChildCount()) {//也有可能超出childIndex的最大值,那就赋值为最大值-1 190 childIndex = getChildCount() - 1; 191  } 192 smoothScrollBy(childIndex * childWidth - scrollX, 0);// 回滚的距离 193  mVelocityTracker.clear(); 194 isFirstTouch = true; 195 break; 196 case MotionEvent.ACTION_CANCEL: 197 break; 198  } 199 downX = event.getRawX(); 200 return super.onTouchEvent(event); 201  } 202 203 //实现平滑地回滚 204 205 /** 206  * 最叼的仍是这个方法,平滑地回滚,从当前位置滚到目标位置 207  * @param dx 208  * @param dy 209 */ 210 void smoothScrollBy(int dx, int dy) { 211 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);//从当前滑动的位置,平滑地过分到目标位置 212  invalidate(); 213  } 214 215  @Override 216 public void computeScroll() { 217 if (mScroller.computeScrollOffset()) { 218  scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 219  invalidate(); 220  } 221  } 222 223 private Scroller mScroller;//这个scroller是为了平滑滑动 224 }

 

 

 activity_main.xml 这个是引用自定义控件的布局文件(记得改控件的包名)

 1 <?xml version="1.0" encoding="utf-8"?>  2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  3 xmlns:tools="http://schemas.android.com/tools"  4 android:layout_width="match_parent"  5 android:layout_height="match_parent"  6 tools:context=".MainActivity">  7  8  9 <tt.zhou.HorizontalScrollViewEx 10 android:layout_width="match_parent" 11 android:layout_height="match_parent"> 12 13 <ListView 14 android:id="@+id/lv_1" 15 android:layout_width="match_parent" 16 android:layout_height="match_parent" 17 android:background="@android:color/holo_blue_dark"></ListView> 18 19 <ListView 20 android:id="@+id/lv_2" 21 android:layout_width="match_parent" 22 android:layout_height="match_parent" 23 android:background="@android:color/holo_green_light"></ListView> 24 25 <ListView 26 android:id="@+id/lv_3" 27 android:layout_width="match_parent" 28 android:layout_height="match_parent" 29 android:background="@android:color/darker_gray"></ListView> 30 31 <ListView 32 android:id="@+id/lv_4" 33 android:layout_width="match_parent" 34 android:layout_height="match_parent" 35 android:background="@android:color/holo_blue_dark"></ListView> 36 37 <ListView 38 android:id="@+id/lv_5" 39 android:layout_width="match_parent" 40 android:layout_height="match_parent" 41 android:background="@android:color/holo_green_light"></ListView> 42 </tt.zhou.HorizontalScrollViewEx> 43 44 45 </LinearLayout>

MainActivity.java  

 1 package tt.zhou;  2  3 import android.app.Activity;  4 import android.os.Bundle;  5 import android.widget.ArrayAdapter;  6 import android.widget.ListView;  7  8 import java.util.ArrayList;  9 import java.util.List; 10 11 public class MainActivity extends Activity { 12 13  ListView lv_1, lv_2, lv_3, lv_4, lv_5; 14 15  @Override 16 protected void onCreate(Bundle savedInstanceState) { 17 super.onCreate(savedInstanceState); 18  setContentView(R.layout.activity_main); 19  initData(); 20  init(); 21  } 22 23 private void init() { 24 lv_1 = findViewById(R.id.lv_1); 25 lv_2 = findViewById(R.id.lv_2); 26 lv_3 = findViewById(R.id.lv_3); 27 lv_4 = findViewById(R.id.lv_4); 28 lv_5 = findViewById(R.id.lv_5); 29 30 ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1); 31  lv_1.setAdapter(adapter1); 32 ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2); 33  lv_2.setAdapter(adapter2); 34 ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data3); 35  lv_3.setAdapter(adapter3); 36 ArrayAdapter<String> adapter4 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data4); 37  lv_4.setAdapter(adapter4); 38 ArrayAdapter<String> adapter5 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data5); 39  lv_5.setAdapter(adapter5); 40  } 41 42 private List<String> data1, data2, data3, data4, data5; 43 44 private void initData() { 45 data1 = new ArrayList<>(); 46 for (int i = 0; i < 100; i++) { 47 data1.add("d1-" + i); 48  } 49 data2 = new ArrayList<>(); 50 for (int i = 0; i < 100; i++) { 51 data2.add("d2-" + i); 52  } 53 data3 = new ArrayList<>(); 54 for (int i = 0; i < 100; i++) { 55 data3.add("d3-" + i); 56  } 57 data4 = new ArrayList<>(); 58 for (int i = 0; i < 100; i++) { 59 data4.add("d4-" + i); 60  } 61 data5 = new ArrayList<>(); 62 for (int i = 0; i < 100; i++) { 63 data5.add("d5-" + i); 64  } 65  } 66 }

 结语

上面的例子是,横向的layout,兼容竖向滑动的子view。

那么,按照这个原理,实现一个竖向的laytou,兼容横向滑动的子view,理解了上面提到的5个原理的同志们应该很容易写出来啦。

 

就酱紫咯。๑乛◡乛๑

相关文章
相关标签/搜索