美文网首页
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