开源库系列:FlycoTabLayout 学习

1字数 2944阅读 6189

写在前面

本文主要目的是全方位学习 FlycoTabLayout 库代码,整体功能比较简单和实用,很多项目都有切Tab效果,所以这里给大家推荐下,同时我也会细致的讲解这个库的实现,认真分析里面用到的知识点,最后基于自己的思考扩展另一种实现方式。https://github.com/whiskeyfei/FlycoTabLayout

目录

以下内容为 FlycoTabLayout 库中使用到的知识点,只需要简单了解每个知识点的用法,就能看懂并且自己也能独立开发类似的控件。注意:我这里只会稍微介绍下含义和使用方法,更深入的研究希望大家自己去学习。

目录

看完上图,让我猜猜你的目的?

1、你想直接用原控件?
FlycoTabLayout 仓库 使用细节 README 中已经很详细了,看着代码实例也能快速上手。

2、你想直接用此控件?

简单看下图


image.png

原仓库很多效果都集成到一起了,实际应用场景可能不会覆盖这么多功能,所以我进行了功能拆分,以下控件任你选,控件代码都进行单独拆分过了,代码看起来也比较简单易懂,大家说好用才好用。

控件 功能
HintImageView.java StateListDrawable 实现三种状态
ShapeTextTabLayout.java 文字切换
SingleTextNewTabLayout.java 文字切换( Adapter实现)
SingleUnderLineTabLayout.java 文字+下划线切换
SingleIconTextTabLayout.java 文字+icon切换
SingleIconTabLayout.java icon切换
SingleIconTabLayout.java icon+消息切换
ShapeTextTabLayout.java 文字+shape背景切换
SinglePointTabLayout.java 文字+原点切换
SingleRectTabLayout.java 文字+矩形块切换

3、你想大概了解下控件?

知识点 :
自定义属性、自定义View、动画、GradientDrawable、StateListDrawable

基本思路:
1、自定义一个横向线性父布局
2、根据数据个数向父布局中添加子View
3、文字和icon状态通过循环遍历来改变。
4、通过动画来持续触发 onDraw()方法,来绘制指示器位置。

你想全面了解代码思路?
请认真查看文字以下内容和代码注释。

一、作用

底部Tab切换、消息个数展示、更新状态展示。

二、Android 自定义属性

自定义属性通常用于自定义 View 控件开发,通过配置相关属性达到某种效果.本节知识点 AttributeSet、TypedArray、styleable

属性资源

属性资源文件放在 /res/values 目录下,属性资源文件的根标签时 <resources><resources/>,包含两个子元素。

1、attr:定义一个属性。
2、declare-styleable:定义一个 styleable 对象,每个styleable 对象就是一组 attr 属性的集合。

当定义好了属性资源文件之后就可以在自定义控件中的构造方法中通过 AttributeSet 对象来获取这些属性了。

整体总结下各个名词解释:

名词 含义
AttributeSet 参数的集合
TypedArray 配合AttributeSet参数使用,简化获取方式
attr 自定义attr标签
declare-styleable 定义一个styleable组,方便操作及复用

一般步骤如下:

  • 首先自定义一个控件,可以是View、ViewGroup 等级别
  • 在 attrs.xml 文件中添加自己 styleable 和 item 等标签元素
  • 在包含自定义控件的 layout 中使用自定义属性
  • 在自定义控件构造方法中通过TypedArray获取属性,做相关操作

自定义Attributes实例:

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>

自定义 View 使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
   xmlns:custom="[http://schemas.android.com/apk/res/com.example.customviews](http://schemas.android.com/apk/res/com.example.customviews)">
<com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>

自定义属性需要一个特殊的名称空间,例如:

http://schemas.android.com/apk/res/[your package name]

使用:

public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PieChart,0, 0);
   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}

项目中使用

