Bootstrap

开源项目: FlycoTabLayout

开源项目效果

调用实例

必练基本功

Android studio 项目导入依赖compile路径

dependencies{
    compile 'com.android.support:support-v4:23.1.1'
    compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.2@aar'
}

FlycoTabLayout是一个Android TabLayout库,目前有3个TabLayout

  • SlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.关于PagerSlidingTabStrip源码分析可以参照我以前的一篇博文http://blog.csdn.net/analyzesystem/article/details/50316745
    • 新增部分属性
    • 新增支持多种Indicator显示器
    • 新增支持未读消息显示
    • 新增方法setViewPager
 /** 关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况 */
    public void setViewPager(ViewPager vp, String[] titles)

    /** 关联ViewPager,用于连适配器都不想自己实例化的情况 */
    public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList<Fragment> fragments) 
  • CommonTabLayout:不同于SlidingTabLayout对ViewPager依赖,它是一个不依赖ViewPager可以与其他控件自由搭配使用的TabLayout.

    • 支持多种Indicator显示器,以及Indicator动画
    • 支持未读消息显示
    • 支持Icon以及Icon位置
    • 新增方法
/** 关联数据支持同时切换fragments */
    public void setTabData(ArrayList<CustomTabEntity> tabEntitys, FragmentManager fm, int containerViewId, ArrayList<Fragment> fragments)
  • SegmentTabLayout:仿照QQ消息列表头部tab切换的控件

自定义属性表

tl_indicator_color          color       设置显示器颜色
tl_indicator_height         dimension   设置显示器高度
tl_indicator_width          dimension   设置显示器固定宽度
tl_indicator_margin_left    dimension   设置显示器margin,当indicator_width大于0,无效
tl_indicator_margin_top     dimension   设置显示器margin,当indicator_width大于0,无效
tl_indicator_margin_right   dimension   设置显示器margin,当indicator_width大于0,无效
tl_indicator_margin_bottom  dimension   设置显示器margin,当indicator_width大于0,无效
tl_indicator_corner_radius  dimension   设置显示器圆角弧度
tl_indicator_gravity        enum        设置显示器上方(TOP)还是下方(BOTTOM),只对常规显示器有用
tl_indicator_style          enum        设置显示器为常规(NORMAL)或三角形(TRIANGLE)或背景色块(BLOCK)
tl_underline_color          color       设置下划线颜色
tl_underline_height         dimension   设置下划线高度
tl_underline_gravity        enum        设置下划线上方(TOP)还是下方(BOTTOM)
tl_divider_color            color       设置分割线颜色
tl_divider_width            dimension   设置分割线宽度
tl_divider_padding          dimension   设置分割线的paddingTop和paddingBottom
tl_tab_padding              dimension   设置tab的paddingLeft和paddingRight
tl_tab_space_equal          boolean     设置tab大小等分
tl_tab_width                dimension   设置tab固定大小
tl_textsize                 dimension   设置字体大小
tl_textSelectColor          color       设置字体选中颜色
tl_textUnselectColor        color       设置字体未选中颜色
tl_textBold                 boolean     设置字体加粗
tl_iconWidth                dimension   设置icon宽度(仅支持CommonTabLayout)
tl_iconHeight               dimension   设置icon高度(仅支持CommonTabLayout)
tl_iconVisible              boolean     设置icon是否可见(仅支持CommonTabLayout)
tl_iconGravity              enum        设置icon显示位置,对应Gravity中常量值,左上右下(仅支持CommonTabLayout)
tl_iconMargin               dimension   设置icon与文字间距(仅支持CommonTabLayout)
tl_indicator_anim_enable    boolean     设置显示器支持动画(only for CommonTabLayout)
tl_indicator_anim_duration  integer     设置显示器动画时间(only for CommonTabLayout)
tl_indicator_bounce_enable  boolean     设置显示器支持动画回弹效果(only for CommonTabLayout)
tl_indicator_width_equal_title  boolean 设置显示器与标题一样长(only for SlidingTabLayout)

该库依赖于动画兼容库NineOldAndroidsFlycoRoundView,稍后在源码分析里简单了解一下FlycoRoundView。

SlidingTabLayout调用

