引子
自定义ViewGroup,用于实现复杂的控件特效。凡是见到的非常花哨牛逼的效果,大多可以分解为若干个 小的效果,然后通过自定义ViewGroup进行组合。但是,在组合的过程中,明明两个牛逼控件各自运行好好的,组合起来就浑身毛病,比较多见的就是滑动冲突。
今天,提供一个可横向滑动的ViewGroup,内部可以放置多个子View,而且子View可以带竖向滑动效果。
本文只提供一个基础控件,重在提供一个写控件的思路,也让我自己日后温故知新。
效果图
(每一个子view都是listView,纵向的滑动效果我没有录,相信大家都能看明白)
源代码
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.ViewGroup; 8 import android.widget.Scroller; 9 10 /** 11 * 可以横向滚动的viewGroup,兼容纵向滚动的子view 12 */ 13 public class HorizontalScrollViewEx extends ViewGroup { 14 15 public HorizontalScrollViewEx(Context context) { 16 this(context, null); 17 } 18 19 public HorizontalScrollViewEx(Context context, AttributeSet attrs) { 20 this(context, attrs, 0); 21 } 22 23 public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) { 24 super(context, attrs, defStyleAttr); 25 init(context); 26 } 27 28 private void init(Context context) { 29 mScroller = new Scroller(context); 30 } 31 32 int childCount; 33 34 /** 35 * 确定每一个子view的宽高 36 * <p> 37 * 如果是逐个去测量子view的话,必须在测量之后,调用setMeasuredDimension来设置宽高 38 * <p> 39 * 这里测量出来的宽高,会在onLayout中用来作为参考 40 * 41 * @param widthMeasureSpec 42 * @param heightMeasureSpec 43 */ 44 @Override 45 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//spec 测量模式, 46 47 int width = MeasureSpec.getSize(widthMeasureSpec); 48 int height = MeasureSpec.getSize(heightMeasureSpec); 49 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 50 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 51 52 childCount = getChildCount(); 53 measureChildren(widthMeasureSpec, heightMeasureSpec);//逐个测量所有的子view 54 55 if (childCount == 0) {//如果子view数量为0, 56 setMeasuredDimension(0, 0);//那么整个viewGroup宽高也就是0 57 } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {//如果viewGroup的宽高都是matchParent 58 width = childCount * getChildAt(0).getMeasuredWidth();// 那么,本viewGroup的宽,就是index为0的子view的测量宽度 乘以 子view的个数 59 height = getChildAt(0).getMeasuredHeight();//高,就是子view的高 60 setMeasuredDimension(width, height);//用子view的宽高,来设定 61 } else if (widthMode == MeasureSpec.AT_MOST) { 62 width = childCount * getChildAt(0).getMeasuredWidth(); 63 setMeasuredDimension(width, height); 64 } else { 65 height = getChildAt(0).getMeasuredHeight(); 66 setMeasuredDimension(width, height); 67 Log.d("setMeasuredDimension", "" + width); 68 } 69 } 70 71 /** 72 * 这个方法用于,处理布局所有的子view,让他们按照代码写的规则去排布 73 * 74 * @param changed 75 * @param l left,当前viewGroup的左边线距离父组件左边线的距离 76 * @param t top,当前viewGroup的上边线距离父组件上边线的距离 77 * @param r right,当前viewGroup的左边线距离父组件右边线的距离 78 * @param b bottom,当前viewGroup的上边线距离父组件下边线的距离 79 */ 80 @Override 81 protected void onLayout(boolean changed, int l, int t, int r, int b) { 82 Log.d("onLayout", ":" + l + "-" + t + "-" + r + "-" + b); 83 int count = getChildCount(); 84 int offsetX = 0; 85 for (int i = 0; i < count; i++) { 86 int w = getChildAt(i).getMeasuredWidth(); 87 int h = getChildAt(i).getMeasuredHeight(); 88 Log.d("onLayout", "w:" + w + " - h:" + h); 89 90 getChildAt(i).layout(offsetX + l, t, offsetX + l + w, b);//保证每次都最多只完整显示一个子view,因为在onMeasure中,已经将子view的宽度设置为了 本viewGroup的宽度 91 offsetX += w;//每次的偏移量都递增 92 } 93 } 94 95 96 private float lastInterceptX, lastInterceptY; 97 98 /** 99 * 事件的拦截, 100 * 101 * @param event 102 * @return 103 */ 104 @Override 105 public boolean onInterceptTouchEvent(MotionEvent event) { 106 boolean ifIntercept = false; 107 switch (event.getAction()) { 108 case MotionEvent.ACTION_DOWN: 109 lastInterceptX = event.getRawX(); 110 lastInterceptY = event.getRawY(); 111 break; 112 case MotionEvent.ACTION_MOVE: 113 //检查是横向移动的距离大,还是纵向 114 float xDistance = Math.abs(lastInterceptX - event.getRawX()); 115 float yDistance = Math.abs(lastInterceptY - event.getRawY()); 116 if (xDistance > yDistance) { 117 ifIntercept = true; 118 } else { 119 ifIntercept = false; 120 } 121 break; 122 case MotionEvent.ACTION_UP: 123 break; 124 case MotionEvent.ACTION_CANCEL: 125 break; 126 } 127 return ifIntercept; 128 } 129 130 private float downX; 131 private float distanceX; 132 private boolean isFirstTouch = true; 133 private int childIndex = -1; 134 135 @Override 136 public boolean onTouchEvent(MotionEvent event) { 137 int scrollX = getScrollX();//控件的左边界,与屏幕原点的X轴坐标 138 int scrollXMax = (getChildCount() - 1) * getChildAt(1).getMeasuredWidth(); 139 final int childWidth = getChildAt(0).getWidth(); 140 switch (event.getAction()) { 141 case MotionEvent.ACTION_DOWN: 142 break; 143 case MotionEvent.ACTION_MOVE: 144 //先让你滑动起来 145 float moveX = event.getRawX(); 146 if (isFirstTouch) {//一次事件序列,只会赋值一次? 147 downX = moveX; 148 isFirstTouch = false; 149 } 150 Log.d("distanceX", "" + downX + "|" + moveX + "|" + distanceX); 151 distanceX = downX - moveX; 152 153 //判定是否可以滑动 154 //这里有一个隐患,由于不知道Move事件,会以什么频率来分发,所以,这里多少都会出现一点误差 155 if (getChildCount() >= 2) {//子控件在2个或者2个以上时,才有下面的效果 156 //如果命令是向左滑动,distanceX>0 ,那么判断命令是否可以执行 157 //如果命令是向右滑动,distanceX<0 ,那么判断命令是否可以执行 158 Log.d("scrollX", "scrollX:" + scrollX); 159 if (distanceX <= 0) { 160 if (scrollX >= 0) 161 scrollBy((int) distanceX, 0);//滑动 162 } else { 163 if (scrollX <= scrollXMax) 164 scrollBy((int) distanceX, 0);//滑动 165 } 166 }//如果只有一个,则不允许滑动,防止bug 167 break; 168 case MotionEvent.ACTION_UP:// 当手指松开的时候,要显示某一个完整的子view 169 170 //找出view之间切换的临界点 171 int[] edgeX = new int[getChildCount() - 1]; 172 // 计算临界点X坐标 173 for (int i = 0; i < edgeX.length; i++) { 174 edgeX[i] = i * childWidth + childWidth / 2; 175 Log.d("edgeX", " - edgeX:" + edgeX[i]); 176 } 177 178 childIndex = (scrollX + childWidth / 2) / childWidth;//整除的方式,来确定X轴应该所在的单元 179 smoothScrollBy(childIndex * childWidth - scrollX, 0);// 回滚的距离 180 181 isFirstTouch = true; 182 break; 183 case MotionEvent.ACTION_CANCEL: 184 break; 185 } 186 downX = event.getRawX(); 187 return super.onTouchEvent(event); 188 } 189 190 //实现平滑地回滚 191 void smoothScrollBy(int dx, int dy) { 192 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);// 193 invalidate(); 194 } 195 196 @Override 197 public void computeScroll() { 198 if (mScroller.computeScrollOffset()) { 199 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 200 invalidate(); 201 } 202 } 203 204 private Scroller mScroller;//这个scroller是为了平滑滑动 205 }
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 }