Mon工具-MarkDown语法的编辑

#Mon

Mou icon

MarkeDown语法

Mou, the missing Markdown editor for web developers.

Syntax

Strong and Emphasize

strong or strong ( Cmd + B )

emphasize or emphasize ( Cmd + I )

Sometimes I want a lot of text to be bold.
Like, seriously, a LOT of text

Blockquotes

Right angle brackets > are used for block quotes.

An email example@example.com link.

Simple inline link http://chenluois.com, another inline link Smaller, one more inline link with title Resize.

A reference style link. Input id, then anywhere in the doc, define the link with corresponding id:

Titles ( or called tool tips ) in the links are optional.

Images

An inline image Smaller icon, title is optional.

A Resize icon reference style image.

Inline code and Block code

Inline code are surround by backtick key. To create a block code:

Indent each line by at least 1 tab, or 4 spaces.
var Mou = exactlyTheAppIwant; 

Ordered Lists

Ordered lists are created using “1.” + Space:

  1. Ordered list item
  2. Ordered list item
  3. Ordered list item

Unordered Lists

Unordered list are created using “*” + Space:

  • Unordered list item
  • Unordered list item
  • Unordered list item

Or using “-“ + Space:

  • Unordered list item
  • Unordered list item
  • Unordered list item

Hard Linebreak

End a line with two or more spaces will create a hard linebreak, called <br /> in HTML. ( Control + Return )
Above line ended with 2 spaces.

Horizontal Rules

Three or more asterisks or dashes:




Headers

Setext-style:

This is H1

This is H2

atx-style:

This is H1

This is H2

This is H3

This is H4

This is H5
This is H6

Extra Syntax

Footnotes

Footnotes work mostly like reference-style links. A footnote is made of two things: a marker in the text that will become a superscript number; a footnote definition that will be placed in a list of footnotes at the end of the document. A footnote looks like this:

That’s some text with a footnote.[^1]

[^1]: And that’s the footnote.

Strikethrough

Wrap with 2 tilde characters:

Strikethrough

Fenced Code Blocks

Start with a line containing 3 or more backticks, and ends with the first line with the same number of backticks:

1
2
3
Fenced code blocks are like Stardard Markdown’s regular code
blocks, except that they’re not indented and instead rely on
a start and end fence lines to delimit the code block.

Tables

A simple table looks like this:

First Header Second Header Third Header
Content Cell Content Cell Content Cell
Content Cell Content Cell Content Cell

If you wish, you can add a leading and tailing pipe to each line of the table:

First Header Second Header Third Header
Content Cell Content Cell Content Cell
Content Cell Content Cell Content Cell

Specify alignment for each column by adding colons to separator lines:

First Header Second Header Third Header
Left Center Right
Left Center Right

Shortcuts

View

  • Toggle live preview: Shift + Cmd + I
  • Toggle Words Counter: Shift + Cmd + W
  • Toggle Transparent: Shift + Cmd + T
  • Toggle Floating: Shift + Cmd + F
  • Left/Right = 1/1: Cmd + 0
  • Left/Right = 3/1: Cmd + +
  • Left/Right = 1/3: Cmd + -
  • Toggle Writing orientation: Cmd + L
  • Toggle fullscreen: Control + Cmd + F

Actions

  • Copy HTML: Option + Cmd + C
  • Strong: Select text, Cmd + B
  • Emphasize: Select text, Cmd + I
  • Inline Code: Select text, Cmd + K
  • Strikethrough: Select text, Cmd + U
  • Link: Select text, Control + Shift + L
  • Image: Select text, Control + Shift + I
  • Select Word: Control + Option + W
  • Select Line: Shift + Cmd + L
  • Select All: Cmd + A
  • Deselect All: Cmd + D
  • Convert to Uppercase: Select text, Control + U
  • Convert to Lowercase: Select text, Control + Shift + U
  • Convert to Titlecase: Select text, Control + Option + U
  • Convert to List: Select lines, Control + L
  • Convert to Blockquote: Select lines, Control + Q
  • Convert to H1: Cmd + 1
  • Convert to H2: Cmd + 2
  • Convert to H3: Cmd + 3
  • Convert to H4: Cmd + 4
  • Convert to H5: Cmd + 5
  • Convert to H6: Cmd + 6
  • Convert Spaces to Tabs: Control + [
  • Convert Tabs to Spaces: Control + ]
  • Insert Current Date: Control + Shift + 1
  • Insert Current Time: Control + Shift + 2
  • Insert entity <: Control + Shift + ,
  • Insert entity >: Control + Shift + .
  • Insert entity &: Control + Shift + 7
  • Insert entity Space: Control + Shift + Space
  • Insert Scriptogr.am Header: Control + Shift + G
  • Shift Line Left: Select lines, Cmd + [
  • Shift Line Right: Select lines, Cmd + ]
  • New Line: Cmd + Return
  • Comment: Cmd + /
  • Hard Linebreak: Control + Return

