2015年7月

当我开始接触Tint这个词的时候,其实是蛮不理解它的意思的,以及并不清楚Google发明它的目的,它一般搭配Background配合使用,但是现在已经有了Background,为什么还需要Tint呢?

Tint 翻译为着色

着色,着什么色呢?和背景有关,当然是着背景的色。当我开发客户端,使用了appcompat-v7包的时候,为了实现Material Design的效果,我们会去设置主题里的几个颜色,重要的比如primaryColorcolorControlNormalcolorControlActived 等等,而我们使用的一些组件,比如EditText就会自动变成我们想要的背景颜色,在背景图只有一张的情况下,这样的做法极大的减少了我们apk包的大小。

实现的方式就是用一个颜色为我们的背景图片设置tint(着色)。

例子:

clipboard.png

看看即将发布的SegmentFault for Android 2.7中,发布问题功能,这个EditText的颜色和我们的主要颜色相同。它利用了TintManager这个类,为自己的背景进行着色(绿色)。
那么这个原始图是什么样子呢?我们从appcompat-v7包中找到了这个图,是一个.9图,样子如下:

clipboard.png
其实它只是一个黑色的条,通过绿色的着色,变成了一个绿色的条。 就是这样的设计方式,使得我们在Material Design中省了多少资源文件呀!

好了,既然理解了tint的含义,我们赶紧看下这一切是如何实现的吧。
其实底层特别简单,了解过渲染的同学应该知道PorterDuffColorFilter这个东西,我们使用SRC_IN的方式,对这个Drawable进行颜色方面的渲染,就是在这个Drawable中有像素点的地方,再用我们的过滤器着色一次。
实际上如果要我们自己实现,只用获取ViewbackgroundDrawable之后,设置下colorFilter即可。

看下最核心的代码就这么几行

if (filter == null) {
    // Cache miss, so create a color filter and add it to the cache
    filter = new PorterDuffColorFilter(color, mode);
}

d.setColorFilter(filter);

通常情况下,我们的mode一般都是SRC_IN,如果想了解这个属性相关的资料,这里是传送门: http://blog.csdn.net/t12x3456/article/details/10432935 (中文)

由于API Level 21以前不支持background tint在xml中设置,于是提供了ViewCompat.setBackgroundTintList方法和ViewCompat.setBackgroundTintMode用来手动更改需要着色的颜色,但要求相关的View继承TintableBackgroundView接口。

源码解析

看下源码是如何实现的吧,我们以AppCompatEditText为例:
看下构造函数(省略无关代码)

public AppCompatEditText(Context context, AttributeSet attrs, int defStyleAttr) {
    super(TintContextWrapper.wrap(context), attrs, defStyleAttr);

    ...
   
    ColorStateList tint = a.getTintManager().getTintList(a.getResourceId(0, -1)); //根据背景的resource id获取内置的着色颜色。
    if (tint != null) {
        setInternalBackgroundTint(tint); //设置着色
    }
   
    ...
}

private void setInternalBackgroundTint(ColorStateList tint) {
    if (tint != null) {
        if (mInternalBackgroundTint == null) {
            mInternalBackgroundTint = new TintInfo();
        }
        mInternalBackgroundTint.mTintList = tint;
        mInternalBackgroundTint.mHasTintList = true;
    } else {
        mInternalBackgroundTint = null;
    }
    //上面的代码是记录tint相关的信息。
    applySupportBackgroundTint();  //对背景应用tint
}


 private void applySupportBackgroundTint() {
    if (getBackground() != null) {
        if (mBackgroundTint != null) {
            TintManager.tintViewBackground(this, mBackgroundTint);
        } else if (mInternalBackgroundTint != null) {
            TintManager.tintViewBackground(this, mInternalBackgroundTint); //最重要的,对tint进行应用
        }
    }
}

