zoukankan      html  css  js  c++  java
  • android-自定义viewGroup-支持滑动

    引子

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

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

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

    注意:以下控件并没有考虑ViewGroup的padding和margin,所以,如果放到真实场景下,必然要做修改。

     

    效果图

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

     

     关键类或方法:

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

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

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

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

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

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

    我观察过网上很多人写的博客,发现每个人实现这个效果的计算方式各不相同。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个原理的同志们应该很容易写出来啦。

    就酱紫咯。๑乛◡乛๑

  • 相关阅读:
    Android 3.0 r1 API中文文档(108) —— ExpandableListAdapter
    Android 3.0 r1 API中文文档(113) ——SlidingDrawer
    Android 3.0 r1 API中文文档(105) —— ViewParent
    Android 中文 API (102)—— CursorAdapter
    Android开发者指南(4) —— Application Fundamentals
    Android开发者指南(1) —— Android Debug Bridge(adb)
    Android中文API(115)——AudioFormat
    Android中文API(116)——TableLayout
    Android开发者指南(3) —— Other Tools
    Android中文API (110) —— CursorTreeAdapter
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9130379.html
Copyright © 2011-2022 走看看