SlidingTabLayout自定义属性支持下划线设置,控制下划线显示方向宽高,可以让线宽=文字宽度,也可以固定比例宽度,可以设置未读消息的小红点,也可以设置未读消息数量,当前这一切的前提都是基于ViewPager来实现,都需要绑定ViewPager,通过多种绑定方法

 /**关联ViewPager,Adapter重写了getPageTitle方法*/
 tabLayout.setViewPager(vp);

 /**关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况*/
 tabLayout.setViewPager(vp, mTitles);

 /**关联ViewPager,用于连适配器都不想自己实例化的情况,内部帮助实例化了一个InnerPagerAdapter*/
 tabLayout.setViewPager(vp, mTitles, this, mFragments);

下面我们来看看tabLayout提供几个对我们比较有用的方法

/**显示指定位置未读红点*/
tabLayout.showDot(4);
/**隐藏指定位置未读红点或消息*/
tabLayout.hideMsg(5);
/**showMsg(int position, int num):position位置,num小于等于0显示红点,num大于0显示数字,作用:显示未读消息,如果消息数量>99,显示效果99+*/
tabLayout.showMsg(3, 5);
/**  setMsgMargin(int position, float leftPadding, float bottomPadding)设置未读消息偏移,原点为文字的右上角.当控件高度固定,消息提示位置易控制,显示效果佳 */
tabLayout.setMsgMargin(3, 0, 10);

/**设置未读消息消息的背景*/
 MsgView msgView = tabLayout.getMsgView(3);
 if (msgView != null) {
     msgView.setBackgroundColor(Color.parseColor("#6D8FB0"));
  }
//...................略...........

自定义的属性那么多,对应的set方法自然也不少,不过对照上面自定义属性xml引用就好,一般情况下哪些方法都用不到了。

CommonTabLayout调用

SlidingTabLayout对应的方法在这里都适用不再重复,CommonTabLayout最重要的就是setTabData(ArrayList tabEntitys)方法,使得CommonTabLayout不再依赖于ViewPager完成初始化,实现底部导航或者头部导航效果,让我们告别RadioButton+ViewPager的时代,CustomTabEntity的命名有点问题哈,命名是一个接口非要定义Entity结尾,TabEntity实现该接口,修改构造方法,初始化内部参数,下面是一个配合CommonTabLayout+ViewPager的导航实例


        mFragmentList = addFragmentList(R.id.home_frameLayout, fragmentClasses);

        for (int i = 0; i < titles.length; i++) {
            mTabEntities.add(new TabEntity(titles[i], checkeds[i], normals[i]));
        }

        commonTabLayout.setTabData(mTabEntities);
        commonTabLayout.setOnTabSelectListener(new OnTabSelectListener() {
            @Override
            public void onTabSelect(int position) {

                if (position == 1) {
                    topBarBuilder.configSearchStyle(titles[position], R.drawable.ic_action_search);
                } else {
                    topBarBuilder.configTitle(titles[position]);
                }

                showFragment(R.id.home_frameLayout, position, mFragmentList);
                onLoggerD("initial callback ,show fragment with position " + position + ";FragmentName:" + mFragmentList.get(position).toString());
            }

            @Override
            public void onTabReselect(int position) {
                //TODO 重选
            }
        });
SegmentTabLayout调用

SegmentTabLayout实现效果就像qq消息列表顶部的切换效果,统一支持setTabData(mTitles)不过这里传入标题数组,tabLayout配合ViewPager切换调用tabLayout提供的方法setCurrentTab,SegmentTabLayout提供的 setTabData(String[] titles, FragmentActivity fa, int containerViewId, ArrayList fragments)方法在我们frameLayout+fragment布局切换Fragment比较实用的

源码分析

FlycoRoundView源码分析

粗略看过这个库的自定义属性文件,明悟了件事:自定义属性的自动提示如下(以前我自定义属性都放在declare-styleable直接定义的)

下面是这些自定义属性的具体含义(attrs里面有具体说明)