<declare-styleable name="CommonTabLayout">
    <!-- indicator -->
    <attr name="tl_indicator_color"/>
    <attr name="tl_indicator_height"/>
    <attr name="tl_indicator_width"/>
    <attr name="tl_indicator_margin_left"/>
    <attr name="tl_indicator_margin_top"/>
    <attr name="tl_indicator_margin_right"/>
    <attr name="tl_indicator_margin_bottom"/>
    <attr name="tl_indicator_corner_radius"/>
    <attr name="tl_indicator_gravity"/>
    <attr name="tl_indicator_style"/>
    <attr name="tl_indicator_anim_enable"/>
    <attr name="tl_indicator_anim_duration"/>
    <attr name="tl_indicator_bounce_enable"/>

    <!-- underline -->
    <attr name="tl_underline_color"/>
    <attr name="tl_underline_height"/>
    <attr name="tl_underline_gravity"/>

    <!-- divider -->
    <attr name="tl_divider_color"/>
    <attr name="tl_divider_width"/>
    <attr name="tl_divider_padding"/>

    <!-- tab -->
    <attr name="tl_tab_padding"/>
    <attr name="tl_tab_space_equal"/>
    <attr name="tl_tab_width"/>

    <!-- title -->
    <attr name="tl_textsize"/>
    <attr name="tl_textSelectColor"/>
    <attr name="tl_textUnselectColor"/>
    <attr name="tl_textBold"/>
    <attr name="tl_textAllCaps"/>

    <!-- icon -->
    <!-- 设置icon宽度 -->
    <attr name="tl_iconWidth" format="dimension"/>
    <!-- 设置icon高度 -->
    <attr name="tl_iconHeight" format="dimension"/>
    <!-- 设置icon是否可见 -->
    <attr name="tl_iconVisible" format="boolean"/>
    <!-- 设置icon显示位置,对应Gravity中常量值 -->
    <attr name="tl_iconGravity" format="enum">
        <enum name="LEFT" value="3"/>
        <enum name="TOP" value="48"/>
        <enum name="RIGHT" value="5"/>
        <enum name="BOTTOM" value="80"/>
    </attr>
    <!-- 设置icon与文字间距 -->
    <attr name="tl_iconMargin" format="dimension"/>
</declare-styleable>

//自定义消息view
<declare-styleable name="MsgView">
    <!-- 圆角矩形背景色 -->
    <attr name="mv_backgroundColor" format="color"/>
    <!-- 圆角弧度,单位dp-->
    <attr name="mv_cornerRadius" format="dimension"/>
    <!-- 圆角弧度,单位dp-->
    <attr name="mv_strokeWidth" format="dimension"/>
    <!-- 圆角边框颜色-->
    <attr name="mv_strokeColor" format="color"/>
    <!-- 圆角弧度是高度一半-->
    <attr name="mv_isRadiusHalfHeight" format="boolean"/>
    <!-- 圆角矩形宽高相等,取较宽高中大值-->
    <attr name="mv_isWidthHeightEqual" format="boolean"/>
</declare-styleable>

三、Android 自定义 View

本节知识点:Rect、Paint、Path、drawRect、drawLine、drawPath

说到自定义View 就不能不说自定义属性,如果不熟悉可以查看上一节,关于自定义三部曲就不展开,主要会介绍关于onDraw() 中 Canvas 在项目中使用到的知识点。

基础概念
名词 含义
Rect 定义一个矩形,用坐标表示.
Paint 可以理解为 Canvas 上的画笔,用来设置绘制风格,包括颜色、画笔粗细、填充颜色、几何图形、文本、位图等.
Path 多种类型路径封装组合,简单理解为绘制图形的外部轮廓组合.
Draw Canvas提供的绘制各种图形方法。包括 :drawRect、drawLine、drawPath等等.
drawRect
/**
 * 绘制矩形
 * @param canvas
 *
 * 为什么会有Rect和RectF两种?两者有什么区别吗?

答案当然是存在区别的,两者最大的区别就是精度不同,Rect是int(整形)的,而RectF是float(单精度浮点型)的。
除了精度不同,两种提供的方法也稍微存在差别,在这里我们暂时无需关注
 */
private void drawRect(Canvas canvas){
    //one:
    canvas.drawRect(100,100,350,400,mPaint);
    //two:
    Rect rect = new Rect(100,100,350,400);
    canvas.drawRect(rect,mPaint);
    //three:
    RectF rectF = new RectF(100,100,350,400);
    canvas.drawRect(rectF,mPaint);
}
drawLine
/**
 * 绘制线
 * @param canvas
 */