然后我们进入tintViewBackground看下TintManager里面的源码

 public static void tintViewBackground(View view, TintInfo tint) {
    final Drawable background = view.getBackground();
    if (tint.mHasTintList) {
        //如果设置了tint的话,对背景设置PorterDuffColorFilter
        setPorterDuffColorFilter(
                background,
                tint.mTintList.getColorForState(view.getDrawableState(),
                        tint.mTintList.getDefaultColor()),
                tint.mHasTintMode ? tint.mTintMode : null);
    } else {
        background.clearColorFilter();
    }

    if (Build.VERSION.SDK_INT <= 10) {
        // On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
        // has changed, so we need to force an invalidation
        view.invalidate();
    }
}


private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
    if (mode == null) {
        // If we don't have a blending mode specified, use our default
        mode = DEFAULT_MODE;
    }

    // First, lets see if the cache already contains the color filter
    PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);

    if (filter == null) {
        // Cache miss, so create a color filter and add it to the cache
        filter = new PorterDuffColorFilter(color, mode);
        COLOR_FILTER_CACHE.put(color, mode, filter);
    }
    
    // 最最重要,原来是对background drawable设置了colorFilter 完成了我们要的功能。
    d.setColorFilter(filter);
}

以上是对API21以下的兼容。
如果我们要实现自己的AppCompat组件实现tint的一些特性的话,我们就可以指定好ColorStateList,利用TintManager对自己的背景进行着色,当然需要对外开放设置的接口的话,我们还要实现TintableBackgroundView接口,然后用ViewCompat.setBackgroundTintList进行设置,这样能完成对v7以上所有版本的兼容。

实例

比如我现在要对一个自定义组件实现对Tint的支持,其实只用继承下,加一些代码就好了,代码如下(几乎通用):

public class AppCompatFlowLayout extends FlowLayout implements TintableBackgroundView {

    private static final int[] TINT_ATTRS = {
            android.R.attr.background
    };

    private TintInfo mInternalBackgroundTint;
    private TintInfo mBackgroundTint;
    private TintManager mTintManager;

    public AppCompatFlowLayout(Context context) {
        this(context, null);
    }

    public AppCompatFlowLayout(Context context, AttributeSet attributeSet) {
        this(context, attributeSet, 0);
    }

    public AppCompatFlowLayout(Context context, AttributeSet attributeSet, int defStyle) {
        super(context, attributeSet, defStyle);

        if (TintManager.SHOULD_BE_USED) {
            TintTypedArray a = TintTypedArray.obtainStyledAttributes(getContext(), attributeSet,
                    TINT_ATTRS, defStyle, 0);
            if (a.hasValue(0)) {
                ColorStateList tint = a.getTintManager().getTintList(a.getResourceId(0, -1));
                if (tint != null) {
                    setInternalBackgroundTint(tint);
                }
            }
            mTintManager = a.getTintManager();
            a.recycle();
        }
    }

    private void applySupportBackgroundTint() {
        if (getBackground() != null) {
            if (mBackgroundTint != null) {
                TintManager.tintViewBackground(this, mBackgroundTint);
            } else if (mInternalBackgroundTint != null) {
                TintManager.tintViewBackground(this, mInternalBackgroundTint);
            }
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        applySupportBackgroundTint();
    }

    private void setInternalBackgroundTint(ColorStateList tint) {
        if (tint != null) {
            if (mInternalBackgroundTint == null) {
                mInternalBackgroundTint = new TintInfo();
            }
            mInternalBackgroundTint.mTintList = tint;
            mInternalBackgroundTint.mHasTintList = true;
        } else {
            mInternalBackgroundTint = null;
        }
        applySupportBackgroundTint();
    }

    @Override
    public void setSupportBackgroundTintList(ColorStateList tint) {
        if (mBackgroundTint == null) {
            mBackgroundTint = new TintInfo();
        }
        mBackgroundTint.mTintList = tint;
        mBackgroundTint.mHasTintList = true;

        applySupportBackgroundTint();
    }

    @Nullable
    @Override
    public ColorStateList getSupportBackgroundTintList() {
        return mBackgroundTint != null ? mBackgroundTint.mTintList : null;
    }

    @Override
    public void setSupportBackgroundTintMode(PorterDuff.Mode tintMode) {
        if (mBackgroundTint == null) {
            mBackgroundTint = new TintInfo();
        }
        mBackgroundTint.mTintMode = tintMode;
        mBackgroundTint.mHasTintMode = true;

        applySupportBackgroundTint();
    }

    @Nullable
    @Override
    public PorterDuff.Mode getSupportBackgroundTintMode() {
        return mBackgroundTint != null ? mBackgroundTint.mTintMode : null;
    }
}

赶快去试试吧~

欢迎关注我Github 以及 @Gemini

之前我们学习过如何写一个简单的Android App。
为了赶上潮流,我特地去学习了下Jetbrains开发的新语言 —— Kotlin

不想说太多的概念,总结出来就是 Swift on JVM
那么为什么要用它呢,我喜欢它的理由很多:

  1. 带来了Nullable Safe特性 —— 以后再也不怕讨厌的 Null Pointer Exception了。
  2. 闭包闭包闭包 —— 重要的事情说三遍.
  3. Smart Type Case —— 很智能的一个特性,当你使用if检查是否是某种类型以后,自动转换为指定类型。
  4. 没有附加的Runtime —— iOSer 看到这会不会哭.
  5. Kotlin stdlib 非常小,打包后Apk的体积几乎没有变化,也不用担心方法数超过限制。

总而言之,就是用极小的代价换来了我们许多振奋人心的特性,那么你心动了么?
当然,在心动之前也要理智,我们要知道Kotlin暂时还没有发布"正式版",一直在0.x.x版本号中徘徊,如果你足够胆大(像我),那么你大可以一试。

老规矩先补上官方文档传送门:http://kotlinlang.org/docs/reference/basic-syntax.html

一些基本的语法如——基础类型、流程控制、类与继承等等特性我们已经不陌生,我们来看看几个新特性。

Nullable Safe

我翻译过来是空指针安全监测,什么意思呢?看下如下语法糖(Swift Developer可以直接跳过)
比如

var text:String? = null
text?.length()

如果是java代码,在text变量为null的时候,调用text.length()是会崩溃滴,那么在这里,我们用了?来告诉编译器,如果textnull,则返回null,否则返回text.length(),具体翻译过来就是这样:

if (text == null) {
   return null;
} else {
   return text.length()
}

看我们少了这么多判断,这个语法糖是不是很棒?

Closure & Lambda & Higer-Order Functions

介绍我最爱的闭包,那么在java 8以下的版本中,java是没有闭包这个特性的,举个例子,你的函数不能被当成一个对象使用,必须使用一个接口封装,而我们使用Kotlin就可以直接传函数当形参啦!

fun lock<T>(lock: Lock, body: () -> T): T {
  lock.lock()
  try {
    return body()
  }
  finally {
    lock.unlock()
  }
}

此处我们的body形参就是一个0参数返回类型为T的函数,可以作为我们的回调函数使用,而不用像java一样定义又臭又长的接口,再传入使用。

fun dfs(graph: Graph) {
  fun dfs(current: Vertex, visited: Set<Vertex>) {
    if (!visited.add(current)) return
    for (v in current.neighbors)
      dfs(v, visited)
  }

  dfs(graph.vertices[0], HashSet())
}

这是一个典型的dfs算法,使用Kotlin的高阶函数特性——可以在函数内定义函数。

Smart Type Case

智能类型转换,什么意思呢?show一段代码

if (str is String) {
    return str.length()
} else if (str is Int) {
    return str.toString()
}

我们知道Int类型是没有length这个方法的,也就是经过这个if判断,如果满足if条件的话,编译器自动帮我们转换成我们要的类型,然后供我们调用。

我试着在SegmentFault for Android中加入对Kotlin的支持,在加入Kotlinlib前后,包大小并没有明显增长(1M以下),性能亦没有降低,所以用户是感知不出来内部发生了什么变化。
综上所述,我要给Kotlin点赞!

Demo

我把土豆记事所有的代码全部改成Kotlin的实现,并开源到Github上 
https://github.com/geminiwen/tudounotepad
大家可以clone下来学习,也非常感谢大家对我的支持。
(顺便跪求各种star star star)

欢迎关注我Github 以及 @Gemini

效果演示

初始状态

clipboard.png

滑动中状态

clipboard.png

结束状态

clipboard.png

这是目前实现在SegmentFault for Android v2.6中的效果。
一切一切的之前,感谢 ikew0ng/SwipeBackLayout
我使用这个库,并经过一些修改,支持了Android 4.0以上所有的版本。
我们来分析下SwipeBackLayout的源码

一些修改

我之前做过实验,碰到的最大问题是上层的Activity底下并不是透明的,因此看不见下层Activity的视图。
SwipeBackLayout中采用的方案是使用一个叫convertToTranslucent的未公开的api,再配合theme
windowIsTranslucent设置为true,即可实现上层的Window背景为透明。

这里要注意的地方是调用convertToTranslucent可以使用反射的方法进行调用,但是在Lollipop中,它的参数变成了两个,而在5.0以下是一个参数,所以需要在源码中对Util.convertActivityToTranslucent这个方法进行一些修改。

public static void convertActivityToTranslucent(Activity activity) {
    try {
        Class[] t = Activity.class.getDeclaredClasses();
        Class translucentConversionListenerClazz = null;
        Class[] method = t;
        int len$ = t.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            Class clazz = method[i$];
            if(clazz.getSimpleName().contains("TranslucentConversionListener")) {
                translucentConversionListenerClazz = clazz;
                break;
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Method var8 = Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz, ActivityOptions.class);
            var8.setAccessible(true);
            var8.invoke(activity, new Object[]{null, null});
        } else {
            Method var8 = Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz);
            var8.setAccessible(true);
            var8.invoke(activity, new Object[]{null});
        }
    } catch (Throwable e) {
    }

}

