• 弹性ScrollView的实现(转)


    弹性ScrollView的实现

     

    本文讨论弹性ScrollView的实现。弹性ScrollView实现原理和ListView不同, 因为ScrollView只能有一个子View, 不能为他添加额外的HeaderView。 弹性ScrollView的实现原理是移动这个唯一的子View的布局。下面首先给出所有的实现代码, 再对实现中的几个关键点进行说明。 (实现原理比较简单, 代码逻辑也不是很复杂, 代码比较少)

    import android.content.Context;

    import android.graphics.Rect;

    import android.util.AttributeSet;

    import android.view.MotionEvent;

    import android.view.View;

    import android.view.animation.TranslateAnimation;

    import android.widget.ScrollView;

     

    /**

     * 有弹性的ScrollView

     * 实现下拉弹回和上拉弹回

     * @author zhangjg

     * @date Feb 13, 2014 6:11:33 PM

     */

    public class ReboundScrollView extends ScrollView {

           

            private static final String TAG = "ElasticScrollView";

           

            //移动因子, 是一个百分比, 比如手指移动了100px, 那么View就只移动50px

            //目的是达到一个延迟的效果

            private static final float MOVE_FACTOR = 0.5f;

           

            //松开手指后, 界面回到正常位置需要的动画时间

            private static final int ANIM_TIME = 300;

           

            //ScrollView的子View, 也是ScrollView的唯一一个子View

            private View contentView;

           

            //手指按下时的Y值, 用于在移动时计算移动距离

            //如果按下时不能上拉和下拉, 会在手指移动时更新为当前手指的Y值

            private float startY;

           

            //用于记录正常的布局位置

            private Rect originalRect = new Rect();

           

            //手指按下时记录是否可以继续下拉

            private boolean canPullDown = false;

           

            //手指按下时记录是否可以继续上拉

            private boolean canPullUp = false;

           

            //在手指滑动的过程中记录是否移动了布局

            private boolean isMoved = false;

     

            public ReboundScrollView(Context context) {

                   super(context);

            }

           

            public ReboundScrollView(Context context, AttributeSet attrs) {

                   super(context, attrs);

            }

     

            @Override

            protected void onFinishInflate() {

                   if (getChildCount() > 0) {

                           contentView = getChildAt(0);

                   }

            }

           

            @Override

            protected void onLayout(boolean changed, int l, int t, int r, int b) {

                   super.onLayout(changed, l, t, r, b);

                  

                   if(contentView == null) return;

     

                   //ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变

                   originalRect.set(contentView.getLeft(), contentView.getTop(), contentView

                                   .getRight(), contentView.getBottom());

            }

     

            /**

             * 在触摸事件中, 处理上拉和下拉的逻辑

             */

            @Override

            public boolean dispatchTouchEvent(MotionEvent ev) {

                  

                   if (contentView == null) {

                           return super.dispatchTouchEvent(ev);

                   }

     

                   int action = ev.getAction();

                  

                   switch (action) {

                   case MotionEvent.ACTION_DOWN:

                          

                           //判断是否可以上拉和下拉

                           canPullDown = isCanPullDown();

                           canPullUp = isCanPullUp();

                          

                           //记录按下时的Y值

                           startY = ev.getY();

                           break;

                          

                   case MotionEvent.ACTION_UP:

                          

                           if(!isMoved) break;  //如果没有移动布局, 则跳过执行

                          

                           // 开启动画

                           TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(),

                                          originalRect.top);

                           anim.setDuration(ANIM_TIME);

                          

                           contentView.startAnimation(anim);

                          

                           // 设置回到正常的布局位置

                           contentView.layout(originalRect.left, originalRect.top,

                                          originalRect.right, originalRect.bottom);

                          

                           //将标志位设回false

                           canPullDown = false;

                           canPullUp = false;

                           isMoved = false;

                          

                           break;

                   case MotionEvent.ACTION_MOVE:

                          

                           //在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度

                           if(!canPullDown && !canPullUp) {

                                   startY = ev.getY();

                                   canPullDown = isCanPullDown();

                                   canPullUp = isCanPullUp();

                                  

                                   break;

                           }

                          

                           //计算手指移动的距离

                           float nowY = ev.getY();

                           int deltaY = (int) (nowY - startY);

                          

                           //是否应该移动布局

                           boolean shouldMove =

                                          (canPullDown && deltaY > 0)    //可以下拉, 并且手指向下移动

                                          || (canPullUp && deltaY< 0)    //可以上拉, 并且手指向上移动

                                          || (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)

                          

                           if(shouldMove){

                                   //计算偏移量

                                   int offset = (int)(deltaY * MOVE_FACTOR);

                                  

                                   //随着手指的移动而移动布局

                                   contentView.layout(originalRect.left, originalRect.top + offset,

                                                  originalRect.right, originalRect.bottom + offset);

                                  

                                   isMoved = true;  //记录移动了布局

                           }

                          

                           break;

                   default:

                           break;

                   }

     

                   return super.dispatchTouchEvent(ev);

            }

           

     

            /**

             * 判断是否滚动到顶部

             */

            private boolean isCanPullDown() {

                   return getScrollY() == 0 ||

                                   contentView.getHeight() < getHeight() + getScrollY();

            }

           

            /**

             * 判断是否滚动到底部

             */

            private boolean isCanPullUp() {

                   return  contentView.getHeight() <= getHeight() + getScrollY();

            }

           

    }

     

     

    关键点解析

     

    1 判断适合拉伸的时机


    也就是说要判断什么时候开始拉伸, 当然是当ScrollView滑动到顶部或底部的时候。 是否移动到顶部或底部,需要根据三个值进行判断, 这三个值分别是ScrollView的高度, ScrollView中的子控件(在本例中是contentView变量)的高度, 和ScrollView在竖直方向上滚动的距离mScrollY。这三个数值的关系如下图所示:


    其中位于下方的蓝色控件是ScrollView, 位于上方的是contentView。

    所以mScrollY等于0的时候, 就说明ScrollView滚动到了顶部。 如下图所示:

    当contentView.height() = scrollView.height + mScrollY时, 就说明滚动到了底部, 如下图所示:

     

    还有一种情况既可以认为滚动到了底部,也可以认为滚动到顶部, 那就是contentView的高度本身就小于ScrollView的高度, 不需要滑动, 这时满足条件contentView.height() < scrollView.height + mScrollY。 如下图所示:

     

    判断滚动到顶部和滚动到底部,分别由isCanPullDown方法和isCanPullUp方法实现。理解了上面图示的内容, 就可以很容易的理解这两个方法的原理。 这两个方法的代码如下所示:

            /**

             * 判断是否滚动到顶部

             */

            private boolean isCanPullDown() {

                   return getScrollY() == 0 ||

                                   contentView.getHeight() < getHeight() + getScrollY();

            }

           

            /**

             * 判断是否滚动到底部

             */

            private boolean isCanPullUp() {

                   return  contentView.getHeight() <= getHeight() + getScrollY();

            }

     

    在用户按下手指时(也就是ACTION_DOWN事件), 调用上面的两个方法判断是否滚动到了顶部或底部, 如果滚动到了顶部或底部, 就说明在移动手指时需要移动contentView的布局。 这时就用标志位canPullDown和canPullUp 记住这两个个状态, 并且也记住ACTION_DOWN发生时, 手指触摸点的Y值, 就是startY成员变量。具体实现代码如下:

                   case MotionEvent.ACTION_DOWN:

                          

                           //判断是否可以上拉和下拉

                           canPullDown = isCanPullDown();

                           canPullUp = isCanPullUp();

                          

                           //记录按下时的Y值

                           startY = ev.getY();

                           break;

     

    2 布局的移动


    如果上面一步执行ACTION_DOWN之后, 判定不处于上拉或下拉的时机上, 那么在ACTION_MOVE事件处理时, 也要随着ACTION_MOVE事件的多次触发持续更新手指所处的Y值(startY变量)并且及时判断在手指移动的过程中是否使contentView 滚动到了顶部或底部, 如果使contentView滚动到了顶部或底部,那么在下一个ACTION_MOVE事件的触发点, 就要移动布局了。 如果上面一步执行ACTION_DOWN之后, 就已经确定要上拉或下拉布局, 那么在ACTION_MOVE时, 也就要随着手指的移动而移动布局。
    布局移动的实现原理是改变contentView中的mTop和mBottom成员变量的值(这两个变量定义在父类View中),并且对 contentView重新布局。 mTop代表当前控件的顶端到父控件的顶端的距离,mBottom代表当前控件的底端到父控件的顶端的距离 。示意图如下:


    同时改变mTop和mBottom的值, 可以使contentView上下移动, 达到随手指拉伸的效果。代码逻辑如下:

                   case MotionEvent.ACTION_MOVE:

                          

                           //在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度

                           if(!canPullDown && !canPullUp) {

                                   startY = ev.getY();

                                   canPullDown = isCanPullDown();

                                   canPullUp = isCanPullUp();

                                  

                                   break;

                           }

                          

                           //计算手指移动的距离

                           float nowY = ev.getY();

                           int deltaY = (int) (nowY - startY);

                          

                           //是否应该移动布局

                           boolean shouldMove =

                                          (canPullDown && deltaY > 0)    //可以下拉, 并且手指向下移动

                                          || (canPullUp && deltaY< 0)    //可以上拉, 并且手指向上移动

                                          || (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)

                          

                           if(shouldMove){

                                   //计算偏移量

                                   int offset = (int)(deltaY * MOVE_FACTOR);

                                  

                                   //随着手指的移动而移动布局

                                   contentView.layout(originalRect.left, originalRect.top + offset,

                                                  originalRect.right, originalRect.bottom + offset);

                                  

                                   isMoved = true;  //记录移动了布局

                           }

                          

                           break;



    上面代码有三点需要注意: 1 MOVE_FACTOR是一个常量, 定义为0.5F。 这是一个因子, 让手指移动的举例乘以这个因子得到布局移动的距离, 例如手指移动了100px, 那么布局就移动100*0.5 = 50px。 这样做主要是达到一种延迟的效果, 增强用户的体验。 2 调用contentView的layout方法对他重新布局, 传入的originalRect.top + offset 和 originalRect.bottom + offset就是新的mTop和mBottom, originalRect.top和originalRect.bottom是原始的mTop和mBottom。 3 isMove变量用于记住布局已经移动的状态, 以便于在ACTION_UP事件触发时, 将布局回弹到正常位置

    3 布局的回弹


    在用户松开手时, 布局要回弹到原始位置。这个回弹很简单, 就是让mTop和mBottom回到原始的值。并加上动画效果。mTop和mBottom的原始值被记录在一个名为originalRect的Rect对象中。 在对ScrollView进行布局操作的时候, 初始化这个originalRect对象, 代码如下:

            @Override

            protected void onLayout(boolean changed, int l, int t, int r, int b) {

                   super.onLayout(changed, l, t, r, b);

                  

                   if(contentView == null) return;

     

                   //ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变

                   originalRect.set(contentView.getLeft(), contentView.getTop(), contentView

                                   .getRight(), contentView.getBottom());

            }


    该方法首先调用父类的同名方法对ScrollView进行布局,在ScrollView进行布局时, 会对他的子View进行递归布局操作, 也就是说, 在调用完父类的onLayout方法后, contentView也已经完成了布局操作, 这时它的位置是可以确定的。 所以下面就将它的位置信息保存在originalRect对象中。
    回弹效果的实现逻辑如下:

                   case MotionEvent.ACTION_UP:

                          

                           if(!isMoved) break;  //如果没有移动布局, 则跳过执行

                          

                           // 开启动画

                           TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(),

                                          originalRect.top);

                           anim.setDuration(ANIM_TIME);

                          

                           contentView.startAnimation(anim);

                          

                           // 设置回到正常的布局位置

                           contentView.layout(originalRect.left, originalRect.top,

                                          originalRect.right, originalRect.bottom);

                          

                           //将标志位设回false

                           canPullDown = false;

                           canPullUp = false;

                           isMoved = false;

                          

                           break;



    上面代码中ANIM_TIME是一个常量, 代表动画的执行时间, 被定义为300毫秒。 回弹到原始位置后, 将canPullDown,canPullUp和 isMoved标志清空, 以便进行下一次的触摸事件周期。

    其他实现方式


    在网上搜寻解决方案时, 发现以下代码也能实现弹性效果。

    public class ReboundScrollView extends ScrollView {

            private static final int MAX_Y_OVERSCROLL_DISTANCE = 500;

        private Context mContext;

        private int mMaxYOverscrollDistance;

         

        public ReboundScrollView(Context context){

            super(context);

            mContext = context;

            initBounceScrollView();

        }

         

        public ReboundScrollView(Context context, AttributeSet attrs){

            super(context, attrs);

            mContext = context;

            initBounceScrollView();

        }

         

        public ReboundScrollView(Context context, AttributeSet attrs, int defStyle){

            super(context, attrs, defStyle);

            mContext = context;

            initBounceScrollView();

        }

         

        private void initBounceScrollView(){

            final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();

                final float density = metrics.density;

             

            mMaxYOverscrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);

        }

        @Override

        protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent){ 

            //这块是关键性代码

            return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxYOverscrollDistance, isTouchEvent);  

        }

    }


    实现原理就是通过overScrollBy方法设置ScrollView可以过度滚动。 这种实现虽然简单, 但是有以下几个缺陷:
    1 下拉或上拉时, 无法实现延迟效果, 也就是手指移动100px, 那么布局也移动100px, 经过尝试, 这种方式体验并不好, 给人的感觉是控件活动太灵活了。
    2 如果下拉或上拉的举例超过 MAX_Y_OVERSCROLL_DISTANCE设定的值, 布局就不会再随着手指的移动而移动
    3 无法设置自定义的动画, 不能控制动画持续的时间

  • 相关阅读:
    System.Web.Mvc.RoutePrefixAttribute.cs
    HTML5: 实现调用系统拍照或者选择照片并预览
    System.DateTime.cs
    System.Math.cs
    System.Web.UI.WebControls.FileUpload.cs
    系统过程分析
    java实现数字黑洞
    java实现数字黑洞
    java实现数字黑洞
    java实现数字黑洞
  • 原文地址:https://www.cnblogs.com/tangxl/p/3615953.html
Copyright © 2020-2023  润新知