private void drawLine(Canvas canvas){
    canvas.drawLine(300,300,500,600,mPaint);    // 在坐标(300,300)(500,600)之间绘制一条直线
    canvas.drawLines(new float[]{               // 绘制一组线 每四数字(两个点的坐标)确定一条线
            100,200,200,200,
            100,300,200,300
    },mPaint);
}
drawPath
    mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
    mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
    mPaint.setStrokeWidth(10);              // 边框宽度 - 10

    canvas.translate(getWidth()/2,getHeight()/2);
    Path path = new Path();
    path.lineTo(200,200);//默认从(0,0)开始
    path.lineTo(200,0);//根据第一次移动完的位置设置
    canvas.drawPath(path,mPaint);

以上只介绍了三个项目中使用的方法,其他暂时就不介绍了。这里推荐 http://www.gcssloop.com/customview/Canvas_BasicGraphics 文章,可以更细致的了解每一个方法使用,照着写一遍就理解了。

四、动画

知识点:ValueAnimator 、OvershootInterpolator

在 Android 动画中,总共有两种类型的动画 View Animation(视图动画)和 Property Animator(属性动画);常见的View Animation 有 alpha、scale、translate、rotate 这里不多介绍。

ValueAnimator

ValueAnimator 属于 Property Animator(属性动画),ValueAnimator 本身不作用与任何对象,也就是说直接使用它是没有任何动画效果的,它是对指定值区间做动画运算,通过监听这个区间值的变化来修改控件的属性值.

用法

第一步:创建 ValueAnimator

ValueAnimator animator = ValueAnimator.ofInt(0,100);  
animator.setDuration(1000);  
animator.start();

在这里我们使用 ValueAnimator.ofInt 创建了一个值从 0 到 100 的动画,动画时长是 1s,然后让动画开始。可以看出,ValueAnimator 没有跟任何的控件相关联,那也正好说明 ValueAnimator 只是对值做动画运算,而不是针对控件的,我们需要监听 ValueAnimator 的动画过程来自己对控件做操作。

第二步:添加监听

ValueAnimator animator = ValueAnimator.ofInt(0,400);  
animator.setDuration(1000);  
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  

    @Override  
    public void onAnimationUpdate(ValueAnimator animation) {  
        int curValue = (int)animation.getAnimatedValue();  
    }  
});  
animator.start();

在上面的代码中,我们通过 addUpdateListener 添加一个监听,在监听传回的结果中,表示当前状态的 ValueAnimator 实例,通过 animation.getAnimatedValue() 得到当前值。

OvershootInterpolator

系统提供的回弹效果插值器,还有其他的效果,例如:BounceInterpolator(弹跳插值器)等等

配合ValueAnimator使用

ValueAnimator animator = ValueAnimator.ofInt(0,400);  
animator.setInterpolator(new OvershootInterpolator());
动画效果实现思路

这里只是根据代码过程简单描述下实现思路

private ValueAnimator mValueAnimator = ValueAnimator.ofObject(new PointEvaluator(), mLastP, mCurrentP);
private OvershootInterpolator mInterpolator = new OvershootInterpolator(1.5f);
mValueAnimator.addUpdateListener(this);

@Override
public void onAnimationUpdate(ValueAnimator animation) {
    //会一直不停的回调,知道到达 mCurrentP 位置停止
    View currentTabView = mTabsContainer.getChildAt(this.mCurrentTab);
    IndicatorPoint p = (IndicatorPoint) animation.getAnimatedValue();
    mIndicatorRect.left = (int) p.left;
    mIndicatorRect.right = (int) p.right;
    //这里用于计算指示器的位置代码省略
    invalidate();
}

//每一次动画更新就会之行 onDraw 方法,mIndicatorRect 中的 left 和 right 是根据动画回调变化的,所以通过刷新就能看到指示器切换效果。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制指示器,省略计算过程,详细请查看具体控件功能代码
        mIndicatorDrawable.setBounds(left,top,right,bottom);
    }
注意事项

1、内存泄露。
2、setVisibility 失效。当多次频繁对View 做动画时,可能会出现无法显示或隐藏问题,经调试发现,需要使用View 的 clearAnimation()方法就能解决这个问题。
3、使用dp。由于Android 设备分辨率较多,使用dp比px要好。

五、资源属性

本节会介绍Android 设备显示相关知识点,DisplayMetrics(density、scaledDensity)、sp、dp

关于DisplayMetrics 学习我们看官方文档了解就可以了,掌握含义和如何使用即可。

DisplayMetrics.java 是在android.util 包下,属于工具类相关类,工具类的作用可以直接使用,直接使用结果。

我们可以看源码,或者官方文档解释