使得能适配4.0 - 5.0+所有的设备

源码分析

我们可以看它的源码可以知道,它是利用ViewDragHelper对View进行拖移效果的,ViewDragHelper主要帮助我们完成了对速度、加速度以及释放后的一些逻辑的设置,大大简化了我们对触摸事件的处理。
我们看下SwipeBackLayout是如何嵌入到我们要的Activity里去的

 public void attachToActivity(Activity activity) {
    this.mActivity = activity;
    // .... 省略部分代码
    ViewGroup decor = (ViewGroup)activity.getWindow().getDecorView();
    ViewGroup decorChild = (ViewGroup)decor.getChildAt(0);
    decorChild.setBackgroundResource(background);
    decor.removeView(decorChild);
    this.addView(decorChild);
    this.setContentView(decorChild);
    decor.addView(this);
}

我们可以看到,本来Activity调用setContentView之后,会把我们要的layout加到windowdecorView上,我们在这里把window中的decorView的子元素改成SwipeBackLayout,然后把原先的contentView加到decorView下,使得ViewDragHelper附着在SwipeBackLayout上。

this.mDragHelper = ViewDragHelper.create(this, new SwipeBackLayout.ViewDragCallback());

ViewDragHelper使用ViewDragHelper.Callback这个回调进行一些View是否可以滑动以及滑动距离的判定。
我们来看下ViewDragHelper.Callback接口的声明
官方文档传送门:http://developer.android.com/reference/android/support/v4/widget/ViewDragHelper.Callback.html