Edit

  • Auto complete current word: Esc
  • Find: Cmd + F
  • Close find bar: Esc

Post

  • Post on Scriptogr.am: Control + Shift + S
  • Post on Tumblr: Control + Shift + T

Export

  • Export HTML: Option + Cmd + E
  • Export PDF: Option + Cmd + P

And more?

Don’t forget to check Preferences, lots of useful options are there.

Follow @Mou on Twitter for the latest news.

For feedback, use the menu Help - Send Feedback

微信热修复Tinker的实践

Tinker热修复简介

Tinker是微信开源出的Android热修复框架,基于MultiDex.

#接入Tinker相关依赖
1.项目的根目录build.gradle:
``bash

dependencies {
classpath ‘com.tencent.tinker:tinker-patch-gradle-plugin:1.7.1’
}

2.项目下的APP目录下的build.gradle:bash
compile(‘com.tencent.tinker:tinker-android-anno:1.7.1’)
//tinker 核心 Android lib
compile(‘com.tencent.tinker:tinker-android-lib:1.7.1’)

3.添加gradle脚本,主要是生成patch包,Tinker是采用插件的形式,你可以参照Tinker-sample-android的做法:bash

def bakPath = file(“${buildDir}/bakApk/“)

/**

  • you can use assembleRelease to build you base apk
  • use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
  • add apk from the build/bakApk
    */
    ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //you should bak the following files
    //old apk file to build patch apk
    tinkerOldApkPath = “${bakPath}/Meiya-debug-tt.apk”
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = “${bakPath}”
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = “${bakPath}”
    }

def getOldApkPath() {
return hasProperty(“OLD_APK”) ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
return hasProperty(“APPLY_MAPPING”) ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
return hasProperty(“APPLY_RESOURCE”) ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
return hasProperty(“TINKER_ID”) ? TINKER_ID : gitSha()
}