<!-- 以下是重用attr的正确姿势,一切为了在布局中可以自动提示-->
    <!-- 圆角矩形背景色 -->
    <attr name="rv_backgroundColor" format="color"/>
    <!-- 圆角矩形背景色press -->
    <attr name="rv_backgroundPressColor" format="color"/>
    <!-- 圆角弧度,单位dp-->
    <attr name="rv_cornerRadius" format="dimension"/>
    <!-- 圆角弧度,单位dp-->
    <attr name="rv_strokeWidth" format="dimension"/>
    <!-- 圆角边框颜色-->
    <attr name="rv_strokeColor" format="color"/>
    <!-- 圆角边框颜色press -->
    <attr name="rv_strokePressColor" format="color"/>
    <!-- 文字颜色press-->
    <attr name="rv_textPressColor" format="color"/>
    <!-- 圆角弧度是高度一半-->
    <attr name="rv_isRadiusHalfHeight" format="boolean"/>
    <!-- 圆角矩形宽高相等,取较宽高中大值-->
    <attr name="rv_isWidthHeightEqual" format="boolean"/>
    <!-- 圆角弧度,单位dp,TopLeft-->
    <attr name="rv_cornerRadius_TL" format="dimension"/>
    <!-- 圆角弧度,单位dp,TopRight-->
    <attr name="rv_cornerRadius_TR" format="dimension"/>
    <!-- 圆角弧度,单位dp,BottomLeft-->
    <attr name="rv_cornerRadius_BL" format="dimension"/>
    <!-- 圆角弧度,单位dp,BottomRight-->
    <attr name="rv_cornerRadius_BR" format="dimension"/>
    <!-- 是否有Ripple效果,api21+有效-->
    <attr name="rv_isRippleEnable" format="boolean"/>

    <declare-styleable name="RoundTextView">
        <attr name="rv_backgroundColor"/>
        <attr name="rv_backgroundPressColor"/>
        <attr name="rv_cornerRadius"/>
        <attr name="rv_strokeWidth"/>
        <attr name="rv_strokeColor"/>
        <attr name="rv_strokePressColor"/>
        <attr name="rv_textPressColor"/>
        <attr name="rv_isRadiusHalfHeight"/>
        <attr name="rv_isWidthHeightEqual"/>
        <attr name="rv_cornerRadius_TL"/>
        <attr name="rv_cornerRadius_TR"/>
        <attr name="rv_cornerRadius_BL"/>
        <attr name="rv_cornerRadius_BR"/>
        <attr name="rv_isRippleEnable"/>
    </declare-styleable>

Round系列的View控件自定义 RoundTextView 、RoundFrameLayout 、RoundLinearLayout RoundRelativeLayout,这几个控件内部源码实现并不复杂,可以说简单之极,构造函数通过RoundViewDelegate代理解析自定义属性,其次就是onMeasure和onLayout的测量相关的,ToggleButton源码分析一篇有提到这里使用EXACTLY

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (delegate.isWidthHeightEqual() && getWidth() > 0 && getHeight() > 0) {
            int max = Math.max(getWidth(), getHeight());
            int measureSpec = MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY);
            super.onMeasure(measureSpec, measureSpec);
            return;
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (delegate.isRadiusHalfHeight()) { //如果弧度是高度的一半,直接设置radio为高度一半,否则调用
            delegate.setCornerRadius(getHeight() / 2);
        }else {
            delegate.setBgSelector();// Ripple效果兼容21+,Ripple效果的实现有多重,这里使用的RippleDrawable,具体使用方法请参考api,这里不是本篇重点
        }
    }

setBgSelector 方法使用到了GradientDrawable、StateListDrawable,没了解过的可以参考Drawable系列这篇博客有相应的简单介绍,RoundViewDelegate提供了这些自定义属性的set get方法,我们代码调用通过自定义控件getDelegate获取构造函数初始化的实例进行设置和获取对应属性。

FlycoTabLayout Lib库源码分析

MsgView仿照FlycoRoundView库编写的一个自定义控件,主要用于未读消息的展示,大同小异的代码就此略过。

SlidingTabLayout自定义控件千篇一律的自定义属性飘过,来到必经之路setViewPager,发现新大陆notifyDataSetChanged()调用,通过dapter的getPageTitle方法获取标题,并inflate添加一个布局到HorizontalScrollView的子View LinearLayout,并绑定Tab想的监听回调。

 /** 更新数据 */
 public void notifyDataSetChanged() {
        mTabsContainer.removeAllViews();
        this.mTabCount = mTitles == null ? mViewPager.getAdapter().getCount() : mTitles.length;
        View tabView;
        for (int i = 0; i < mTabCount; i++) {
            if (mViewPager.getAdapter() instanceof CustomTabProvider) {
                tabView = ((CustomTabProvider) mViewPager.getAdapter()).getCustomTabView(this, i);
            } else {
                tabView = View.inflate(mContext, R.layout.layout_tab, null);
            }

            CharSequence pageTitle = mTitles == null ? mViewPager.getAdapter().getPageTitle(i) : mTitles[i];
            addTab(i, pageTitle.toString(), tabView);
        }

        updateTabStyles();
    }