定义注释
int clampViewPositionHorizontal (View child, int left, int dx)此方法返回一个值,告诉Helper,这个view能滑动的最大(或者负向最大)的横向坐标
int clampViewPositionVertical (View child, int top, int dy)此方法返回一个值,告诉Helper,这个view能滑动的最大(或者负向最大)的纵向坐标
int getOrderedChildIndex (int index)返回这个索引所指向的子视图的Z轴坐标
int getViewHorizontalDragRange (View child)返回指定View在横向上能滑动的最大距离
int getViewVerticalDragRange (View child)返回指定View在纵向上能滑动的最大距离
void onEdgeDragStarted (int edgeFlags, int pointerId)当边缘开始拖动的时候,会调用这个回调
boolean onEdgeLock (int edgeFlags)返回指定的边是否被锁定
void onEdgeTouched (int edgeFlags, int pointerId)当边缘被触摸时,系统会回调这个函数
void onViewCaptured (View capturedChild, int activePointerId)当有一个子视图被指定为可拖动时,系统会回调这个函数
void onViewDragStateChanged (int state)拖动状态改变时,会回调这个函数
void onViewPositionChanged (View changedView, int left, int top, int dx, int dy)当子视图位置变化时,会回调这个函数
void onViewReleased (View releasedChild, float xvel, float yvel)当手指从子视图松开时,会调用这个函数,同时返回在x轴和y轴上当前的速度
boolean tryCaptureView (View child, int pointerId)系统会依次列出这个父容器的子视图,你需要指定当前传入的这个视图是否可拖动,如果可拖动则返回true 否则为false

利用ViewDragHelper的回调函数,我们知道了视图被拖动的距离,然后根据这个距离和宽度的一些比例,我们可以对SwipeBackLayout的父容器进行一些透明度和阴影的设置。

实现这个效果就是这么简单~

欢迎关注我Github 以及 @Gemini

应各位朋友的要求,写这篇文章,让我们来简单了解下Android Studio中不同目录(文件)的位置和用途。
首先看下一个App的最简单的目录结构

clipboard.png
【= = 好复杂的样子】

OK,我们这么看,第一,把这么多文件先分成这么三块

  1. 编译系统(Gradle)
  2. 配置文件
  3. 应用模块

Gradle是Google推荐使用的一套基于Groovy的编译系统脚本(当然,你也可以使用ant),具体的介绍和文档可以参考这个传送门:https://developer.android.com/tools/building/plugin-for-gradle.html
如果你学会之后,会对Android项目的编译了如指掌(总之非常爽~),它的缺点目前是效率不高,然后因为有功夫网的存在,所以在bintray上下载依赖会比较慢。

上面那个图中出现gradle字眼的就是gradle相关的一些文件。
Android中使用Gradle WrapperGradle进行了一层包装,我猜测这么做的原因是因为gradle更新速度实在太快,为了兼容性着想,才出了这么一套方案。(如果觉得这个猜想有问题请指正)
gradlew相关的文件就是和Gradle Wrapper有关。我们对除了app文件夹以外的文件列一下。

文件(夹)名用途
.gradleGradle编译系统,版本由wrapper指定
.ideaAndroid Studio IDE所需要的文件
build代码编译后生成的文件存放的位置
gradlewrapper的jar和配置文件所在的位置
.gitignoregit使用的ignore文件
build.gradlegradle编译的相关配置文件(相当于Makefile)
gradle.propertiesgradle相关的全局属性设置
gradlew*nix下的gradle wrapper可执行文件
graldew.batwindows下的gradle wrapper可执行文件
local.properties本地属性设置(key设置,android sdk位置等属性),这个文件是不推荐上传到VCS中去的
settings.gradle和设置相关的gradle脚本

这些就是外部文件相关的一些文件的介绍。我们来看下更重要的app模块里的文件

clipboard.png
这是app模块下的文件目录结构,介绍下他们的用途

文件(夹)名用途
build编译后的文件存在的位置(包括最终生成的apk也在这里面)
libs依赖的库所在的位置(jaraar)
src源代码所在的目录
src/main主要代码所在位置(src/androidTest)就是测试代码所在位置了
src/main/assetsandroid中附带的一些文件
src/main/java最最重要的,我们的java代码所在的位置
src/main/jniLibsjni的一些动态库所在的默认位置(.so文件)
src/main/resandroid资源文件所在位置
src/main/AndroidManifest.xmlAndroidManifest不用介绍了吧~
build.gradle和这个项目有关的gradle配置,相当于这个项目的Makefile,一些项目的依赖就写在这里面
proguard.pro代码混淆配置文件

以上就是对Android Studio目录结构的简单介绍~

有问题可以直接留言,我会尽快回复。