A structure describing general information about a display, such   as its size, density, and font scaling.
To access the DisplayMetrics members, initialize an object like this:

DisplayMetrics metrics = new DisplayMetrics();

getWindowManager().getDefaultDisplay().getMetrics(metrics);

描述有关设备显示一般信息结构,例如:大小、密度、字体缩放,可以通过以下方式获取 DisplayMetrics 对象。

以上是官方文档简单理解,按照上面的代码使用一点问题都没有。

不过日常我们习惯这么用

DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();

常见的字段使用:

方法 含义
scaledDensity A scaling factor for fonts displayed on the display.字体缩放比例
density The logical density of the display.屏幕密度
widthPixels The absolute width of the available display size in pixels.屏幕宽度
heightPixels The absolute height of the available display size in pixels.屏幕高度

其他字段就不在这里一一介绍了,如果有其他场景使用,可以查看DisplayMetrics 完成代码。

说到和尺寸相关的能说好几天,这里就简单介绍下概念,之后会总结下屏幕适配相关知识点。

px:即像素,1px代表屏幕上一个物理的像素点;

dp:在定义 UI 布局时应使用的虚拟像素单位,用于以密度无关方式表示布局维度 或位置。

密度无关像素等于 160 dpi 屏幕上的一个物理像素,这是 系统为“中”密度屏幕假设的基线密度。在运行时,系统 根据使用中屏幕的实际密度按需要以透明方式处理 dp 单位的任何缩放 。

dp 单位转换为屏幕像素很简单: px = dp * (dpi / 160)。 例如,在 240 dpi 屏幕上,1 dp 等于 1.5 物理像素。在定义应用的 UI 时应始终使用 dp 单位 ,以确保在不同密度的屏幕上正常显示 UI。

sp:缩放独立的像素,同dp相似,但是会根据系统字体大小偏好来缩放

对应关系

类型 dpi density
ldpi 120 0.75
mdpi 160 1.0
hdpi 240 1.5
xhdpi 320 2.0

等等

这里要掌握,px和dp转换.

protected int dp2px(float dp) {
    final float scale = mContext.getResources().getDisplayMetrics().density;
    return (int) (dp * scale + 0.5f);
}

protected int sp2px(float sp) {
    final float scale = this.mContext.getResources().getDisplayMetrics().scaledDensity;
    return (int) (sp * scale + 0.5f);
}

六、Drawable

Drawable 简介

Drawable 表示一种图像概念,可以是图片也可以是其他颜色构造出得效果。实际开发中 Drawable 常被用来作为 View 的背景,一般都是通过 XML 文件定义,但是通过代码也是可以的,这样对于减小 APK 包体积大小也是有帮助的。

本节会介绍 Drawable 的两个子类,这两个是在项目中使用到的 Drawable, 分别是 GradientDrawable 和 StateListDrawable,我们先了解和掌握本库是如何使用,其他子类目前暂时不介绍。

GradientDrawable

GradientDrawable 表示一个渐变区域,可以实现线性渐变、发散渐变和平铺渐变效果(RECTANGLE, OVAL, LINE, RING)。GradientDrawable 是 Drawable 其中一个子类,用来设置按钮背景、颜色、角度、边框等,可以使用代码或者xml实现。

介绍几个 Api:

方法 含义
void setCornerRadius(float radius) Specifies the radius for the corners of the gradient.指定四个角的圆角大小
void setCornerRadii(float[] radii) Specifies radii for each of the 4 corners.分别指定4个角的圆角圆角半径大小
void setColor (ColorStateList colorStateList) Changes this drawable to use a single color state list instead of a gradient. Calling this method with a null argument will clear the color and is equivalent to calling setColor(int) with the argument TRANSPARENT.根据当前状态设置颜色显示.
void setColor (int argb) Changes this drawable to use a single color instead of a gradient.设置单色值.
int[] getState () Describes the current state, as a union of primitve states, such as state_focused, state_selected, etc. Some drawables may modify their imagery based on the selected state.返回当前选中状态.
int getShape () Returns the type of shape used by this drawable, one of LINE, OVAL, RECTANGLE or RING.返回当前shape类型.
public void setBounds (int left, int top, int right, int bottom) 父类中的方法,用来指定绘制区域边界

