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个原理的同志们应该很容易写出来啦。

    就酱紫咯。๑乛◡乛๑

  • 相关阅读:
    《新下级学》第八章第八、九节——责任总论等
    《新下级学》第八章第五、六、七节——信息不透明导致奖金失效等
    《新下级学》第八章第四节——不信任沟通
    《新下级学》第八章第三节——信任沟通
    《新下级学》第八章第二节——沟通工具
    《新下级学》第八章序和第一节——宏观互动
    《新下级学》第七章第五节——互动的陷阱
    《新下级学》第七章第四节——互动的三个频道
    GPS校时设备,GPS对时产品,NTP授时服务器
    北斗授时设备(NTP)在医疗行业的重要性
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9130379.html
Copyright © 2011-2022 走看看