def buildWithTinker() {
return hasProperty(“TINKER_ENABLE”) ? TINKER_ENABLE : ext.tinkerEnabled
}
def gitSha() {
try {
String gitRev = ‘git rev-parse –short HEAD’.execute().text.trim()
if (gitRev == null) {
throw new GradleException(“can’t get git rev, you should add git to system path or just input test value, such as ‘testTinkerId’”)
}
return gitRev
} catch (Exception e) {
throw new GradleException(“can’t get git rev, you should add git to system path or just input test value, such as ‘testTinkerId’”)
}
}
if (buildWithTinker()) {
apply plugin: ‘com.tencent.tinker.patch’

tinkerPatch {
    /** 全局信息相关的配置项
     * necessary,default 'null'
     * the old apk path, use to diff with the new apk to build
     * add apk from the build/bakApk
     */
    oldApk = getOldApkPath()
    /**
     如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
     1. minSdkVersion小于14,但是dexMode的值为"raw";
     2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
     3. 定义在dex.loader用于加载补丁的类不在main dex中;
     4. 定义在dex.loader用于加载补丁的类出现修改;
     5. resources.arsc改变,但没有使用applyResourceMapping编译
     */
    ignoreWarning = true
    /**
     在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名
     */
    useSign = true

    /**
     * Warning, applyMapping will affect the normal android build!
     */
    buildConfig {
        /** 编译相关的配置项
         * optional,default 'null'
         * if we use tinkerPatch to build the patch apk, you'd better to apply the old
         * apk mapping file if minifyEnabled is enable!
         * Warning:
         * you must be careful that it will affect the normal assemble build!
         */
        applyMapping = getApplyMappingPath()
        /**
         可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少
         补丁包的大小,同时也避免由于ResId改变导致remote view异常
         */
        applyResourceMapping = getApplyResourceMappingPath()

        /**
         * necessary,default 'null'
         在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个
         是决定补丁包能
         运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等
         */
        tinkerId = getTinkerIdValue()
    }

    dex {
        /**
         * optional,default 'jar'
         * only can be 'raw' or 'jar'. for raw, we would keep its original format
         * for jar, we would repack dexes with zip format.
         * if you want to support below 14, you must use jar
         * or you want to save rom or check quicker, you can use raw mode also
         */
        dexMode = "jar"
        /**
         * necessary,default '[]'
         * what dexes in apk are expected to deal with tinkerPatch
         * it support * or ? pattern.
         */
        pattern = ["classes*.dex",
                   "assets/secondary-dex-?.jar"]
        /**
         * necessary,default '[]'
         * Warning, it is very very important, loader classes can't change with patch.
         * thus, they will be removed from patch dexes.
         * you must put the following class into main dex.
         * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
         * own tinkerLoader, and the classes you use in them
         *
         */
        loader = ["com.tencent.tinker.loader.*",
                  "com.meiyaapp.meiya.APP",
                  //use sample, let BaseBuildInfo unchangeable with tinker
                  "tinker.com.sen.mytinkerdemo.BaseBuildInfo"
        ]
    }

    lib {
        /**
         * optional,default '[]'
         * what library in apk are expected to deal with tinkerPatch
         * it support * or ? pattern.
         * for library in assets, we would just recover them in the patch directory
         * you can get them in TinkerLoadResult with Tinker
         */
        pattern = ["lib/armeabi/*.so"]
    }

    res {
        /**
         * optional,default '[]'
         * what resource in apk are expected to deal with tinkerPatch
         * it support * or ? pattern.
         * you must include all your resources in apk here,
         * otherwise, they won't repack in the new apk resources.
         */
        pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

        /**
         * optional,default '[]'
         * the resource file exclude patterns, ignore add, delete or modify resource change
         * it support * or ? pattern.
         * Warning, we can only use for files no relative with resources.arsc
         */
        ignoreChange = ["assets/sample_meta.txt"]

        /**
         * default 100kb
         * for modify resource, if it is larger than 'largeModSize'
         * we would like to use bsdiff algorithm to reduce patch file size
         */
        largeModSize = 100
    }

    packageConfig {
        /**
         * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
         * package meta file gen. path is assets/package_meta.txt in patch file
         * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
         * or TinkerLoadResult.getPackageConfigByName
         * we will get the TINKER_ID from the old apk manifest for you automatic,
         * other config files (such as patchMessage below)is not necessary
         */
        configField("patchMessage", "tinker is sample to use")
        /**
         * just a sample case, you can use such as sdkVersion, brand, channel...
         * you can parse it in the SamplePatchListener.
         * Then you can use patch conditional!
         */
        configField("platform", "all")

    }
    //or you can add config filed outside, or get meta value from old apk
    //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
    //project.tinkerPatch.packageConfig.configField("test2", "sample")

    /**
     * if you don't use zipArtifact or path, we just use 7za to try
     */
    sevenZip {
        /**
         * optional,default '7za'
         * the 7zip artifact path, it will use the right 7za with your platform
         */
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        /**
         * optional,default '7za'
         * you can specify the 7za path yourself, it will overwrite the zipArtifact value
         */

// path = “/usr/local/bin/7za”
}
}

}

/**

  • task type, you want to bak
    /
    def taskName = “debug”
    /*
  • bak apk and mapping
    */

if (buildWithTinker()) {//在Tinker模式下复制bakApk(by shouwang)
tasks.getByName(“assemble${taskName.capitalize()}”) {
it.doLast {
copy {

            def date = new Date().format("MMdd-HH-mm-ss")
            from "${buildDir}/outputs/apk/${project.getName()}-${taskName}.apk"
            into bakPath
            rename { String fileName ->
                fileName.replace("${project.getName()}-${taskName}.apk", "${project.getName()}-${taskName}-${date}.apk")
            }

            from "${buildDir}/outputs/mapping/${taskName}/mapping.txt"
            into bakPath
            rename { String fileName ->
                fileName.replace("mapping.txt", "${project.getName()}-${taskName}-${date}-mapping.txt")
            }

            from "${buildDir}/intermediates/symbols/${taskName}/R.txt"
            into bakPath
            rename { String fileName ->
                fileName.replace("R.txt", "${project.getName()}-${taskName}-${date}-R.txt")
            }

        }
    }
}

}

``

如何生成补丁,这边有几个步骤