GradientDrawable 只是 Drawable 中其中一个子类,它还有很多的兄弟类型,用于处理不同的 Drawable 效果。我这里先不介绍其他子类的情况,之后会总结下 Drawable 的所有子类,详细介绍下每个子类功能。

2、StateListDrawable

StateListDrawable 表示一个多状态对应不同 drawable 的 Drawable资源集合。是 Drawable 其中一个子类,常应用于 <selector> 实现各种按钮点击态、默认态等功能。

介绍几个 Api:

方法 含义
void addState (int[] stateSet, Drawable drawable) Add a new image/string ID to the set of images. 添加资源到一中状态下
void applyTheme(Resources.Theme theme) Applies the specified theme to this Drawable and its children.应用指定的主题到Drawable及其子项。
void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) Inflate this Drawable from an XML resource optionally styled by a theme.根据 XML 资源生成 Drawable。
boolean isStateful() Indicates whether this drawable will change its appearance based on state.是否可以根据状态改变当前显示
Drawable mutate() Make this drawable mutable.Drawable 可变,也就是可以生成一个新的变化样式的Drawable

StateSet:这些常量基本覆盖了 Android 控件中大多数状态,比如我们最常见的 PRESSED 状态。通过位运算我们可以用一个 int 表示这里所有的状态。

StateSet

StateSet 在utils包下,属于工具类辅助使用,简单看下几中状态有个印象即可。

static final int[] VIEW_STATE_IDS = new int[] {
        R.attr.state_window_focused,    VIEW_STATE_WINDOW_FOCUSED,
        R.attr.state_selected,          VIEW_STATE_SELECTED,
        R.attr.state_focused,           VIEW_STATE_FOCUSED,
        R.attr.state_enabled,           VIEW_STATE_ENABLED,
        R.attr.state_pressed,           VIEW_STATE_PRESSED,
        R.attr.state_activated,         VIEW_STATE_ACTIVATED,
        R.attr.state_accelerated,       VIEW_STATE_ACCELERATED,
        R.attr.state_hovered,           VIEW_STATE_HOVERED,
        R.attr.state_drag_can_accept,   VIEW_STATE_DRAG_CAN_ACCEPT,
        R.attr.state_drag_hovered,      VIEW_STATE_DRAG_HOVERED
};

应用

同样一张图需要三种不同状态效果,例如:播放音乐下一首按钮,三种状态:不可用、默认、点击态。我们就能使用一张图片,和三种着色搞定。

HintImageView.java StateListDrawable 实现三种状态

七、完整Demo

https://github.com/whiskeyfei/FlycoTabLayout

八、扩展思路

出发点:原控件设计时 View 和数据都在一个类中,耦合性太强。
思考:控件只负责View相关,数据交给 Adapter,借鉴 RecyclerView 一些使用思路。
落地:控件只包含View 效果等实现,具体数据、颜色等一切可以设置的东西通过 Adapter 实现。
好处:通过切换 Adapter 实现不同效果。
坏处:没有发挥自定义属性优势。
总结:使用 Adapter 形式有利有弊,目前还是初步阶段,由于控件功能比较独立,不像 RecyclerView 时专门做列表的,Adpater 设计没有那么灵活,后面会深入了解下 RecyclerView.Adpater 设计,借鉴一下。

demo 请查看这里
TabAdapter.java

学习讨论

刚刚建了一个 Android 开源库分享学习群,有兴趣的小伙伴可以加入一起学习。

群二维码

参考资料

https://developer.android.com/reference/android/content/res/TypedArray
https://developer.android.com/reference/android/util/AttributeSet
https://developer.android.com/training/custom-views/create-view
https://developer.android.com/guide/topics/ui/custom-components
https://developer.android.com/training/custom-views/create-view
https://developer.android.com/reference/android/graphics/Rect
https://developer.android.com/reference/android/graphics/Paint
https://developer.android.com/reference/android/graphics/Canvas
http://www.gcssloop.com/customview/Canvas_BasicGraphics
https://developer.android.com/reference/android/graphics/drawable/GradientDrawable
https://developer.android.com/reference/android/graphics/drawable/StateListDrawable
https://developer.android.com/reference/android/animation/ValueAnimator
https://developer.android.com/reference/android/view/animation/OvershootInterpolator
http://wiki.jikexueyuan.com/project/android-animation/4.html
http://wiki.jikexueyuan.com/project/android-animation/5.html
https://developer.android.com/guide/practices/screens_support?hl=zh-cn