• [置顶] Android仿人人客户端(v5.7.1)——采用ViewGroup做父容器,实现左侧滑动菜单(三)


     转载请标明出处:http://blog.csdn.net/android_ls/article/details/8761410      

            前面已实现以滑动的方式显示或隐藏左侧菜单,采用的父容器是自定义类继承自RelativeLayout来实现的。看到有网友留言说,采用父容器自定义类继承自ViewGroup去实现,这篇我就讲解下采用继承自ViewGroup的方式去如何实现。

            自定义类让其继承自ViewGroup类后,编译器强制添加一个带Context类型参数的构造方法和onLayout()方法,代码如下:

    package com.everyone.android.widget;
    
    import android.content.Context;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * 功能描述:手指在屏幕上左右滑动时,该类的实例负责让其子View根据用户的手势左右偏移(滚动)
     * 父容器采用ViewGroup
     * @author android_ls
     */
    public class HorizontalScrollerContainer extends ViewGroup {
    
        public HorizontalScrollerContainer(Context context) {
            super(context);
        }
       
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {        }
    
    }

              通过阅读上面的代码片段,引发的思考,ViewGroup类有无参构造方法吗?答案是没有;至于onLayout()方法,肯定是一个抽象方法了,那么ViewGroup类肯定是抽象类。在Java的继承体系中,父类(ViewGroup)中有抽象方法,子类继承父类必须(强制性的)去实现父类的抽象方法或者把自己也定义成抽象的。既然onLayout()方法是抽象方法,我们自定义的类去继承了ViewGroup类,最后自定义的类是要用来诞生对象的,那肯定不能也定义成抽象的。我们就去实现它,代码如下:

      @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }

           既然容器有了,我们就向其中添加子View,在Activity的onCreate()方法添加如下代码:

           // 父容器
            HorizontalScrollerContainer mSlideContainer = new HorizontalScrollerContainer(mContext);
            
            // 左侧面板
            LeftPanelLayout mLeftPanelLayout = new LeftPanelLayout(mContext);
            LayoutParams layoutParams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
            // 添加子View
            mSlideContainer.addView(mLeftPanelLayout, layoutParams);
            
            // 设置要显示的视图
            setContentView(mSlideContainer);
    

           我们运行程序,效果图如下:



           看到这张效果图,有人可能要说,我们自定义的父容器HorizontalScrollerContainer类的对象,到底有没有设置到Window(哪来的Window对象呢?查看过Android源码的朋友肯定知道,没翻过源码的见这行: getWindow().setContentView(view),这不是重点。)上,我们为父容器添加一张背景图,代码如下:

      public HorizontalScrollerContainer(Context context) {
            super(context);
            this.setBackgroundResource(R.drawable.v5_3_0_guide_pic1);
        }

    运行效果图:


            通过上面的验证,说明我们的父容器View是已添加到PhoneWindow上了,只是其中的子View没有显示出来。如何才能让父容器中的子View显示出来呢? 我们还得去重写父容器ViewGroup的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,代码如下:

      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
            }
        }

           运行看效果:


            这次明显达到了我们的预期(目的),效果是实现了,到底为什么之前没达到预期效果呢,应用框架层明明强制我(自定义类继承自ViewGroup类)去实现的只有onLayout()方法,我按你(应用框架)的要求去做了,为什么达不到预期效果呢?

           当你为一个Activty 的PhoneWindow设置一个可见的View(ViewGroup),并且运行这个Activty时,应用框架层的调用(控制反转)ViewGroup的显示视图相关方法的顺序:onAttachedToWindow-->onMeasure-->onSizeChanged-->onLayout(注:其它相关的方法我们不关注,这里就没列出来)。下面来解释下这几个我们关注的方法的含义:

            1、onAttachedToWindow:当View附加到一个窗体上时,应用框架层调用此方法。

            2、onMeasure:View自己调用此方法,测量自己及其子View的实际宽度和高度。

            3、onSizeChanged当View大小改变时,调用此方法

            4、onLayout:View自己调用此方法,为所有子View分配显示空间的大小,可能会与子View定义时设置的大小不一样。比如:父容器是LinearLayout,里面的子View排列方向是竖直方式,我向父容器中添加第一个子View(A),设置宽度为fill_parent,高度为50dip;我再向父容器中添加第二个子View(B),设置宽度为fill_parent,高度也为fill_parent。在这种情况下,子View(B)的高度为多少? 是fill_parent吗?肯定不是啦,是父容器的高度减去子View(A)的高度。(可能会与子View定义时设置的大小不一样)

             我的个人观点:既然编写自定义类去继承ViewGroup后,子类必须去实现onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,为什么不把onMeasure()方法在ViewGroup类中定义成抽象的呢?翻看源码,会发现:

    onMeasure()是定义在View类中的,源码如下:

       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }

             ViewGroup类完全可以重写父类的onMeasure()方法,将其修改成抽象方法。那么当我们想自定义类去继承ViwGroup类,都会去实现onMeasure()方法,就不会出现按应用框架要求的去做,而没达到预期的,我们想要的结果。

           编写两个类,进行测试,TestA类源码:

    package com.everyone.android.widget;
    
    public class TestA { 
        
     public void test(){
         System.out.println("Hello World");
     } 
     
    }

           TestB类源码:

    package com.everyone.android.widget;
    
    public abstract class TestB extends TestA{
        public abstract void test();
    }
    

            通过上面的测试,大家看到这么写,子类是可以修改父类的方法修饰词的。

           假如:Android应用框架里,ViewGroup类里重写了父类的onMeasure()方法,在ViewGroup类中添加如下代码:

     protected abstract void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

            我们自已的类继承ViewGroup后,我们实现onMeasure()方法的代码如下:

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);
            setMeasuredDimension(width, height);
            
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
            }
        }

              至于Android应用框架层为什么没按我们预想的那样去实现,自然有他们的道理,呵呵。我个人觉的若在子类中必须重写父类的某个方法后,才既能满足框架层又能让使用框架的达到预期的效果。干嘛不把该方法定义成抽象的呢?何乐而不为。
            好了,到这里这一篇的重点聊完了。其它的关于用户触摸屏幕事件的分发、拦截和处理(响应)与前面的采用RelativeLayout做父容器时是一样的。

            采用ViewGroup做父容器,实现水平滑动显示或隐藏左侧菜单的完整代码如下:

    package com.everyone.android.widget;
    
    import android.content.Context;
    import android.util.Log;
    import android.util.TypedValue;
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewGroup;
    import android.widget.Scroller;
    
    /**
     * 功能描述:手指在屏幕上左右滑动时,该类的实例负责让其子View根据用户的手势左右偏移(滚动)
     * 父容器采用ViewGroup
     * @author android_ls
     */
    public class HorizontalScrollerContainer extends ViewGroup {
    
        private static final String TAG = "ScrollerContainer";
    
        private Scroller mScroller;
    
        private VelocityTracker mVelocityTracker;
    
        /**
         * 手柄(手把)的宽度
         */
        private int mHandlebarWidth;
    
        /**
         * 在偏移过程中,动画持续的时间
         */
        private static final int ANIMATION_DURATION_TIME = 300;
        
        /**
         * 记录当前的滑动结束后的状态,左侧面板是否可见
         * true  向右滑动(左侧面板处于可见)
         * false 向左滑动(左侧面板处于不可见)
         */
        private boolean mPanelInvisible;
        
        /**
         * 是否已滑动结束
         */
        private boolean mFinished;
        
        /**
         * 是否允许滚动
         * 满足的条件:
         *     左侧面板可见,当前手指按下的坐标x值 ,是在手柄宽度范围内;
         *     左侧面板不可见,当前手指按下的坐标x值 < 手柄宽度
         */
        private boolean mAllowScroll;
        
        /**
         * 是否满足响应单击事件的条件
         * 满足的条件:左侧面板可见,当前手指按下的坐标x值 ,是在手柄宽度范围内
         */
        private boolean isClick;
        
        public HorizontalScrollerContainer(Context context) {
            super(context);
            // this.setBackgroundResource(R.drawable.v5_3_0_guide_pic1);
            
            mScroller = new Scroller(context);
            mHandlebarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 70, getResources().getDisplayMetrics());
        }
        
        // 测量自己及其子View的实际宽度和高度
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
            }
        }
        
        /*
         * 为所有子View分配显示空间的大小 ,可能会与子View定义时设置的大小不一样。
         * 比如:父容器是LinearLayout,里面的子View排列方向是竖直方式,我向父容器中添加第一个子View(A),
         * 设置宽度为fill_parent,高度为50dip;我再向父容器中添加第二个子View(B),设置宽度为fill_parent,高度也为fill_parent。
         * 在这种情况下,子View(B)的高度为多少? 是fill_parent吗?肯定不是啦,是父容器的高度减去子View(A)的高度。
         * (可能会与子View定义时设置的大小不一样)
         * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)
         */
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }
        
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.e(TAG, "dispatchTouchEvent()");
            
            switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatchTouchEvent():  ACTION_DOWN");
                
                mFinished = mScroller.isFinished();
                if(mFinished){
                    int x = (int) ev.getX();
                    int width = getWidth();
                    
                    if(mPanelInvisible)// 左侧面板可见
                    {
                        if(x > (width - mHandlebarWidth)){ // 当前手指按下的坐标x值 ,是在手柄宽度范围内
                            isClick = true;
                            mAllowScroll = true;
                            return true;
                        } else {
                            isClick = false;
                            mAllowScroll = false;
                        }
                    } else { // 左侧面板不可见
                        if(x < mHandlebarWidth ){ // 当前手指按下的坐标x值 < 手柄宽度 (也就是说在手柄宽度范围内,是可以相应用户的向右滑动手势)
                            mAllowScroll = true;
                        }else{
                            mAllowScroll = false;
                        }
                    }
                    
                } else {
                    // 当前正在滚动子View,其它的事不响应
                    return false;
                }
                
                break;
    
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatchTouchEvent():  ACTION_MOVE");
                int margin = getWidth() - (int) ev.getX();
                if (margin < mHandlebarWidth && mAllowScroll) {
                    
                    Log.e(TAG, "dispatchTouchEvent ACTION_MOVE margin = " + margin + "\t mHandlebarWidth = " + mHandlebarWidth);
                    return true;
                }
                
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatchTouchEvent():  ACTION_UP");
                
                if (isClick && mPanelInvisible && mAllowScroll) {
                    isClick = false;
                    mPanelInvisible = false;
                    
                    int scrollX = getChildAt(1).getScrollX();
                    mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);
                    invalidate();
                    
                    return true;
                }
                
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.i(TAG, "dispatchTouchEvent():  ACTION_CANCEL");
                break;
            default:
                break;
            }
            
            return super.dispatchTouchEvent(ev);
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.e(TAG, "onInterceptTouchEvent()");
            
            switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onInterceptTouchEvent():  ACTION_DOWN");
                mFinished = mScroller.isFinished();
                if(!mFinished){
                    return false;
                }
                
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onInterceptTouchEvent():  ACTION_MOVE");
                
                mVelocityTracker = VelocityTracker.obtain();
                mVelocityTracker.addMovement(ev);
                
                // 一秒时间内移动了多少个像素
                mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());
                float velocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;
                Log.d(TAG, "onInterceptTouchEvent():  mVelocityValue = " + velocityValue);
                
                if (velocityValue > 300 && mAllowScroll) {
                    return true;
                }
                
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onInterceptTouchEvent():  ACTION_UP");
                
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.i(TAG, "onInterceptTouchEvent():  ACTION_CANCEL");
                break;
            default:
                break;
            }
            
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e(TAG, "onTouchEvent()");
    
            float x = event.getX();
            
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent():  ACTION_DOWN");
                mFinished = mScroller.isFinished();
                if(!mFinished){
                    return false;
                }
                break;
    
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent():  ACTION_MOVE");
                getChildAt(1).scrollTo(-(int)x, 0);
                break;
    
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onTouchEvent():  ACTION_UP");
                
                if(!mAllowScroll){
                    break;
                }
                
               float width = getWidth();
               // 响应滚动子View的临界值,若觉得响应过于灵敏,可以将只改大些。
               // 比如:criticalWidth = width / 3或criticalWidth = width / 2,看情况而定,呵呵。
               float criticalWidth = width / 5;
               
               Log.i(TAG, "onTouchEvent():  ACTION_UP x = " + x + "\t criticalWidth = " + criticalWidth);
               
               int scrollX = getChildAt(1).getScrollX();
               
               if ( x < criticalWidth) {
                   Log.i(TAG, "onTouchEvent():  ACTION_UP 向左滑动");
                   
                    mPanelInvisible = false;
                   
                    mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);
                    invalidate();
                } else if ( x > criticalWidth){
                    Log.i(TAG, "onTouchEvent():  ACTION_UP 向右滑动");
                  
                    mPanelInvisible = true;
                    
                    int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);
                    mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
                    invalidate();
                }
                
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.i(TAG, "onTouchEvent():  ACTION_CANCEL");
                break;
            default:
                break;
            }
            
            return super.onTouchEvent(event);
        }
    
        @Override
        public void computeScroll() {
            // super.computeScroll();
            
            if(mScroller.computeScrollOffset()){
                this.getChildAt(1).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                this.postInvalidate();
            }
        }
    
        /**
         * 向右滑动View,让左侧操作面饭可见
         */
        public void slideToRight() {
            mFinished = mScroller.isFinished();
            if(mFinished && !mPanelInvisible){
                mPanelInvisible = true;
                
                float width = getWidth();
                int scrollX = getChildAt(1).getScrollX();
                int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);
                
                mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
                invalidate();
            }
        }
        
        /**
         * View滑动事件监听器
         * @author android_ls
         */
        public interface OnSlideListener {
            /**
             * 向左滑动子View
             */
            public abstract void toLeft();
            
            /**
             * 向右滑动子View
             */
            public abstract void toRight();
        }
        
    }
    

          测试的Activity源码如下:

    package com.everyone.android.ui;
    
    import android.os.Bundle;
    import android.view.ViewGroup.LayoutParams;
    
    import com.everyone.android.AppBaseActivity;
    import com.everyone.android.widget.FreshNewsLayout;
    import com.everyone.android.widget.HorizontalScrollerContainer;
    import com.everyone.android.widget.HorizontalScrollerContainer.OnSlideListener;
    import com.everyone.android.widget.LeftPanelLayout;
    
    /**
     * 功能描述:应用主界面
     * @author android_ls
     *
     */
    public class EveryoneActivity extends AppBaseActivity implements OnSlideListener {
    
        /**
         * 滚动(滑动)容器
         */
        private HorizontalScrollerContainer mSlideContainer;
    
        /**
         * 左侧面板
         */
        private LeftPanelLayout mLeftPanelLayout;
    
        /**
         * 新鲜事
         */
        private FreshNewsLayout mFreshNewsLayout;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(mSlideContainer);
           
        }
    
        @Override
        protected int getLayoutId() {
            return 0;
        }
    
        @Override
        protected void setupView() {
            mSlideContainer = new HorizontalScrollerContainer(mContext);
    
            mLeftPanelLayout = new LeftPanelLayout(mContext);
            mFreshNewsLayout = new FreshNewsLayout(mContext);
            mFreshNewsLayout.setOnSlideListener(this);
    
            LayoutParams layoutParams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
            mSlideContainer.addView(mLeftPanelLayout, layoutParams);
            mSlideContainer.addView(mFreshNewsLayout, layoutParams);
        }
    
        @Override
        protected void initialized() {
            // TODO Auto-generated method stub
    
        }
    
        @Override
        public void toLeft() {
            // TODO Auto-generated method stub
    
        }
    
        @Override
        public void toRight() {
            mSlideContainer.slideToRight();
        }
    
    }

           运行效果图:


  • 相关阅读:
    ThinkPHP---thinkphp视图(V)
    PHP封装文件上传
    小数据池、深浅拷贝、集合
    字典、字典嵌套
    列表、元组、range
    字符串详解:整型、可变数据类型和不可变数据类型、进制转换、索引、切片、步长、字符串方法、进制转换
    while循环、break、格式化、运算符、编码初始
    强烈推荐一款CSS导航菜单
    解决WEB页面上"焦点控制"一法
    新闻发布项目——后台JSP界面adminManage/manageTopic.jsp
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/3001624.html
Copyright © 2020-2023  润新知