1.修改buildPatch.gradle里面tinkerOldApkPath的值(对应上个apk名称,就是要修复的APK)(运行项目会生成在APP/build/bakApk/ 目录下)
2.要填写TinkerId的值,在gradle.properties里,项目里采用App-VersionName(默认是git版本号)
3.修改buildPatch.gradle里tinkerEnabled的值为true(注:发版本的时候也要用true打包)
4.运行task tinkerPatchDebug.补丁包为patch_signed_7zip.apk,生成目录在App/build/output/tinkerPatch/ 下
5.可以push到手机测试

这里注意一点:Tinker支持对同一基准版本做多次补丁修复,在生成补丁时,oldApk依然是已经发布出去的那个版本。即补丁版本二的oldApk不能是补丁版本一,它应该依然是用户手机上已经安装的基准版本。
bash adb push ./Meiya/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/

关于打包

我们需要保证tinkerId一定是要唯一性的,这里我们一定要注意,升级可客户端版本,需要更新tinkerId!

补丁加载

关于补丁加载,其实主要是项目Applicaion类的改造,改造的话主要是代理:

``bash
public class APP extends TinkerApplication {
private static APP sInstance;
public APP() {

super(
        //tinker标志, 要支持那种类型 默认为ALL
        //dex only, library only, all support
        ShareConstants.TINKER_ENABLE_ALL,
        // 这边最好用完整包名,参数:AppApplication的托管类,Tinker的核心类,是否验证
        // have a binary dependency on your ApplicationLifeCycle class.
        "com.meiyaapp.meiya.MeiyaApplicationLike","com.tencent.tinker.loader.TinkerLoader",false);
sInstance=this;

}

public static APP getInstance() {
return sInstance;
}

``
注:Tinker前提是项目的Application类不能引入其他类,否者会导致他们无法被补丁修改。
改造的方法采用委托:这里将全部的实现移到了单独的类里面SampleApplicationLike.java(具体参照项目源码)

android 热修复原理与实践

#什么是热修复?

##传统的开发流程-版本上线-安装-发现bug-紧急修复-发布新版本。
热修复说白了就是“打补丁”-让用户下载没有bug的代码替换掉当前apk的代码-解决了问题,也提高了用户体验。

#业界热门的热修复技术

总结Android开发流程

#前言

##对于开发App 无论你是在哪个公司,无论你是不是公司的主程,还是说小弟,我觉得吧,你都应该有一套完成开发一个App的流程,当然这里主要是说安卓开发。

#开发环境选择

##很早之前是采用eclipse,自从进了美芽,在前辈的带领下开启了AndroidStudio之旅,越来越觉得AndroidStudio的好处,从此就用这开发了。

#模拟器的选择

##选好的模拟器很重要的,尤其是运行速度,不能卡。Genymotion是好的选择。

#产品开发流程

##正常的互联网开发app的流程大致如下:

-产品规划,定产品方向
=需求调研,产出需求文档
-需求评审,修订需求文档
-产品狗画app线框图提供给射鸡师
-射鸡师根据线框图设计视觉稿
-程序猿根据视觉稿搭建UI框架
-程序猿根据需求文档开发功能
-测试媛编写测试用例,根据排期进行测试
-程序猿修复回归测试反馈的bug,提交beta版
-测试通过,提交给运营喵发布到渠道上线

#定义一套开发规范

##这边规范主要主要是命名规范(项目命名、包命名、变量命名、资源文件命名、http://blog.csdn.net/wwj_748/article/details/42347283)

#代码管理

##一个产品迭代是必须的最好是采用git管理,可以自己搭建gitlab

#架构搭建

##Android应用其实就是简单的MVC-MVP框架,基本的视图与数据分离方式;

#架构设计

##包括接口设计、技术选型、数据层设计、业务层设计、展示层设计

#接口设计

##1.安全机制 RESTful,设计Token用户用密码登录成功后,服务器返回token给客户端;
客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
token错误,这时需要用户重新登录,获取正确的token
token过期,这时客户端需要再发起一次认证请求,获取新的token

#接口数据设计

##JSON数据格式
Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号[]中
Object:对象包含在大括号{}中
Null:空类型

#接口版本设计/v2 /v1

#技术选型

##主要是H5/Native的抉择

#轮子的选择

##UI框架(比如下拉刷新PullToRefresh、侧滑菜单Slidingmenu)
网络请求库(比如okhtttp、AndroidAsyncHttp、Volley)
数据操作库(比如GreenDao、Ormlite)
图片缓存框架(比如Universal-Imageloader)
数据解析库(比如Gson)

#第三方集成

#云测

#混淆

#打包

#APP壳

参考:http://www.jianshu.com/p/42c249168275
http://keeganlee.me/post/architecture/20160222

Android解析Html标签

#富文本

在安卓显示富文本是一个比较常见的功能,我们对一个TextView的文本显示不同的样式,包括字体颜色、字体大小等等。

#实现方式

有两种简单的方式实现富文本的现实,有个共同点就是和html有关。一个是采用webView直接显示;一个是个用TextView解析标签。

#关于Html.formHtml()

##安卓自带的解析是有限的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<a href="...">
<b>
<big>
<blockquote>
<br>
<cite>
<dfn>
<div align="...">
<em>
<font size="..." color="..." face="...">
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
<i>
<img src="...">
<p>
<small>
<strike>
<strong>
<sub>
<sup>
<tt>
<u>

也就是说除了以上标签,其他的自定义标签就得自己实现;

#如何实现

自己实现Html.TagHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* HTML标签解析
* 实现Html.TagHandle解析
*
* @author shouwang
* @date 2016-7-20
*/
public class HtmlParser implements Html.TagHandler, ContentHandler {

private final TagHandler handler;
private ContentHandler wrapped;
private Editable text;
private ArrayDeque<Boolean> tagStatus = new ArrayDeque<Boolean>();

public HtmlParser(TagHandler handler) {
this.handler = handler;
}

public static Spanned buildSpannedText(String html, TagHandler handler) {
return Html.fromHtml(html, null, new HtmlParser(handler));
}

public static String getValue(Attributes attributes, String name) {
for (int i = 0, n = attributes.getLength(); i < n; i++) {
if (name.equals(attributes.getLocalName(i)))
return attributes.getValue(i);
}
return null;
}

@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (wrapped == null) {
text = output;
wrapped = xmlReader.getContentHandler();
xmlReader.setContentHandler(this);
tagStatus.addLast(Boolean.FALSE);
}
}

@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
boolean isHandled = handler.handleTag(true, localName, text, attributes);
tagStatus.addLast(isHandled);
if (!isHandled)
wrapped.startElement(uri, localName, qName, attributes);
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (!tagStatus.removeLast())
wrapped.endElement(uri, localName, qName);
handler.handleTag(false, localName, text, null);
}

