扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
如您需要查看Android系统手机的相关信息可以依次点击进入:设置——关于手机。在这个功能里,您可以知道手机软件的版本信息、硬件信息、电池状态等。(上述内容仅适用于广东联通用户)
创新互联自2013年创立以来,先为薛城等服务建站,薛城等地企业,进行企业商务咨询服务。为薛城企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
在 Android 中,使用 Span 定义文本的样式. 通过 Span 改变几个文字的颜色,让它们可点击,放缩文字的大小甚至是绘制自定义的项目符号点(bullet points,国外人名中名字之间的间隔符号 · ,HTML 中无序列表项的默认符号)。Span 能够改变 TextPaint 属性,在 Canvas 上绘制,甚至是改变文本的布局和影响像行高这样的元素。Span 是可以附加到文本或者从本文分离的标记对象(markup objects);它们可以被应用到部分或整段的文本中。
让我们来看看Span如何使用、提供了哪些开箱即用的功能、怎样简单地创建我们自己的 Span 以及如何使用和测试它们。
Styling text in Android
Creating custom spans
Testing custom spans implementation
Testing spans usage
Android 提供了几种定义文本样式的方法:
单一样式 使用 XML 属性或者 样式和主题 引入了 TextView 的所有内容的样式。这种方式实现简单,通过 XML 即可实现,但是并不能只定义部分内容的样式。举个例子,通过设置 textStyle=”bold” ,所有的文本都会变为黑体;你不能只定义特定的几个字符为黑体。
多重样式 引入了给一段文本添加多种样式的功能。例如,一个单词斜体而另一个粗体。多重样式可以通过使用 HTML 标签、 Span 或者是在 Canvas 上处理自定义的文本绘制。
左图:单一样式文本。设置了 textSize=”32sp” 和 textStyle=”bold” 的 TextView 。右图:多重样式文本。设置了 ForegroundColorSpan, StyleSpan(ITALIC), ScaleXSpan(1.5f), StrikethroughSpan 的文本。
HTML 标签 是解决简单问题的简单办法,例如使文本加粗、斜体,甚至是显示项目符号点。为了展示含有 HTML 标签的文本,使用 Html.fromHtml 方法。在内部实现时,HTML 标签被转换成了 span 。但是请注意, Html 类并不支持完整的 HTML 标签和 CSS 样式,例如将小黑点改为其他的颜色。
当你有文本样式的需求,但是 Android 平台默认不支持时,你还可以手动地在 Canvas 上绘制文本,例如让文字弯曲排布。
Span 允许你实现具有更细粒度自定义的多重样式文本。举个例子,通过 BulletSpan ,你可以定义你的段落文本拥有项目符号点。你可以定制文本和点号之间的间距和点号的颜色。从 Android P 开始,你甚至可以 设置点号的半径 。你也可以创建 span 的自定义实现。在文章中查看 “创建自定义 span” 部分可以找到如何实现。
左图:使用 HTML 标签;中图:使用 BulletSpan,默认圆点大小;右图:在 Android P 上使用 BulletSpan 或者自定义实现。
你可以组合使用单一样式和多重样式。你可以考虑将设置给 TextView 的样式作为一种“基本”样式,而 span 文本样式是应用在基本样式“之上”并且会覆盖基本样式的样式。例如,当给一个 TextView 设置了 textColor=”@color.blue” 属性且给头4个字符应用了 ForegroundColorSpan(Color.PINK) ,则头4个字符会使用 span 设置的粉色,而其他文本使用 TextView 属性设置的颜色。
TextView 组合使用 XML 属性和 span 样式
当使用 span 时,你会和以下类的其中之一打交道: SpannedString , SpannableString 或 SpannableStringBuilder 。 它们之间的区别在于文本或标记对象是可改变的还是不可改变的以及它们使用的内部结构: SpannedString 和 SpannableString 使用线性数组记录已添加的 span,而 SpannableStringBuilder 使用 区间树 。
下面是如何决定使用哪一个的方法:
举个例子,你用到的文本并不会改变,但你想要附加 span 时,你应该使用 SpannableString 。
上面所有的这些类都继承自 Spanned 接口,但拥有可变标记的类( SpannableString 和 SpannableStringBuilder ) 同时也继承自 Spannable 。
Spanned -- 带有不可变标记的不可变文本
Spannable (继承自 Spanned) -- 带有可变标记的不可变文本
通过 Spannable 对象调用 setSpan(Object what, int start, int end, int flags) 方法应用 span。 What 对象是一个标记,应用于从开始到结束的索引之间的文本。falg 标志位标记了 span 是否应该扩展至包含插入文本的开始和结束的点。任何标志位设置以后,只要插入文本的位置位于开始位置和结束位置之间,span 就会自动的扩展。
举个例子,设置 ForegroundColorSpan 可以像下面这样完成:
因为 span 设置时使用了 SPAN_EXCLUSIVE_INCLUSIVE 标志位,在 span 的后面插入文本时,新插入的文本也会自动地继承此 span。
左图:带有 ForegroundColorSpan 的文本;右图:带有 ForegroundColorSpan 和 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 的文本。
如果 span 设置了 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 标志位,在 span 后面插入文本时则不会修改 span 的结束索引。
多个 span 可以被组合且同时附加到同一段文本上。例如,粗体红色的文本可以像这样构建:
带有多种 span 的文本:ForegroundColorSpan(Color.RED) 和 StyleSpan(BOLD)
Android framework 定义了几个接口和抽象类,它们会在测量和渲染时被检查。这些类有允许 span 访问像 TextPaint 或 Canvas 对象的方法。
Android framework 在 android.text.style 包、主要接口的字类和抽象类中提供了20+的 span, 我们可以通过下面几个方式对 span 进行分类:
span 类别:字符对比段落,外形对比大小
第一种类型以修改外形的方式在字符级别起作用:文本或背景颜色、下划线、中横线等等,它会触发文本重新绘制但是并不会重新布局。这些 span 引入了 UpdateAppearance 且继承自 CharacterStyle . CharacterStyle 字类通过提供更新 TextPaint 的访问方法,定义了怎样绘制文本。
影响外形的 span
影响尺寸 的 span 更改了文本的尺寸和布局,因此观察 span 的变化的对象会重新绘制文本以保证布局和渲染的正确。
举个例子,影响文本字体大小的 span 要求重新测量和布局,也要求重新绘制。这种 span 通常继承自 MetricAffectingSpan 类。这个抽象类通过提供对 TextPaint 的访问,允许字类定义 span 如何影响文本测量,而 MetricAffectingSpan 继承自 CharacterSpan ,子类在字符级别影响文本的外形。
影响尺寸的 span
你可能会想要一直重新创建带有文本和标记的 CharSequence 并且调用 TextView.setText(CharSequence) 方法,但是这样做很有可能一直触发已经创建好的布局和额外的对象的重新测量和重新绘制。为了减少性能损耗,将文本设置为 TextView.setText(Spannable, BufferType.SPANNABLE) ,然后当你需要更改 span 的时候,通过将 TextView.getText() 转换为 Spannable 从 TextView 获得 Spannable 对象。我们会在未来的文章中详细讨论 TextView.setText 的实现原理和不同的性能优化方式。
举个例子,考虑通过这样的方式设置和获取 Spannable :
现在,当我们在 spannableText 上设置了 span 之后,我们就不需要再调用 textView.setText 了,因为我们正在直接修改 TextView 持有的 CharSequence 对象的引用。
这是当我们设置了不同的 span 之后会发生什么:
情形1: 影响外观的 span
当我们附加了一个影响外观的 span 之后, TextView.onDraw 方法被调用但 TextView.onLayout 没有。这是文本重绘,但宽和高保持原样。
情形2: 影响尺寸的 span
因为 RelativeSizeSpan 改变了文本的大小,文本的宽和高变化,文本的布局方式(举个例子,在 TextView 的大小没有变化的情况下,一个特定的单词现在可能会换行)。 TextView 需要计算新的大小所以 onMeasure 和 onLayout 均被调用。
左图:ForegroundColorSpan——影响外观的 span;右图:RelativeSizeSpan——影响尺寸的 span
一个 span 对文本产生的影响既可以在字符级别,更新元素,如背景颜色、样式或大小,也可以在段落级别,更改整个文本块的对齐或者边距。根据所需的样式,span 既可以继承自 CharacterStyle ,也可以引入 ParagraphStyle 。 继承自 ParagraphStyle 的 span 必须从第一个字符附加到单个段落的最后一个字符,否则 span 不会被显示。在 Android 中,段落是基于换行符 (\n) 定义的。
在 Android 中,段落是基于换行符 (\n) 定义的。
影响段落的 span
举个例子,像 BackgroundColorSpan 这样的 CharacterStyle ,可以被附加到文本中的任何字符上。这里,我们把它附加到第五到第八个字符上。
ParagraphStyle 的 span,像 QuoteSpan ,只能够被附加到段落的开始,否则行和文本之间的边距就不会出现。例如,“Text is\nspantastic” 在文本的第8个字符包含了一个换行符,所以我们可以给它附加一个 QuoteSpan ,段落从那里开始就会被添加样式。如果我们在0或8之外的任何位置附加 span,text 就不会被添加样式。
左图:BackgroundColorSpan -- 影响字符的 span。右图:QuoteSpan -- 影响段落的 span
在实现你自己的 span 时,你需要确定你的 span 是否会影响字符或段落级别的文本,以及它是否也会影响文本的布局或外观。但是,在从头开始编写自己的实现之前,检查一下是否可以使用 framework 中提供的 span。
太长不看:
假设我们需要实现一个 span,它允许以一定的比例增加文本的大小,比如 RelativeSizeSpan ,并设置文本的颜色,比如 ForegroundColorSpan 。为此,我们可以扩展 RelativeSizeSpan ,并且 RelativeSizeSpan 提供了 updateDrawState 和 updateMeasureState 回调,我们可以复写绘制状态回调并设置 TextPaint 的颜色。
注意:同样的效果可以通过在同一文本上同时应用 RelativeSizeSpan 和 ForegroundColorSpan 实现。
测试 span 意味着检查确实已对 TextPaint 进行了预期的修改,或者是否已经将正确的元素绘制到了 canvas 上。例如,假设一个 span 的自定义实现为段落添加制定大小和颜色的项目符号点,以及左边距和项目符号点之间的间隙。在 android-text sample 查看具体实现。为了测试这个类,实现一个 AndroidJUnit 类,确实检查:
测试 Canvas 的交互可以通过 mock canvas,给 drawLeadingMargin 方法传 mock 过的引用并使用正确的参数验证是否已调用正确的方法来实现。
在 BulletPointSpanTest 查看其余的测试。
Spanned 接口允许给文本设置 span 和从文本获取 span 。通过实现一个 Android JUnit 测试,检查是否在正确的位置添加了正确的 span。在 android-text sample 我们把项目符号点标记标签转换为了项目符号点。这是通过给文本在正确的位置附加 BulletPointSpans 。 下面展示了它是如何被测试的:
查看 MarkdownBuilderTest 获取更多测试示例。
span 是一个非常强大的概念,它深深的嵌入在文本渲染功能中。它们可以访问 TextPaint 和 Canvas 等组件,这些组件允许在 Android 上使用高度可自定义的文本样式。在 Android P 中,我们为 framework span 添加了大量文档,所以,在实现你自己的 span 之前,查看那些能够获取到的内容。
在以后的文章中,我们将向你详细介绍 span 在底层是如何工作的以及怎样高效地使用它们。例如,你需要使用 textView.setText(CharSequence, BufferType) 或者 Spannable.Factory 。 有关原因的详细信息,请保持关注。
本文原作者 Florina Muntenescu ,Android Developer Advocate @Google . 原文地址: . 本文由 TonnyL 翻译,发表在:
Android 中线程可分为 主线程 和 子线程 两类,其中主线程也就是 UI线程 ,它的主要这作用就是运行四大组件、处理界面交互。子线程则主要是处理耗时任务,也是我们要重点分析的。
首先 Java 中的各种线程在 Android 里是通用的,Android 特有的线程形态也是基于 Java 的实现的,所以有必要先简单的了解下 Java 中的线程,本文主要包括以下内容:
在 Java 中要创建子线程可以直接继承 Thread 类,重写 run() 方法:
或者实现 Runnable 接口,然后用Thread执行Runnable,这种方式比较常用:
简单的总结下:
Callable 和 Runnable 类似,都可以用来处理具体的耗时任务逻辑的,但是但具体的差别在哪里呢?看一个小例子:
定义 MyCallable 实现了 Callable 接口,和之前 Runnable 的 run() 方法对比下, call() 方法是有返回值的哦,泛型就是返回值的类型:
一般会通过线程池来执行 Callable (线程池相关内容后边会讲到),执行结果就是一个 Future 对象:
可以看到,通过线程池执行 MyCallable 对象返回了一个 Future 对象,取出执行结果。
Future 是一个接口,从其内部的方法可以看出它提供了取消任务(有坑!!!)、判断任务是否完成、获取任务结果的功能:
Future 接口有一个 FutureTask 实现类,同时 FutureTask 也实现了 Runnable 接口,并提供了两个构造函数:
用 FutureTask 一个参数的构造函数来改造下上边的例子:
FutureTask 内部有一个 done() 方法,代表 Callable 中的任务已经结束,可以用来获取执行结果:
所以 Future + Callable 的组合可以更方便的获取子线程任务的执行结果,更好的控制任务的执行,主要的用法先说这么多了,其实 AsyncTask 内部也是类似的实现!
注意, Future 并不能取消掉运行中的任务,这点在后边的 AsyncTask 解析中有提到。
Java 中线程池的具体的实现类是 ThreadPoolExecutor ,继承了 Executor 接口,这些线程池在 Android 中也是通用的。使用线程池的好处:
常用的构造函数如下:
一个常规线程池可以按照如下方式来实现:
执行任务:
基于 ThreadPoolExecutor ,系统扩展了几类具有新特性的线程池:
线程池可以通过 execute() 、 submit() 方法开始执行任务,主要差别从方法的声明就可以看出,由于 submit() 有返回值,可以方便得到任务的执行结果:
要关闭线程池可以使用如下方法:
IntentService 是 Android 中一种特殊的 Service,可用于执行后台耗时任务,任务结束时会自动停止,由于属于系统的四大组件之一,相比一般线程具有较高的优先级,不容易被杀死。用法和普通 Service 基本一致,只需要在 onHandleIntent() 中处理耗时任务即可:
至于 HandlerThread,它是 IntentService 内部实现的重要部分,细节内容会在 IntentService 源码中说到。
IntentService 首次创建被启动的时候其生命周期方法 onCreate() 会先被调用,所以我们从这个方法开始分析:
这里出现了 HandlerThread 和 ServiceHandler 两个类,先搞明白它们的作用,以便后续的分析。
首先看 HandlerThread 的核心实现:
首先它继承了 Thread 类,可以当做子线程来使用,并在 run() 方法中创建了一个消息循环系统、开启消息循环。
ServiceHandler 是 IntentService 的内部类,继承了 Handler,具体内容后续分析:
现在回过头来看 onCreate() 方法主要是一些初始化的操作, 首先创建了一个 thread 对象,并启动线程,然后用其内部的 Looper 对象 创建一个 mServiceHandler 对象,将子线程的 Looper 和 ServiceHandler 建立了绑定关系,这样就可以使用 mServiceHandler 将消息发送到子线程去处理了。
生命周期方法 onStartCommand() 方法会在 IntentService 每次被启动时调用,一般会这里处理启动 IntentService 传递 Intent 解析携带的数据:
又调用了 start() 方法:
就是用 mServiceHandler 发送了一条包含 startId 和 intent 的消息,消息的发送还是在主线程进行的,接下来消息的接收、处理就是在子线程进行的:
当接收到消息时,通过 onHandleIntent() 方法在子线程处理 intent 对象, onHandleIntent() 方法执行结束后,通过 stopSelf(msg.arg1) 等待所有消息处理完毕后终止服务。
为什么消息的处理是在子线程呢?这里涉及到 Handler 的内部消息机制,简单的说,因为 ServiceHandler 使用的 Looper 对象就是在 HandlerThread 这个子线程类里创建的,并通过 Looper.loop() 开启消息循环,不断从消息队列(单链表)中取出消息,并执行,截取 loop() 的部分源码:
dispatchMessage() 方法间接会调用 handleMessage() 方法,所以最终 onHandleIntent() 就在子线程中划线执行了,即 HandlerThread 的 run() 方法。
这就是 IntentService 实现的核心,通过 HandlerThread + Hanlder 把启动 IntentService 的 Intent 从主线程切换到子线程,实现让 Service 可以处理耗时任务的功能!
AsyncTask 是 Android 中轻量级的异步任务抽象类,它的内部主要由线程池以及 Handler 实现,在线程池中执行耗时任务并把结果通过 Handler 机制中转到主线程以实现UI操作。典型的用法如下:
从 Android3.0 开始,AsyncTask 默认是串行执行的:
如果需要并行执行可以这么做:
AsyncTask 的源码不多,还是比较容易理解的。根据上边的用法,可以从 execute() 方法开始我们的分析:
看到 @MainThread 注解了吗?所以 execute() 方法需要在主线程执行哦!
进而又调用了 executeOnExecutor() :
可以看到,当任务正在执行或者已经完成,如果又被执行会抛出异常!回调方法 onPreExecute() 最先被执行了。
传入的 sDefaultExecutor 参数,是一个自定义的串行线程池对象,所有任务在该线程池中排队执行:
可以看到 SerialExecutor 线程池仅用于任务的排队, THREAD_POOL_EXECUTOR 线程池才是用于执行真正的任务,就是我们线程池部分讲到的 ThreadPoolExecutor :
再回到 executeOnExecutor() 方法中,那么 exec.execute(mFuture) 就是触发线程池开始执行任务的操作了。
那 executeOnExecutor() 方法中的 mWorker 是什么? mFuture 是什么?答案在 AsyncTask 的构造函数中:
原来 mWorker 是一个 Callable 对象, mFuture 是一个 FutureTask 对象,继承了 Runnable 接口。所以 mWorker 的 call() 方法会在 mFuture 的 run() 方法中执行,所以 mWorker 的 call() 方法在线程池得到执行!
同时 doInBackground() 方法就在 call() 中方法,所以我们自定义的耗时任务逻辑得到执行,不就是我们第二部分讲的那一套吗!
doInBackground() 的返回值会传递给 postResult() 方法:
就是通过 Handler 将最终的耗时任务结果从子线程发送到主线程,具体的过程是这样的, getHandler() 得到的就是 AsyncTask 构造函数中初始化的 mHandler , mHander 又是通过 getMainHandler() 赋值的:
可以在看到 sHandler 是一个 InternalHandler 类对象:
所以 getHandler() 就是在得到在主线程创建的 InternalHandler 对象,所以
就可以完成耗时任务结果从子线程到主线程的切换,进而可以进行相关UI操作了。
当消息是 MESSAGE_POST_RESULT 时,代表任务执行完成, finish() 方法被调用:
如果任务没有被取消的话执行 onPostExecute() ,否则执行 onCancelled() 。
如果消息是 MESSAGE_POST_PROGRESS , onProgressUpdate() 方法被执行,根据之前的用法可以 onProgressUpdate() 的执行需要我们手动调用 publishProgress() 方法,就是通过 Handler 来发送进度数据:
进行中的任务如何取消呢?AsyncTask 提供了一个 cancel(boolean mayInterruptIfRunning) ,参数代表是否中断正在执行的线程任务,但是呢并不靠谱, cancel() 的方法注释中有这么一段:
大致意思就是调用 cancel() 方法后, onCancelled(Object) 回调方法会在 doInBackground() 之后被执行而 onPostExecute() 将不会被执行,同时你应该 doInBackground() 回调方法中通过 isCancelled() 来检查任务是否已取消,进而去终止任务的执行!
所以只能自己动手了:
AsyncTask 整体的实现流程就这些了,源码是最好的老师,自己跟着源码走一遍有些问题可能就豁然开朗了!
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流