/** 创建并添加tab */
private void addTab(final int position, String title, View tabView) {
        TextView tv_tab_title = (TextView) tabView.findViewById(R.id.tv_tab_title);
        if (tv_tab_title != null) {
            if (title != null) tv_tab_title.setText(title);
        }

        tabView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mViewPager.getCurrentItem() != position) {
                    mViewPager.setCurrentItem(position);
                    if (mListener != null) {
                        mListener.onTabSelect(position);
                    }
                } else {
                    if (mListener != null) {
                        mListener.onTabReselect(position);
                    }
                }
            }
        });

        /** 每一个Tab的布局参数 */
        LinearLayout.LayoutParams lp_tab = mTabSpaceEqual ?
                new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f) :
                new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        if (mTabWidth > 0) {
            lp_tab = new LinearLayout.LayoutParams((int) mTabWidth, LayoutParams.MATCH_PARENT);
        }

        mTabsContainer.addView(tabView, position, lp_tab);
    }

calcIndicatorRect方法根据不同的属性计算出Rect范围以便以绘制,具体绘制方法参考Canvas API,这里提一点下面这个方法非常有必要,如果在自定义控件的构造函数或者其他绘制相关地方使用系统依赖的代码,会导致可视化编辑器无法报错并提示:Use View.isInEditMode() in your custom views to skip code when shown in Eclipseis,加上了isInEditMode的判断就不会再报错了。

 if (isInEditMode() || mTabCount <= 0) {
            return;
        }

对于代码设置自定义的属性值,会调用下面这两个方法 invalidate()和 updateTabStyles();涉及到了绘制的则调用invalidate,可以直接修改的则调用updateTabStyles(个人感觉不太友好,比如代码设置一个属性值,就需要遍历所有view,同时重新调用属性赋值,关键只修改了其中一个属性!!)

  private void updateTabStyles() {
        for (int i = 0; i < mTabCount; i++) {
            View v = mTabsContainer.getChildAt(i);
//            v.setPadding((int) mTabPadding, v.getPaddingTop(), (int) mTabPadding, v.getPaddingBottom());
            TextView tv_tab_title = (TextView) v.findViewById(R.id.tv_tab_title);
            if (tv_tab_title != null) {
                tv_tab_title.setTextColor(i == mCurrentTab ? mTextSelectColor : mTextUnselectColor);
                tv_tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextsize);
                tv_tab_title.setPadding((int) mTabPadding, 0, (int) mTabPadding, 0);
                if (mTextAllCaps) {
                    tv_tab_title.setText(tv_tab_title.getText().toString().toUpperCase());
                }

                if (mTextBold) {
                    tv_tab_title.getPaint().setFakeBoldText(mTextBold);
                }
            }
        }
    }

setMsg 、setDot方法在调用示例有提到用法,这里的show hide状态如何保存的呢?这里有用到和AbsListView内部状态保持一样的方法:SparseArray来保存对应位置的状态

    /**
     * 显示未读消息
     *
     * @param position 显示tab位置
     * @param num      num小于等于0显示红点,num大于0显示数字
     */
    public void showMsg(int position, int num) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }

        View tabView = mTabsContainer.getChildAt(position);
        MsgView tipView = (MsgView) tabView.findViewById(R.id.rtv_msg_tip);
        if (tipView != null) {
            UnreadMsgUtils.show(tipView, num);

            if (mInitSetMap.get(position) != null && mInitSetMap.get(position)) {
                return;
            }

            setMsgMargin(position, 4, 2);
            mInitSetMap.put(position, true);
        }
    }

    /**
     * 显示未读红点
     *
     * @param position 显示tab位置
     */
    public void showDot(int position) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }
        showMsg(position, 0);
    }

    /** 隐藏未读消息 */
    public void hideMsg(int position) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }

        View tabView = mTabsContainer.getChildAt(position);
        MsgView tipView = (MsgView) tabView.findViewById(R.id.rtv_msg_tip);
        if (tipView != null) {
            tipView.setVisibility(View.GONE);
        }
    }