@Override
public void setDocumentLocator(Locator locator) {
wrapped.setDocumentLocator(locator);
}

@Override
public void startDocument() throws SAXException {
wrapped.startDocument();
}

@Override
public void endDocument() throws SAXException {
wrapped.endDocument();
}

@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
wrapped.startPrefixMapping(prefix, uri);
}

@Override
public void endPrefixMapping(String prefix) throws SAXException {
wrapped.endPrefixMapping(prefix);
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
wrapped.characters(ch, start, length);
}

@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
wrapped.ignorableWhitespace(ch, start, length);
}

@Override
public void processingInstruction(String target, String data) throws SAXException {
wrapped.processingInstruction(target, data);
}

@Override
public void skippedEntity(String name) throws SAXException {
wrapped.skippedEntity(name);
}

public interface TagHandler {
boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes);
}
}

#对标签进行解析

##

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
public class HtmlTagFormatter {
private static final String TAG_HANDLE_SPAN = "span";
private static final String TAG_HANDLE_STYLE = "style";
private static final String TAG_HANDLE_ALIGN = "align";
private static final String TAG_FONT_SIZE = "font-size";
private static final String TAG_BACKGROUND_COLOR = "background-color";
private static final String Tag_FONT_COLOR = "color";
private static final String TAG_TEXT_ALIGN = "text-align";
private int startIndex;
private int stopIndex;
private String styleContent = "";
private Vector<String> mListParents = new Vector<String>();//用来标记列表(有序和无序列表)
private int mListItemCount = 0;//用来标记列表(有序和无序列表)

public Spanned handlerHtmlContent(final Context context, String htmlContent) throws NumberFormatException {
return HtmlParser.buildSpannedText(htmlContent, new HtmlParser.TagHandler() {
@Override
public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes) {
if (tag.equals(TAG_HANDLE_SPAN)) {
//<style>标签的处理方式
if (opening) {
startIndex = output.length();
styleContent = HtmlParser.getValue(attributes, TAG_HANDLE_STYLE);
} else {
stopIndex = output.length();
if (!TextUtils.isEmpty(styleContent)) {
String[] styleValues = styleContent.split(";");
for (String styleValue : styleValues) {
String[] tmpValues = styleValue.split(":");
if (tmpValues != null && tmpValues.length > 0) { //(font-size=14px)
if (TAG_FONT_SIZE.equals(tmpValues[0])) { //处理文字效果字体大小
int size = Integer.valueOf(getAllNumbers(tmpValues[1]));
output.setSpan(new AbsoluteSizeSpan(sp2px(context, size)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (TAG_BACKGROUND_COLOR.equals(tmpValues[0])) { //处理背景效果
output.setSpan(new BackgroundColorSpan(Color.parseColor(tmpValues[1])), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (Tag_FONT_COLOR.equals(tmpValues[0])) {//处理字体颜色<span style="color:"#000000">
output.setSpan(new ForegroundColorSpan(Color.parseColor(tmpValues[1])), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if(TAG_TEXT_ALIGN.equals(tmpValues[0])){
handleAlignTag(output,tmpValues[1]);
}
}
}
}
}
} else if (tag.equals(TAG_HANDLE_ALIGN)) {
if (opening) {
startIndex = output.length();
} else {
stopIndex = output.length();
}
}
//列表标签的解析渲染
if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
if (opening) {
mListParents.add(tag);
} else mListParents.remove(tag);

mListItemCount = 0;
} else if (tag.equals("li") && !opening) {
handleListTag(output);
}
return false;
}
});
}

//正则获取字体
private static String getAllNumbers(String body) {
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher(body);
while (matcher.find()) {
return matcher.group(0);
}
return "";
}

//处理列表标签
private void handleListTag(Editable output) {
if (mListParents.lastElement().equals("ul")) {
output.append("\n");
String[] split = output.toString().split("\n");

int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.setSpan(new BulletSpan(15), start, output.length(), 0);
} else if (mListParents.lastElement().equals("ol")) {
mListItemCount++;
output.append("\n");
String[] split = output.toString().split("\n");

int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.insert(start, mListItemCount + ". ");
output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start, output.length(), 0);
}
}
//处理<text-align>
private void handleAlignTag(Editable output,String alignTag){
AlignmentSpan.Standard as=new AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL);
if(alignTag.equals("center")){
as = new AlignmentSpan.Standard(
Layout.Alignment.ALIGN_CENTER);
}else if(alignTag.equals("right")){
as = new AlignmentSpan.Standard(
Layout.Alignment.ALIGN_OPPOSITE);
}else if(alignTag.equals("left")){
as = new AlignmentSpan.Standard(
Layout.Alignment.ALIGN_NORMAL);
}
// 参考:https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/SpannableStringBuilder.java
// throw new RuntimeException("PARAGRAPH span must start at paragraph boundary");
// AlignmentSpan继承ParagraphStyle;会检查前后是不是有换行符\n;没有的话抛出以上异常
if(!"\n".equals(output.charAt(stopIndex-1))) {
output.append("\n");
output.setSpan(as, startIndex, stopIndex+1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}

public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}

#几个坑

##
1.各种预设的span类为我们带来了各种各样的样式,这些span分为两类,

  • 一类是CharacterStyle,也就是说是针对单个字符可以设置的样式
  • 另一类是ParagraphStyle,是针对段落进行的操作,ParagraphStyle的span运行的时候会检测目标段落前后是否有\n,如果没有的话会抛出错误”PARAGRAPH span must start at paragraph boundary”,这是一个大坑请务必要注意。
    2.设置文字大小要进行单位转换
    1
    2
    final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
    return (int) (spValue * fontScale + 0.5f);

#参考
https://commonsware.com/blog/Android/2010/05/26/html-tags-supported-by-textview.html

https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/SpannableStringBuilder.java

http://lrdcq.com/me/read.php/37.htm

Android快速打渠道包

修改项目gradle

##

1
2
3
4
5
6
7
8

buildscript {
......
dependencies{
// add packer-ng
classpath 'com.mcxiaoke.gradle:packer-ng:1.0.5'
}
}

修改修改moudle级别gradle

##

1
2
3
4
5
6
apply plugin: 'packer' 

dependencies {
// add packer-helper
compile 'com.mcxiaoke.gradle:packer-helper:1.0.5'
}

注意:packer-ng 和 packer-helper 的版本号需要保持一致

在你的项目根目录中加入渠道列表文件,比如文件名是market.txt,内容是

1
2
3
Google_Play#play store market
Gradle_Test#test
SomeMarket#some market

#打包

gradle -Pmarket=markets.txt clean apkRelease

#友盟统计

##
最后需要提的一点就是如何让友盟统计知道目前的apk是哪个渠道。首先你需要删除之前的productFlavor或者manifest的占位符的方式的代码,删除AndroidManifest中友盟的渠道Channel的META-Data的配置。
然后在app入口(Application)的onCreate中加入下列代码

1
2
final String market = PackerNg.getMarket(this,"defaul_channel);
AnalyticsConfig.setChannel(market); //AnalyticsConfig是友盟的代码方式设置渠道类

Android样式开发

#Shape

##矩形

1
2
3
4
5
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- solid指定形状的填充色,只有android:color一个属性 -->
<solid android:color="#2F90BD" />
</shape>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//添加内边距
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- solid指定形状的填充色,只有android:color一个属性 -->
<solid android:color="#2F90BD" />
<!-- padding设置内容区域离边界的间距 -->
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp" />
</shape>
//渐变
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- solid指定形状的填充色,只有android:color一个属性 -->
<solid android:color="#2F90BD" />
<!-- padding设置内容区域离边界的间距 -->
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp" />
<!-- gradient设置渐变 -->
<gradient
android:angle="270"
android:endColor="#CC2F90BD"
android:startColor="#222F90BD" />
</shape

参照:https://github.com/guosen/kstyle

Android原生权限探索

AppOpsManager

AppOps就是管理应用权限,但是API被谷歌隐藏了。

###就是我们平常说的应用程序权限管理,但是每次更新时,谷歌都会隐藏入口。(6.0y已经加入运行时权限 也就是基于该类的)

AppOpsManager总体概览

核心服务是AppOpsService,API是AppOpsManager,UI层是AppOpsSummary,AppOpsCategory,配置文件为appops.xml appops_policy.xml。

相关API

int

checkOp(String op, int uid, String packageName)
Op对应一个权限操作,该接口来检测应用是否具有该项操作权限。

int
noteOp(String op, int uid, String packageName)
和checkOp基本相同,但是在检验后会做记录。

int
checkOpNoThrow(String op, int uid, String packageName)
和checkOp类似,但是权限错误,不会抛出SecurityException,而是返回AppOpsManager.MODE_ERRORED.

int
noteOpNoThrow(String op, int uid, String packageName)
类似noteOp,但不会抛出SecurityException。

如何使用?

接口被隐藏了,有人想到直接把类打包成jar然后去调用,其实我们可以采用反射的方式代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final int version = Build.VERSION.SDK_INT;
if (version>19) {
Object appOpsManager = getContext().getSystemService(Context.APP_OPS_SERVICE);
Class c = appOpsManager.getClass();
try {
Class[] arg = new Class[3];
arg[0] = int.class;
arg[1] = int.class;
arg[2] = String.class;
Method lmethod = c.getDeclaredMethod("checkOp", arg);
//27代表录音权限
//op 1~47
return (Integer) lmethod.invoke(appOpsManager, 27, Binder.getCallingUid(), getContext().getPackageName());
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
}
}
return -1;
}

参考

http://blog.csdn.net/hyhyl1990/article/details/46842915

http://tmq.qq.com/2016/06/let-your-brief-encounter-android-permission-to-business-practices/
http://tmq.qq.com/2016/06/android-authorization-management-theory/

TextView textIsSelectable与点击事件共存

#问题所在

如果一个TextView设置了textIsSelectable属性来实现文字可选复制,这时候设置其点击事件(OnClickListener)会出现第二次点击才响应事件;

#解决

采用OnTouch代替

实现

##

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//TextView 设置了TextIsSelectable属性实现选择复制,导致点击事件在点两下才响应/
//采用onTouch拦截
public void setSelectableTextViewClick(final IntentData intentData, TextView textView) {
final GestureDetectorCompat gestureDetector = new GestureDetectorCompat(mActivity, new SimpleOnGestureListener());
gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
GoodDetailActivity.start(intentData);//点击
return false;
}

@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}

@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
});

textView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
return false;
}
});

}