欢迎关注我Github 以及 @Gemini

上一篇文章,我们讲了如何创建一个工程,以及Android工程的一些基本概念,把工程创建出来后,我们看下文件目录结构,一个简单的工程结构如下。

clipboard.png
其实这个目录结构初次看还是挺让人心慌慌的。
Android现在引入了一个构建系统叫做Gradle,你可以理解为一个C/C++里面的Makefile 或者是node里面的gulp

Android Studio里面是分模块进行开发的,一个app可以只有一个模块,也可以有多个模块组成(比如一些自己开发的库)。如果我们的应用足够简单的话,那么就只有一个模块,Android Studio默认创建的模块就叫app,可以看见,文件夹旁边还有个小手机的标志,代表这是一个Android Application模块,而不是一个Android Library或者是其他模块。

看看我们的代码应该放哪,我们把注意力集中在这个文件夹
clipboard.png

srcsource的意思,也就是源码所在的目录,我们主要就是在这个文件夹里写东西。

Main在哪?

我们来看看,我们要的Main函数在哪里?
首先我们看src/main目录下的AndroidManifest.xml文件。

clipboard.png

AndroidManifest 是描述App的一个最最重要的文件,一些内容的定义,主题的设置都在这里,如果熟悉node的朋友肯定知道package.json,一样一样的。

我们看到下图,在AndroidManifest中,出现的MAINLAUNCHER字眼,Activity有了他们两个的描述,它就成了你点击app的icon启动的第一个Activity
clipboard.png

src/main/java文件夹中,找到MainActivity,打开,看见其中有一个onCreate的函数

clipboard.png
顾名思义,这个函数是在这个Activity创建的时候调用的,它首先调用了下父类的onCreate方法(不可省略),然后调用了setContentView方法,这个方法是告诉Android系统:我用哪个布局文件去渲染这个Activity,好了,到这里一个入口的Activity就创建好了。

注:在Android系统中,Activity类的对象不是用来给开发者直接去new的,它的生命周期由系统直接管控因为我们不参与控制Activity的生命周期,因此它在什么时候回调什么函数变得异常重要。学习编程最好的去处就是官方文档,如果想更加深入了解Android Activity生命周期的童鞋,我这里推荐看下官方对它的描述 传送门:http://developer.android.com/training/basics/activity-lifecycle/index.html

界面如何自定义?

接下来说说Android中的布局系统,众所周知,Android一开始的设计就是为了相对布局而生的。它提供了许多强大的布局特性,我们先学习下Android中最常见的两种布局:

  1. 线性布局(LinearLayout)
  2. 相对布局(RelativeLayout)

线性布局就是子控件按顺序依次排列,线性布局可以设置方向,从上到下(vertical)或者从左到右(horizontal)。
相对布局就更自由了,如果你增加一个子控件,不设置任何属性,则子空间的位置在容器的左上角(0,0)处。如果想改变位置,可以通过在容器中的位置(比如左上,右上,左下,右下,中间,左对齐垂直居中,右对齐垂直居中等等),或者和兄弟结点的对齐方式来决定控件的位置。

布局相关的参考可以看这个链接:http://developer.android.com/training/basics/firstapp/building-ui.html

如何和控件交互

Activity中,和xml相关的绑定在setContentView这步就算完成了。Activity在这之后会回调一个叫onContentChanged方法,在这个方法中,你可以使用类似如下代码:

TextView textView = findViewById(R.id.textview);

来获得对指定控件的引用,其中R.id.textview是你在xml中指定的android:id,通常情况下,在一个xml文件中,同样的id只允许出现一次。
获取到对控件的引用,我们就能调用一些控件里的方法获取我们要的内容,或者设置我们要的内容,比如我这里引用了一个TextView,则可以如下:

textView.getText()

获取到textView里面的内容。

以上就是最基础的UI部分的入门讲解。接下去,我们可以看看要写的App的整个结构。

本文提到的项目源码地址:https://github.com/geminiwen/tudounotepad
欢迎留言Github或者@geminiwen