美文网首页
ViewGroup做导航栏-----------开源中国客户端导

ViewGroup做导航栏-----------开源中国客户端导

作者: pkxutao | 来源:发表于2017-11-29 14:50 被阅读5次

    众所周知,官方已经不推荐用TabActivity了,推荐用fragment,具体原因不去纠结。前段时间下载了开源中国的源码看了下,发现导航栏做法挺有意思的(或者很多人都这样做了只是我没发现),于是抽离了出来研究了下,下面来剖析具体做法。
    先看布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="#ffffff">
        
        <include layout="@layout/main_header" />   
        
        <com.example.testandroid.ScrollLayout   
          android:id="@+id/main_scrolllayout"    
          android:layout_width="fill_parent"    
          android:layout_height="fill_parent"
          android:layout_weight="1">    
      
          <include layout="@layout/frame_news" />
          
          <include layout="@layout/frame_question" />
          
          <include layout="@layout/frame_tweet" />
              
          <include layout="@layout/frame_active" />
           
        </com.example.testandroid.ScrollLayout> 
        
        <include layout="@layout/main_footer" />   
    
    </LinearLayout>
    

    很简单,结构也很清晰就是一个LinearLayout包含三个布局,上面一个头layout,中间一个自定义的layout,下面一个footlayout。头layout不说了,里面内容可以任意写,甚至可以把这个layout去掉,footlayout代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal" 
        android:id="@+id/main_linearlayout_footer"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:background="@drawable/widget_bar_bg_n">
        <RadioButton 
            android:id="@+id/main_footbar_news"
            style="@style/main_footbar_radio"
            android:drawableTop="@drawable/widget_bar_news"/>
        <ImageView
            style="@style/main_footbar_cutline"
            android:src="@drawable/widget_bar_cut_off"/>
        <RadioButton 
            android:id="@+id/main_footbar_question"
            style="@style/main_footbar_radio"
            android:drawableTop="@drawable/widget_bar_question"/>
        <ImageView
            style="@style/main_footbar_cutline"
            android:src="@drawable/widget_bar_cut_off"/>
        <RadioButton 
            android:id="@+id/main_footbar_tweet"
            style="@style/main_footbar_radio"
            android:drawableTop="@drawable/widget_bar_tweet"/>
        <ImageView
            style="@style/main_footbar_cutline"
            android:src="@drawable/widget_bar_cut_off"/>
        <RadioButton
            android:id="@+id/main_footbar_active"
            style="@style/main_footbar_radio"
            android:drawableTop="@drawable/widget_bar_active"/>
        <ImageView
            style="@style/main_footbar_cutline"
            android:src="@drawable/widget_bar_cut_off"/>
        <ImageView
            android:id="@+id/main_footbar_setting"
            style="@style/main_footbar_image"
            android:src="@drawable/widget_bar_more"/>
    </LinearLayout>
    

    很简单,就是radiobutton,底部导航栏的布局。
    重点在自定义布局ScrollLayout 。
    先看onMeasure中是怎样计算每个view大小的:

         @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //Log.e(TAG, "onMeasure");
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            final int width = MeasureSpec.getSize(widthMeasureSpec);
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            if (widthMode != MeasureSpec.EXACTLY) {
                throw new IllegalStateException(
                        "ScrollLayout only canmCurScreen run at EXACTLY mode!");
            }
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            if (heightMode != MeasureSpec.EXACTLY) {
                throw new IllegalStateException(
                        "ScrollLayout only can run at EXACTLY mode!");
            }
    
            // The children are given the same width and height as the scrollLayout
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
            }
            // Log.e(TAG, "moving to screen "+mCurScreen);
            scrollTo(mCurScreen * width, 0);
        }
    

    通过MeasureSpec.getSize和MeasureSpec.getMode分别得到了view的宽度和宽度模式。
    onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。我们需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size= MeasureSpec.getSize(widthMeasureSpec)。
    这个mode有三种:

    UNSPECIFIED:父VIEW对子VIEW无任何约束,子VIEW可以为任意大小
    EXACTLY :固定大小
    AT_MOST:子VIEW可以达到最大size。
    

    这里如果宽高的模式不是固定大小则抛出异常,scrollTo函数在这里被调用是可以打开应用默认为第N个view。
    接着来看onLayout是怎样布局的:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int childLeft = 0;
            final int childCount = getChildCount();//得到子view的数目
            for (int i = 0; i < childCount; i++) {
                final View childView = getChildAt(i);
                if (childView.getVisibility() != View.GONE) {
                    final int childWidth = childView.getMeasuredWidth();//得到子view宽度,在这里得到的宽度是屏幕的宽度
                    Log.e(TAG, "childWidth:"+ childWidth+",childHeight:"+childView.getMeasuredHeight());
                    childView.layout(childLeft, 0, childLeft + childWidth,
                            childView.getMeasuredHeight());//布局子view位置,
                    childLeft += childWidth;
                }
            }
        }
    

    UI完成后再来看滑动逻辑(推荐先看我之前的关于手势分发的代码)。
    onInterceptTouchEvent代码:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
            //Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);
            final int action = ev.getAction();
            if ((action == MotionEvent.ACTION_MOVE)
                    && (mTouchState != TOUCH_STATE_REST)) {//如果是move事件且view在运动状态,则直接返回true,手势由onTouchEvent处理
                return true;
            }
            final float x = ev.getX();
            final float y = ev.getY();
            switch (action) {
            case MotionEvent.ACTION_MOVE:
                final int xDiff = (int) Math.abs(mLastMotionX - x);//滑动距离
                if (xDiff > mTouchSlop) {
                    mTouchState = TOUCH_STATE_SCROLLING;//如果大于一个阈值,则把view状态设置为运动状态,然后把手势交给onTouchEvent处理
                }
                break;
            case MotionEvent.ACTION_DOWN:
                mLastMotionX = x;
                mLastMotionY = y;
                mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
                        : TOUCH_STATE_SCROLLING;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchState = TOUCH_STATE_REST;
                break;
            }
            return mTouchState != TOUCH_STATE_REST;
        }
    
    mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    

    意思就是这个距离是我们认为用户想滑动屏幕之前的像素距离,通俗点来说就是超过这个值就可以判断用户是想滑动屏幕,而不是点击或者误触。
    onTouchEvent代码就不贴了,里面有涉及到滑动速度问题,以后再分享。具体就是调用scrollBy移动view,使view能跟随手指滑动。
    scrollBy和scrollTo的区别:

    滑动到某一位置,为相对位置,如果参数为(10,10),则x和y方向分别滑动10单位距离,而不是滑动到(10,10)这个坐标。

    滑动到某一位置,为绝对位置,如果参数为(10,10),则滑动到(10,10)这个坐标。

    •scrollTo就是把View移动到屏幕的X和Y位置,也就是绝对位置。而scrollBy其实就是调用的scrollTo,但是参数是当前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy调用的是相对于mScrollX和mScrollY的位置。我们在上面的代码中可以看到当我们手指不放移动屏幕时,就会调用scrollBy来移动一段相对的距离。而当我们手指松开后,会调用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0,duration);来产生一段动画来移动到相应的页面,在这个过程中系统回不断调用computeScroll(),我们再使用scrollTo来把View移动到当前Scroller所在的绝对位置。
    这样,子view就能根据跟随我们的手指滑动了,如果点击导航上的某个按钮,可以调用startScroll函数进行滑动动画,并且在滑动结束后调用OnViewChange接口来告诉Activity:我执行了滑动,你可以执行更新内容等动作了。
    到此,导航栏布局已经完成,支持跟随手指滑动、滑动动画等,这种做法很简单也很实用。

    相关文章

      网友评论

          本文标题:ViewGroup做导航栏-----------开源中国客户端导

          本文链接:https://www.haomeiwen.com/subject/avgubxtx.html