细心的你一定会发现show并没有直接控制View的Visibility显示隐藏,而是用了UnreadMsgUtils,这个类提供了两个方法setSize和show方法,show方法对show的countSize进行了一次判断转换0为点,1-99圆+数字,>99则显示99+,背景的弧度为宽度的一半

再回到setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)方法,在我们调用该方法时内部帮我们创建了内部定义的InnerPagerAdapter适配器,如果你想偷懒不想写适配器就可以调用这个方法。InnerPagerAdapter重写了getPageTitle,以便于notifyDataSetChanged方法调用动态添加tab项。

 class InnerPagerAdapter extends FragmentPagerAdapter {
        private ArrayList<Fragment> fragments = new ArrayList<>();
        private String[] titles;

        public InnerPagerAdapter(FragmentManager fm, ArrayList<Fragment> fragments, String[] titles) {
            super(fm);
            this.fragments = fragments;
            this.titles = titles;
        }

        @Override
        public int getCount() {
            return fragments.size();
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return titles[position];
        }

        @Override
        public Fragment getItem(int position) {
            return fragments.get(position);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            // 覆写destroyItem并且空实现,这样每个Fragment中的视图就不会被销毁
            // super.destroyItem(container, position, object);
        }

        @Override
        public int getItemPosition(Object object) {
            return PagerAdapter.POSITION_NONE;
        }
    }

CommonTabLayout与上面SlidingTabLayout百分之99的相似度,重复的不在叙述,区别点在于setTabData和notifyDataSetChanged方法,notifyDataSetChanged根据Icon的Gravity属性进入不同布局的View做Tab,虽然TextView有drawableLeftRightTopBottom的相关属性,但是并不能让我们那么自由的控制Ui。

    /** 更新数据 */
    public void notifyDataSetChanged() {
        mTabsContainer.removeAllViews();
        this.mTabCount = mTabEntitys.size();
        View tabView;
        for (int i = 0; i < mTabCount; i++) {
            if (mIconGravity == Gravity.LEFT) {
                tabView = View.inflate(mContext, R.layout.layout_tab_left, null);
            } else if (mIconGravity == Gravity.RIGHT) {
                tabView = View.inflate(mContext, R.layout.layout_tab_right, null);
            } else if (mIconGravity == Gravity.BOTTOM) {
                tabView = View.inflate(mContext, R.layout.layout_tab_bottom, null);
            } else {
                tabView = View.inflate(mContext, R.layout.layout_tab_top, null);
            }

            tabView.setTag(i);
            addTab(i, tabView);
        }

        updateTabStyles();
    }

setTabData(ArrayList tabEntitys, FragmentActivity fa, int containerViewId, ArrayList fragments)方法内部初始化了一个Fragment的管理辅助类FragmentChangeManager,该类在构造函数动态添加隐藏了fragment,对外提供setFragments(int index)显示指定位置的Fragment,这个在frameLayout+Fragment+commonTabLayout布局里面免去了我们管理fagment的烦恼

SegmentTabLayout相比较于CommonTabLayout多了动画这块的处理,点击了某一项Tab,调用setCurrentTab,间接调用calcOffset开启了动画,动画的执行过程中onAnimationUpdate重新重绘,调整位置。

  tabView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = (Integer) v.getTag();
                if (mCurrentTab != position) {
                    setCurrentTab(position);
                    if (mListener != null) {
                        mListener.onTabSelect(position);
                    }
                } else {
                    if (mListener != null) {
                        mListener.onTabReselect(position);
                    }
                }
            }
        });

    //setter and getter
    public void setCurrentTab(int currentTab) {
        mLastTab = this.mCurrentTab;
        this.mCurrentTab = currentTab;
        updateTabSelection(currentTab);
        if (mFragmentChangeManager != null) {
            mFragmentChangeManager.setFragments(currentTab);
        }
        if (mIndicatorAnimEnable) {
            calcOffset();
        } else {
            invalidate();
        }
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        IndicatorPoint p = (IndicatorPoint) animation.getAnimatedValue();
        mIndicatorRect.left = (int) p.left;
        mIndicatorRect.right = (int) p.right;
        invalidate();
    }

小结

首先说一点这里不提供demo,需要去官方https://github.com/H07000223/FlycoTabLayout down ,其次呢这个库看了之后还是有很大收获的,比如自定义属性的运用, setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)内部实例一个adapter适配器,最重要的是自定义属性解析和属性值代码设置通过一个类来代理完成。

;