-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 104 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 104 KB
1
{"meta":{"title":"厉圣杰的博客","subtitle":"啦啦啦,德玛西亚","description":"希望有一天我能够很坦然地说:\"让我来告诉你,在我眼中,这是一个怎样的世界。\"","author":"厉圣杰","url":"http://www.littlejie.com"},"pages":[{"title":"tags","date":"2017-02-10T09:12:40.000Z","updated":"2017-02-10T09:13:38.000Z","comments":false,"path":"tags/index.html","permalink":"http://www.littlejie.com/tags/index.html","excerpt":"","text":""},{"title":"categories","date":"2017-02-10T09:13:56.000Z","updated":"2017-02-10T09:14:34.000Z","comments":false,"path":"categories/index.html","permalink":"http://www.littlejie.com/categories/index.html","excerpt":"","text":""}],"posts":[{"title":"Android 应用内多语言切换","slug":"Android-应用内多语言切换","date":"2017-05-19T08:49:43.000Z","updated":"2017-06-05T01:58:16.000Z","comments":true,"path":"2017/05/19/Android-应用内多语言切换/","link":"","permalink":"http://www.littlejie.com/2017/05/19/Android-应用内多语言切换/","excerpt":"","text":"最近公司的 App 里需要用到多语言切换,简单来说,就是如果用户没有选择语言选项时,App 默认跟随系统语言,如果用户在 App 内进行了语言设置,那么就使用用户设置的语言。当然,你会发现,App 的语言切换也会导致加载的其它资源文件进行切换 上述内容大概可以分为以下几块: 获取系统默认的语言和地区(注意地区,后面会讲述这里的坑) 更改 App 的语言 Android 应用资源国际化在正式开始之前,先来讲解一下 Android 应用资源国际化的知识。对于资源文件的国际化,我们一般是在 Android src/main/res/ 目录下,建立对应语言文件夹,格式一般为:values-语言代号-地区代号,默认的资源是不包含语言代号和地区代号的。一般情况下,应用资源是没有做任何适配的,所以不管如何切换语言和地区设置,应用显示的资源都不会发生任何改变。 配置选项包括语言代号和地区代号。表示中文和中国的配置选项是 zh-rCN(zh表示中文,CN表示中国)。表示英文和美国的配置选项是 en-rUS(en表示英文,US表示美国)。同一语言代号可以有多个地区代号,用 r 表示区分。 常见的国际化资源表示形式: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117中文(中国):values-zh-rCN中文(台湾):values-zh-rTW中文(香港):values-zh-rHK维吾尔文(中国):values-ug-rCN英语(美国):values-en-rUS英语(英国):values-en-rGB英文(澳大利亚):values-en-rAU英文(加拿大):values-en-rCA英文(爱尔兰):values-en-rIE英文(印度):values-en-rIN英文(新西兰):values-en-rNZ英文(新加坡):values-en-rSG英文(南非):values-en-rZA阿拉伯文(埃及):values-ar-rEG阿拉伯文(以色列):values-ar-rIL保加利亚文: values-bg-rBG加泰罗尼亚文:values-ca-rES捷克文:values-cs-rCZ丹麦文:values-da-rDK德文(奥地利):values-de-rAT德文(瑞士):values-de-rCH德文(德国):values-de-rDE德文(列支敦士登):values-de-rLI希腊文:values-el-rGR西班牙文(西班牙):values-es-rES西班牙文(美国):values-es-rUS芬兰文(芬兰):values-fi-rFI法文(比利时):values-fr-rBE法文(加拿大):values-fr-rCA法文(瑞士):values-fr-rCH法文(法国):values-fr-rFR希伯来文:values-iw-rIL印地文:values-hi-rIN克罗里亚文:values-hr-rHR匈牙利文:values-hu-rHU印度尼西亚文:values-in-rID意大利文(瑞士):values-it-rCH意大利文(意大利):values-it-rIT日文:values-ja-rJP韩文:values-ko-rKR立陶宛文:valueslt-rLT拉脱维亚文:values-lv-rLV挪威博克马尔文:values-nb-rNO荷兰文(比利时):values-nl-BE荷兰文(荷兰):values-nl-rNL波兰文:values-pl-rPL葡萄牙文(巴西):values-pt-rBR葡萄牙文(葡萄牙):values-pt-rPT罗马尼亚文:values-ro-rRO俄文:values-ru-rRU斯洛伐克文:values-sk-rSK斯洛文尼亚文:values-sl-rSI塞尔维亚文:values-sr-rRS瑞典文:values-sv-rSE泰文:values-th-rTH塔加洛语:values-tl-rPH土耳其文:values–r-rTR乌克兰文:values-uk-rUA越南文:values-vi-rVN 获取系统默认的语言和地区总的来说,获取语言和地区有三种方法: 通过 Java 自带的接口来实现,即: 123Locale locale = Locale.getLocale();String language = locale.getLanguage();String country = locale.getCountry(); 通过 Configuration 来获取 123456//方法1,该方法已废弃,如果在 API >= 17 的版本上使用 方法2Locale locale = context.getResources().getConfiguration().locale;//方法2,在 API >= 17 的版本上可以使用Locale locale = context.getResources().getConfiguration().getLocales().get(0);String language = locale.getLanguage();String country = locale.getCountry(); 其中, context.getResources() 也可以用 Resources.getSystem() 来代替,前者获取的是应用内部的语言和地区设置,后者获取的是系统的语言地区设置,默认情况下,前者跟随系统设置。 更改 App 的语言设置通过上述分析,我们已经知道怎么获取系统和应用的语言地区设置了。接下来,我们来讲一下如何实现 Android App 的多语言切换。在前面我们已经使用到了 Configuration ,这个类保存了 Android 应用的所有设备信息,详见 Configuration。要实现应用的多语言切换,我们所要做的就是更新 Configuration 中关于语言地区的属性。 1234567Resources resources = context.getResources();DisplayMetrics metrics = resources.getDisplayMetrics();Configuration configuration = resources.getConfiguration();//API >= 17 可以使用configuration.setLocale(locale);//该方法已经废弃,官方建议使用 Context.createConfigurationContext(Configuration)resources.updateConfiguration(configuration, metrics); 资源目录结构大致如下: 123456789101112131415161718192021222324252627282930│ └── res│ ├── drawable│ ├── drawable-xhdpi│ │ └── icon_test.png│ ├── drawable-zh-rCN-xhdpi//图标适配│ │ └── icon_test.png│ ├── layout│ │ └── activity_main.xml│ ├── mipmap-hdpi│ │ └── ic_launcher.png│ ├── mipmap-mdpi│ │ └── ic_launcher.png│ ├── mipmap-xhdpi│ │ └── ic_launcher.png│ ├── mipmap-xxhdpi│ │ └── ic_launcher.png│ ├── mipmap-xxxhdpi│ │ └── ic_launcher.png│ ├── values│ │ ├── colors.xml│ │ ├── strings.xml│ │ └── styles.xml│ ├── values-en-rWW│ │ └── strings.xml│ ├── values-ja-rJP│ │ └── strings.xml│ ├── values-ko-rKR│ │ └── strings.xml│ └── values-zh-rCN│ └── strings.xml 重新加载资源看到这里,你是不是觉得就结束了?不,当然不是,更新 Configuration 之后,如果不重启 Activity,应用的资源就不会被重新加载。 1234Intent intent = new Intent(this, MainActivity.class);//开始新的activity同时移除之前所有的activityintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);startActivity(intent); 持久化存储设置经过上述步骤,我们已经可以看到应用显示的资源发生了改变,但是当应用被杀掉重启后,之前所有的设置都已经失效,应用的语言地区又变成了系统默认的,这是因为我们对应用所做的变更只是保存在内存中,当应用被杀掉,在内存中的数据也随之被释放,再次启动应用的时候,应用读取的是系统的 Configuration ,语言地区也随之变成系统默认的。 当应用需要保存用户更改的操作,就需要对用户的选择进行持久化,并在应用重启的时候,从配置中读取并应用该配置。 123456789public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); //一般在 Application 的 onCreate() 方法中更新 Configuration LanguageUtil.changeAppLanguage(this, Locale.SIMPLIFIED_CHINESE, true); }} LanguageUtil.java 123456789101112131415161718192021222324252627282930/*** 更改应用语言** @param context* @param locale 语言地区* @param persistence 是否持久化*/public static void changeAppLanguage(Context context, Locale locale, boolean persistence) { Resources resources = context.getResources(); DisplayMetrics metrics = resources.getDisplayMetrics(); Configuration configuration = resources.getConfiguration(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { configuration.setLocale(locale); } else { configuration.locale = locale; } resources.updateConfiguration(configuration, metrics); if (persistence) { saveLanguageSetting(context, locale); }}private static void saveLanguageSetting(Context context, Locale locale) { String name = context.getPackageName() + \"_\" + LANGUAGE; SharedPreferences preferences = context.getSharedPreferences(name, Context.MODE_PRIVATE); preferences.edit().putString(LANGUAGE, locale.getLanguage()).apply(); preferences.edit().putString(COUNTRY, locale.getCountry()).apply();} 这样,Android 应用内多语言切换基本完工。接下来,分享一下我在多语言切换过程中遇到的坑。 多语言切换中遇到的坑 以静态变量的方式,在 Application 初始化时初始化网络请求错误提示语,然后再系统中切换语言后,网络请求错误提示语未更新。 解决办法:使用时直接通过 getString() 方法获取 App 多语言切换设置持久化后,在应用启动时, Application 的 onCreate() 中也进行了多语言切换。然后去系统设置中切换语言,App 也会随之跟随系统语言。 原因:在我们改变系统的语言时,应用的 Configuration 也随之跟随系统改变,而不是我们启动应用时的设置了 解决办法:监听 Activity 的生命周期,在 Activty 的 onCreate() 中判断应用当前的语言设置是否与用户设置值相同,否则强制更新应用语言设置。因为,当系统切换语言选项的时候,系统会重启 Activity,就如前文所说,我们需要重启 Activity 才能实现资源的重新加载一样。这里也有两种方案: 创建一个基类 BaseActivity ,在其 onCreate() 方法中做处理 使用 ActivityLifecycleCallbacks ,在其回调 onActivityCreated() 中做处理 对比一下,上述两种方案,第一种只能针对继承自 BaseActivity 的才有效,第二种则是监听所以 Activity 的生命周期。所以相对而言,第二种方案更好点。 12345678910/*** 判断是否与设定的语言相同.** @param context* @return*/public static boolean isSameWithSetting(Context context) { Locale current = context.getResources().getConfiguration().locale; return current.equals(getAppLocale(context));} 12345678910111213141516171819202122public class App extends Application { @Override public void onCreate() { super.onCreate(); LanguageUtil.init(this); //注册Activity生命周期监听回调 registerActivityLifecycleCallbacks(callbacks); } ActivityLifecycleCallbacks callbacks = new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { //强制修改应用语言 if (!LanguageUtil.isSameWithSetting(activity)) { LanguageUtil.changeAppLanguage(activity, LanguageUtil.getAppLocale(activity)); } } //Activity 其它生命周期的回调 };} 对于在 AndroidManifest.xml 中配置 launchMode 为 singleInstance 的 Activity,使用 1234Intent intent = new Intent(this, MainActivity.class);//开始新的activity同时移除之前所有的activityintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);startActivity(intent); 资源文件不更新。 原因:launchMode 为 singleInstance 的 Activity 与当前应用时不在同一个 Task 栈 解决方法:将 launchMode 改为其它模式或者杀掉应用重新启动 资源文件夹为 values-zh-rCN时,将应用 Locale 设置为 Locale.CHINESE 时,找不到对应的资源文件。 原因:values-zh-rCN 对应的 Locale 为 Locale.SIMPLIFIED_CHINESE 解决办法:将 Locale 设置为 Locale.SIMPLIFIED_CHINESE 或者将资源文件改为 values-zh 这是踩得最惨的一个坑,浪费了大量时间,所以才会有开头 Android 应用资源国际化 那么一小节插曲。 这是在 华为 Nexus 6P 上测出来的一个问题,6P 上多语言的选项有点诡异:简体中文(中文)、简体中文(香港)、繁体中文(香港)、简体中文(澳门)、繁体中文(台湾)、简体中文(新加坡),其中有几个简体中文的选项在以前的 Android 版本中是没有的,而且简体中文(香港)和繁体中文(香港)的语言地区都是”zh-HK”,后面在调试中发现,Locale 对象中发现了 script 属性,简体中文对应Hans,繁体中文对应Hant,其余语言默认为空 123456789101112131415161718192021222324252627282930313233343536/** * 是否用中文 * * @return true是中文 */ public static boolean isChinese() { String language = Locale.getDefault().getLanguage(); String country = Locale.getDefault().getCountry(); boolean isZh = Locale.SIMPLIFIED_CHINESE.getLanguage().equals(language); //API 21以上,在Nexus出现繁体中文(香港)和简体中文(香港) //通过Locale.getScript()区分 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //虽然此接口是官方的,但是并不是每台手机都实现,有可能返回空 String script = Locale.getDefault().getScript(); //Hans表示简体中文,Hant表示繁体中文 if (TextUtils.isEmpty(script)) { return isZh && isUseSimpleChinese(country); } else { return isZh && \"Hans\".equals(script); } } else { return isZh && isUseSimpleChinese(country); } } /** * 当前地区是否使用简体中文. * 目前已知包括CN、SG(新加坡) * * @param country * @return */ private static boolean isUseSimpleChinese(String country) { return Locale.SIMPLIFIED_CHINESE.getCountry().equals(country) || \"SG\".equals(country); } 如果语言地区为新加坡简体中文(zh-rSG)且你对该语言也做了适配,但是系统仍然会适配到简体中文(zh-rCN),解决的办法是:在 strings.xml 中定义一个 locale 字符串,据此来判断正确的语言。 代码已上传 Git,欢迎大家 Star 和 Fork。","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"多语言","slug":"多语言","permalink":"http://www.littlejie.com/tags/多语言/"},{"name":"国际化","slug":"国际化","permalink":"http://www.littlejie.com/tags/国际化/"}]},{"title":"基于Material Design的Gank IO客户端","slug":"基于Material-Design的Gank-IO客户端","date":"2017-03-13T04:58:03.000Z","updated":"2017-03-21T15:20:40.000Z","comments":true,"path":"2017/03/13/基于Material-Design的Gank-IO客户端/","link":"","permalink":"http://www.littlejie.com/2017/03/13/基于Material-Design的Gank-IO客户端/","excerpt":"","text":"基于 Material Design 的 Gank IO 客户端 版权声明:本文为博主原创文章,未经博主允许不得转载。微博:厉圣杰源码:GankIO文中如有纰漏,欢迎大家留言指出。 据说去年 RxJava + Retrofit 很火,但是自己一直没有接触,周末闲来无事,使用 MVP + RxJava + Retrofit + ButterKnife 写了一个简单的基于 GankIO 提供的 API 的 Material Design 的客户端。不过,不得不自己吐槽一下,每日精选自己设计的真的很丑,特别是显示日期的 Item。 先上个效果图: gank 项目架构简单说下项目架构,项目分为 app 、 core 和 library 三个 module。其中,library 是第三方的下拉刷新库,可以先忽略。 w300 core 是与业务逻辑无关的库,包含一些基类,如:BaseActivity、BaseFragment等,这里封装好了 ButterKnife ,不得不说,使用注解真的可以极大的加快开发效率。manager 包下的 ActivityManager 用于管理应用的 Activity 栈,但此类与系统的类重名,有待商榷BaseImage 是临时封装 ImageView 和 Glide 的,封装的很差,待改进。utils 包下是一些常见的工具类。 app 是基于 GankIO 的客户端,涉及到具体的业务逻辑,其项目结构大致如下: 屏幕快照 其中,modules 包下是客户端的界面,由于界面不多,所以没有细分。 contract 是 MVP 模式的契约类,具体可以参考 Google 开源的 MVP 项目:android-architecture。就 MVP 模式来说,Google 的这种方式不得不说的确很不错,在契约类中包含了 Model 、View 、 Presenter 三层的契约,查看起整体逻辑会方便很多,在一定程度上也能减少使用 MVP 模式导致的类爆炸问题。 presenter 包下则是 P 层的实现,只涉及业务、数据,不涉及 UI ,UI 的操作全部封装在 V 层。 model 包下则是 Model 层的实现,该层只负责数据请求,如果数据不是很复杂,完全可以将其与 P 层合并。个人觉得,模式这种东西提供的是一种思路,要善于变通 如果对 MVP 模式不是很理解,可以切换到 master 分支,检出 tag 为 v1.0.0 的提交记录,该项目最初版本为非 MVP 模式实现。 项目中碰到的问题小记SwipeRefreshLayout 与 WebView 的滑动冲突在 v1.0.0 版本中,使用 WebView 加载页面时,存在将网页上滑到顶部时,会与 SwipeRefreshLayout 的下拉事件存在冲突,这里采用了一种侵入式的解决方法,代码如下: 1234567891011mWebView.setOnScrollChangedListener(new ScrollWebView.OnScrollChangedListener() { @Override public void onScrollChanged(int l, int t) { Log.d(TAG, \"l = \" + l + \";t = \" + t); if (mWebView.getScrollY() == 0) { mSwipeRefreshLayout.setEnabled(true); } else { mSwipeRefreshLayout.setEnabled(false); } } }); WebView 页面自适应细心的朋友可能已经发现,图片加载跟网页加载一样,都是使用 WebView 实现,但是 WebView 加载图片时并不会自适应,所以这里需要对 WebView 进行设置。这里给出项目中对 WebView 进行的所以设置 12345678910111213141516private void initWebViewSettings() { WebSettings settings = mWebView.getSettings(); // 开启 JS 支持 settings.setJavaScriptEnabled(true); // 支持屏幕缩放 settings.setSupportZoom(true); // 设置出现缩放工具 settings.setBuiltInZoomControls(true); // 不显示webview缩放按钮 settings.setDisplayZoomControls(false); // 扩大比例的缩放 settings.setUseWideViewPort(true); //自适应屏幕 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); settings.setLoadWithOverviewMode(true);} ViewPager + TabLayout + Fragment的状态保存相信这个问题许多人使用 ViewPager 时都碰到过,那就是有多个 Pager 的时候,比方从第 1 个页面切换到第 5 个页面,然后再切回去,会发现 Pager 又会重新初始化一次。这就会造成一种不是很好的用户体验。其实处理这个问题很简单,在 onSaveInstanceState 保存你需要保存的数据,再次初始化时判断 savedInstanceState 是否为空即可,代码如下: 1234567891011121314151617@Overridepublic void onSaveInstanceState(Bundle outState) { mPresenter.onSaveInstanceState(outState); //计算出当前RecyclerView垂直方向的偏移量 outState.putInt(\"offset\", mRecyclerView.computeVerticalScrollOffset()); super.onSaveInstanceState(outState);}@Overrideprotected void initData(Bundle savedInstanceState) { mPresenter = new CategoryPresenter(this); if (savedInstanceState != null) { //恢复之前的RecyclerView的偏移量 //调用RecyclerView.offsetChildrenVertical可滚动指定偏移量 mOffset = savedInstanceState.getInt(\"offset\"); }} 欢迎 Star 和 Fork:GankIO","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"Material Design","slug":"Material-Design","permalink":"http://www.littlejie.com/tags/Material-Design/"},{"name":"RxJava","slug":"RxJava","permalink":"http://www.littlejie.com/tags/RxJava/"},{"name":"Retrofit","slug":"Retrofit","permalink":"http://www.littlejie.com/tags/Retrofit/"},{"name":"ButterKnife","slug":"ButterKnife","permalink":"http://www.littlejie.com/tags/ButterKnife/"},{"name":"MVP","slug":"MVP","permalink":"http://www.littlejie.com/tags/MVP/"}]},{"title":"【译】如何在 Android 5.0 上获取 SD卡 的访问权限","slug":"【译】如何在-Android-5-0-上获取-SD卡-的访问权限","date":"2017-03-11T00:53:21.000Z","updated":"2017-03-11T00:53:58.000Z","comments":true,"path":"2017/03/11/【译】如何在-Android-5-0-上获取-SD卡-的访问权限/","link":"","permalink":"http://www.littlejie.com/2017/03/11/【译】如何在-Android-5-0-上获取-SD卡-的访问权限/","excerpt":"","text":"因为最近项目需要,涉及到 SD卡 的读写操作,然而申请 123<!-- 读写权限 --><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 权限只能对 SD卡 进行读操作,而没有写权限,也就是说,Android 在某个版本中对 SD卡 的读写权限进行了限制。后在 StackoverFlow 上找到一篇相关问答,解了心中疑惑。在此,对该问答进行翻译并附上相关 Demo,已做备忘。 原文地址:How to use the new SD card access API presented for Android 5.0 (Lollipop)? 问背景在 Android 4.4(KitKat) 中,Google 对 SD卡 的访问已经做了严格的限制。在 Android 5.0(Lollipop) 中,开发者可以使用 新API 要求用户对某个指定的文件夹进行访问授权,详见:Android 4.4 Samsung Galaxy s4 external sd card is now read only, Remove or option to edit non app files.(译者注:开头挺搞笑的,都是开发者吐槽 Google 对 SD卡 做了限制) 问题上述文章中有两个链接: https://android.googlesource.com/platform/development/+/android-5.0.0_r2/samples/Vault/src/com/example/android/vault/VaultProvider.java#258 此链接中代码看起来更像是内部示例(可能会在以后的 API Demo 中出现),但是真的很难理解这部分代码的意图。 http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html 这是 新API 的官方文档,但是并没有多少如何使用的细节。(译者注:这份文档其实还是有很多内容的,后面会具体细讲。至于为什么会有这种差别,可能作者提问时,该文档尚未完善吧~) If you really do need full access to an entire subtree of documents, start by launching ACTION_OPEN_DOCUMENT_TREE to let the user pick a directory. Then pass the resulting getData() into fromTreeUri(Context, Uri) to start working with the user selected tree.As you navigate the tree of DocumentFile instances, you can always use getUri() to obtain the Uri representing the underlying document for that object, for use with openInputStream(Uri), etc.To simplify your code on devices running KITKAT or earlier, you can use fromFile(File) which emulates the behavior of a DocumentsProvider. 对于新 API 我有以下问题: 新 API 的正确使用方式? 根据文档,系统会记录 app 被授予访问权限的文件和文件夹。那么,我该如何检测我对某个文件或者文件夹是否有访问权限?是否有方法获取可访问的文件或文件夹列表呢? 在 Android 4.4 上如何处理这个问题?Support Library 是否包含相应的解决方案 系统中是否有对应的界面可以查看哪些 App 可以访问哪些文件。 在多用户的设备上授权该如何处理? 是否有其它关于新 API 的文档? 对 SD卡 的授权是否可以被取消?如果是,那对应的意图是什么? 对于文件夹授权是否是递归授权?指代文件夹内还嵌套有文件夹。 SD 授权是否支持多选?或该应用程序需要专门告诉意图要允许的文件/文件夹吗? 模拟器可以测试新 API 嘛?我的意思是,模拟器具有 SD 卡的分区,但它的作用是主要的外部存储,简单使用 android.permission.WRITE_EXTERNAL_STORAGE 是否足够? 当用户替换 SD卡 是会发生什么? 来自 Jeff Sharkey 的回答这些问题问的都非常好,让我们来深入挖掘下 如何使用新的 API在 Kitkat 中有一份非常好的关于与 Storage Access Framework 交互的文档:Document provider. 新 API 的使用与之很相似。通过发送以下 Intent ,让用户在文档树(Directory Tree)中选择授权目录。 12Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);startActivityForResult(intent, 42); 在 onActivityResult() 中,将用户选择的 Uri 传递给辅助类 DocumentFile。以下代码片段展示了如何列出选中目录下的文件和如何创建一个文件。 1234567891011121314151617public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (resultCode == RESULT_OK) { Uri treeUri = resultData.getData(); DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); // List all existing files inside picked directory for (DocumentFile file : pickedDir.listFiles()) { Log.d(TAG, \"Found file \" + file.getName() + \" with size \" + file.length()); } // Create a new file and write into it DocumentFile newFile = pickedDir.createFile(\"text/plain\", \"My Novel\"); OutputStream out = getContentResolver().openOutputStream(newFile.getUri()); out.write(\"A long time ago...\".getBytes()); out.close(); }} 由 DocumentFile.getUri() 返回的 Uri 使用非常灵活,可以与不同的 API 搭配使用。例如,你可以通过 Inetnt.setData() 将 Uri 分享出去,不过得将 Intent 的 flag 设置为 Intent.FLAG_GRANT_READ_URI_PERMISSION。 如何检测是否对某个文件/文件夹有访问权限默认情况下,通过 Storage Access Framework 获取的 Uri 授权并不是永久的,设备重启后就会消失。不过,系统提供了相关的接口让授权永久化,如果需要的话可自行设置。在上述代码,你可以如此设置: 123getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 之后,你就可以通过 ContentResolver.getPersistedUriPermissions() 来获取 APP 已经被永久授予权限的 Uri。如果不在需要某个 Uri 的权限,可以通过 ContentResolver.releasePersistableUriPermission() 来释放。 能否在 Kitkat 中使用不能,因为该 API 是在 Lollipop 中添加的 能否知道有哪些 APP 拥有该权限能。但是目前是没有 UI 界面的,你得通过 adb shell dumpsys activity providers 来获取。 在多用户的设备上授权该如何处理?与多用户系统的其它功能一样,Uri 授权也是用户独立的。因此,同一个 APP 的 Uri 授权对每个用户是透明的。 授权是否可以被取消?DocumentProvider 支持随时撤销授权。取消授权最常见的方法就是通过上面提到 ContentResolver.releasePersistableUriPermission() 。 当清除应用的数据时,应用相关的授权也都会被清除。 对于文件夹授权是否是递归授权的?是的,通过 ACTION_OPEN_DOCUMENT_TREE 的 Intent 获取到授权之后,对该 Uri 下的所有文件都有读写权限。 授权是否支持多选操作?从 Android 4.4(Kitkat) 起就支持了。您可以在启动 ACTION_OPEN_DOCUMENT Intent 时通过设置 EXTRA_ALLOW_MULTIPLE 来实现。您可以通过使用 Intent.setType() 或者 EXTRA_MIME_TYPES 来设置可选文件类型。具体参考:ACTION_OPEN_DOCUMENT 是否可以在模拟器上尝试新 API可以的。如果你的 APP 只使用 Storage Access Framework 访问共享存储,你甚至不再需要 READ/WRITE_EXTERNAL_STORAGE 权限或者使用 android:maxSdkVersion 在较旧的版本上使用它们。 当用户替换 SD卡 时会发生什么?当涉及物理介质时,底层媒体的 UUID(例如FAT序列号)总是被烧录到返回的 Uri 中。The system uses this to connect you to the media that the user originally selected, even if the user swaps the media around between multiple slots.(翻译不了) 如果用户替换了新的 SD卡,您需要重新申请 SD卡 授权。 由于系统会记住基于每个UUID的授权,如果用户以后重新插入,您将继续先前授予对原始卡的访问权限。 参考:磁盘序列号","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"SD卡","slug":"SD卡","permalink":"http://www.littlejie.com/tags/SD卡/"},{"name":"存储","slug":"存储","permalink":"http://www.littlejie.com/tags/存储/"}]},{"title":"Android 自定义View之圆形进度条总结","slug":"Android-自定义View之圆形进度条总结","date":"2017-03-02T14:39:18.000Z","updated":"2017-03-02T14:40:05.000Z","comments":true,"path":"2017/03/02/Android-自定义View之圆形进度条总结/","link":"","permalink":"http://www.littlejie.com/2017/03/02/Android-自定义View之圆形进度条总结/","excerpt":"","text":"版权声明:本文为博主原创文章,未经博主允许不得转载。微博:厉圣杰微信公众号:牙锅子源码:CircleProgress文中如有纰漏,欢迎大家留言指出。 最近撸了一个圆形进度条的开源项目,算是第一次完完整整的使用自定义 View 。在此对项目开发思路做个小结,欢迎大家 Star 和 Fork 该项目总共实现了三种圆形进度条效果 CircleProgress:圆形进度条,可以实现仿 QQ 健康计步器的效果,支持配置进度条背景色、宽度、起始角度,支持进度条渐变 DialProgress:类似 CircleProgress,但是支持刻度 WaveProgress:实现了水波纹效果的圆形进度条,不支持渐变和起始角度配置,如需此功能可参考 CircleProgress 自行实现。 先上效果图,有图才好说。CircleProgress 效果图 a DialProgress 和 WaveProgress 效果图 b 恩,那么接下来,就来讲讲怎么实现以上自定义进度条的效果。 圆形进度条圆形进度条是第一个实现的进度条效果,用了我大半天的时间,实现起来并不复杂。 其思路主要可以分为以下几步: View 的测量 计算绘制 View 所需参数 圆弧的绘制及渐变的实现 文字的绘制 动画效果的实现 首先,我们要测量出所绘制 View 的大小,即重写 onMeasure() 方法,代码如下: 123456@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize), MiscUtil.measure(heightMeasureSpec, mDefaultSize));} 由于其他两个进度条类都需要实现 View 的测量,这里对代码进行了封装: 12345678910111213141516171819/*** 测量 View** @param measureSpec* @param defaultSize View 的默认大小* @return 测量出来的 View 大小*/public static int measure(int measureSpec, int defaultSize) { int result = defaultSize; int specMode = View.MeasureSpec.getMode(measureSpec); int specSize = View.MeasureSpec.getSize(measureSpec); if (specMode == View.MeasureSpec.EXACTLY) { result = specSize; } else if (specMode == View.MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } return result;} 关于 View 测量可以看下这篇博客 Android 自定义View 中的onMeasure的用法 接下来,在 onSizeChanged() 中计算绘制圆及文字所需的参数,考虑到屏幕旋转的情况,故未直接在 onMeasure() 方法中直接计算。这里以下面草图来讲解绘制计算过程中的注意事项,图丑,勿怪~ WechatIMG2 图中,外面蓝色矩形为 View,里面黑色矩形为圆的外接矩形,蓝色矩形和黑色矩形中间空白的地方为 View 的内边距(padding)。两个蓝色的圆其实是一个圆,代表圆的粗细,这是因为 Android 在绘制圆或者圆弧的时候是圆的边宽的中心与外接矩形相交,所以在计算的时候要考虑到内边距(padding) 和 圆与外接矩形的相交。 默认不考虑圆弧的宽度,绘制出来的效果如下: device-2017-03-02-071101 12345678910111213141516171819202122232425262728293031@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); Log.d(TAG, \"onSizeChanged: w = \" + w + \"; h = \" + h + \"; oldw = \" + oldw + \"; oldh = \" + oldh); //求圆弧和背景圆弧的最大宽度 float maxArcWidth = Math.max(mArcWidth, mBgArcWidth); //求最小值作为实际值 int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth, h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth); //减去圆弧的宽度,否则会造成部分圆弧绘制在外围 mRadius = minSize / 2; //获取圆的相关参数 mCenterPoint.x = w / 2; mCenterPoint.y = h / 2; //绘制圆弧的边界 mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2; mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2; mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2; mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2; //计算文字绘制时的 baseline //由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算 //若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算 mValueOffset = mCenterPoint.y - (mValuePaint.descent() + mValuePaint.ascent()) / 2; mHintOffset = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2; mUnitOffset = mCenterPoint.y * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2; updateArcPaint(); Log.d(TAG, \"onSizeChanged: 控件大小 = \" + \"(\" + w + \", \" + h + \")\" + \"圆心坐标 = \" + mCenterPoint.toString() + \";圆半径 = \" + mRadius + \";圆的外接矩形 = \" + mRectF.toString());} 关于 Android 中文字绘制可以参考以下两篇文章: Android 自定义View学习(三)——Paint 绘制文字属性 measureText() vs .getTextBounds() 以上,已经基本完成了 View 绘制所需全部参数的计算。接下来就是绘制圆弧及文字了。 绘制圆弧需要用到 Canvas 的 123456// oval 为 RectF 类型,即圆弧显示区域// startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数。3点钟方向为0度,顺时针递增// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360// useCenter:如果为 true 时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形// 绘制圆弧的画笔drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint); 为了方便计算,绘制圆弧的时候使用了 Canvas 的 rotate() 方法,对坐标系进行了旋转 1234567891011private void drawArc(Canvas canvas) { // 绘制背景圆弧 // 从进度圆弧结束的地方开始重新绘制,优化性能 canvas.save(); float currentAngle = mSweepAngle * mPercent; canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y); // +2 是因为绘制的时候出现了圆弧起点有尾巴的问题 canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint); canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint); canvas.restore();} 恩,圆环已经绘制完成,那么接下来就是实现圆环的渐变,这里使用 SweepGradient 类。SweepGradient 可以实现从中心放射性渐变的效果,如下图: 1344993412_1866 SweepGradient 类有两个构造方法, 123456789101112131415/** * @param cx 渲染中心点x坐标 * @param cy 渲染中心点y坐标 * @param colors 围绕中心渲染的颜色数组,至少要有两种颜色值 * @param positions 相对位置的颜色数组,可为null, 若为null,可为null,颜色沿渐变线均匀分布。一般不需要设置该参数 /public SweepGradient(float cx, float cy, int[] colors, float[] positions)/** * @param cx 渲染中心点x坐标 * @param cy 渲染中心点y坐标 * @param color0 起始渲染颜色 * @param color1 结束渲染颜色 /public SweepGradient(float cx, float cy, int color0, int color1) 这里我们选择第一个构造方法。由于设置渐变需要每次都创建一个新的 SweepGradient 对象,所以最好不要放到 onDraw 方法中去更新,最好在初始化的时候就设置好,避免频繁创建导致内存抖动。 123456private void updateArcPaint() { // 设置渐变 int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED}; mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null); mArcPaint.setShader(mSweepGradient);} 这里还有一个值得注意的地方,草图如下 WechatIMG3 假设,渐变颜色如下: 1int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE}; 因为 SweepGradient 渐变是 360 度的,所以如果你绘制的圆弧只有 270度,则蓝色部分(图中黑色阴影部分)的渐变就会不可见。 接下来,就是文字的绘制了。文字绘制在上述提到的文章中已经进行了详细的讲解,这里就不再赘述。代码如下: 1234567891011private void drawText(Canvas canvas) { canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint); if (mHint != null) { canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint); } if (mUnit != null) { canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint); }} 最后,我们来实现进度条的动画效果。这里我们使用 Android 的属性动画来实现进度更新。 123456789101112131415161718private void startAnimator(float start, float end, long animTime) { mAnimator = ValueAnimator.ofFloat(start, end); mAnimator.setDuration(animTime); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPercent = (float) animation.getAnimatedValue(); mValue = mPercent * mMaxValue; if (BuildConfig.DEBUG) { Log.d(TAG, \"onAnimationUpdate: percent = \" + mPercent + \";currentAngle = \" + (mSweepAngle * mPercent) + \";value = \" + mValue); } invalidate(); } }); mAnimator.start();} 这里有两个注意点: 不要在 ValueAnimator.AnimatorUpdateListener 中输出 Log,特别是动画调用频繁的情况下,因为输出 Log 频繁会生成大量 String 对象造成内存抖动,当然也可以使用 StringBuilder 来优化。 关于 invalidate() 和 postInvalidate() 两者最本质的前者只能在 UI 线程中使用,而后者可以在非 UI 线程中使用,其实 postInvalidate() 内部也是使用 Handler 实现的。 关于 Android 属性动画可以参考: Android 属性动画(Property Animation) 完全解析 (上) Android 属性动画(Property Animation) 完全解析 (下) 补充:同一个属性如何支持颜色和颜色数组考虑到圆弧设置单色和渐变的区别,即单色只需要提供一种色值,而渐变至少需要提供两种色值。可以有以下几种解决方案: 定义两个属性,渐变的优先级高于单色的。 定义一个 format 为 string 属性,以 #FFFFFF|#000000 形式提供色值 定义一个 format 为 color|reference 的属性,其中 reference 属性指代渐变色的数组。 这里选用第三种方案,实现如下: 123456789101112131415161718192021222324252627282930<!-- 圆形进度条 --><declare-styleable name=\"CircleProgressBar\"> <!-- 圆弧颜色, --> <attr name=\"arcColors\" format=\"color|reference\" /></declare-styleable><!-- colors.xml --><color name=\"green\">#00FF00</color><color name=\"blue\">#EE9A00</color><color name=\"red\">#EE0000</color><!-- 渐变颜色数组 --><integer-array name=\"gradient_arc_color\"> <item>@color/green</item> <item>@color/blue</item> <item>@color/red</item></integer-array><!-- 布局文件中使用 --><!-- 使用渐变 --><com.littlejie.circleprogress.DialProgress android:id=\"@+id/dial_progress_bar\" android:layout_width=\"300dp\" android:layout_height=\"300dp\" app:arcColors=\"@array/gradient_arc_color\" /><!-- 使用单色 --> <com.littlejie.circleprogress.DialProgress android:id=\"@+id/dial_progress_bar\" android:layout_width=\"300dp\" android:layout_height=\"300dp\" app:arcColors=\"@color/green\" /> 代码中读取 xml 中配置: 1234567891011121314151617181920int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0); if (gradientArcColors != 0) { try { int[] gradientColors = getResources().getIntArray(gradientArcColors); if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值 int color = getResources().getColor(gradientArcColors); mGradientColors = new int[2]; mGradientColors[0] = color; mGradientColors[1] = color; } else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色 mGradientColors = new int[2]; mGradientColors[0] = gradientColors[0]; mGradientColors[1] = gradientColors[0]; } else { mGradientColors = gradientColors; } } catch (Resources.NotFoundException e) { throw new Resources.NotFoundException(\"the give resource not found.\"); } } 带刻度进度条前面,详细讲了 CircleProgress 的绘制思路,接下来讲 DialProgress。 实话说,DialProgress 与 CircleProgress 的实现极其相似,因为两者之间其实就差了一个刻度,但考虑到扩展以及类职责的单一,所以将两者分开。 这里主要讲一下刻度的绘制。刻度绘制主要使用 Canvas 类的 save()、rotate()和restore() 方法,当然你也可以使用 translate() 方法对坐标系进行平移,方便计算。 123456789101112131415161718192021222324/** * 用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 */public void save()/** * 旋转一定的角度绘制图像 * @param degrees 旋转角度 * @param x 旋转中心点x轴坐标 * @param y 旋转中心点y轴坐标 */public void rotate(float degrees, float x, float y)/** * 在当前的坐标上平移(x,y)个像素单位 * 若dx <0 ,沿x轴向上平移; dx >0 沿x轴向下平移 * 若dy <0 ,沿y轴向上平移; dy >0 沿y轴向下平移 */public void translate(float dx, float dy)/** * 用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。 */public void restore() 12345678910private void drawDial(Canvas canvas) { int total = (int) (mSweepAngle / mDialIntervalDegree); canvas.save(); canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y); for (int i = 0; i <= total; i++) { canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint); canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y); } canvas.restore();} 关于 Canvas 的画布操作可以参考这篇文章:安卓自定义View进阶-Canvas之画布操作 水波纹效果的进度条水波纹效果的进度条实现需要用到贝塞尔曲线,主要难点在于 绘制区域的计算 和 波浪效果 的实现,其余的逻辑跟上述两种进度条相似。 这里使用了 Path 类,该类在 Android 2D 绘图中是非常重要的,Path 不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。也可以对多个路径进行布尔操作,类似设置 Paint 的 setXfermode() ,具体使用可以参考这篇博客:安卓自定义View进阶-Path基本操作。这里就不再赘述,有机会自己也会对 Android 自定义 View 的知识进行总结,不过,感觉应该了了无期。 继续上示意图,请叫我灵魂画手~ WechatIMG1 图中黑色的圆为我们要绘制的进度条圆,黑色的曲线为初始状态的的波浪,该波浪使用贝塞尔曲线绘制,其中奇数的点为贝塞尔曲线的起始点,偶数的点为贝塞尔曲线的控制点。例如:1——>2——>3就为一条贝塞尔曲线,1 是起点,2 是控制点,3 是终点。从图中可以看到波浪在园内圆外各一个(1—>5 和 5->9),通过对波浪在 x 轴上做平移,即图中蓝色实线,来实现波浪的动态效果,所以一个波浪的完整动画效果需要有两个波浪来实现。同理,通过控制 y 轴的偏移量,即图中蓝色虚线,可以实现波浪随进度的上涨下降。 贝塞尔曲线上起始点和控制点的计算如下: 123456789101112131415161718192021222324/** * 计算贝塞尔曲线上的起始点和控制点 * @param waveWidth 一个完整波浪的宽度 */private Point[] getPoint(float waveWidth) { Point[] points = new Point[mAllPointCount]; //第1个点特殊处理,即数组的中心 points[mHalfPointCount] = new Point((int) (mCenterPoint.x - mRadius), mCenterPoint.y); //屏幕内的贝塞尔曲线点 for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) { float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum); points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight)); points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y); points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight)); points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y); } //屏幕外的贝塞尔曲线点 for (int i = 0; i < mHalfPointCount; i++) { int reverse = mAllPointCount - i - 1; points[i] = new Point(points[mHalfPointCount].x - points[reverse].x, points[mHalfPointCount].y * 2 - points[reverse].y); } return points;} 以上,我们已经获取到绘制贝塞尔曲线所需的路径点。接下来,我们就需要来计算出绘制区域,即使用 Path 类。 WechatIMG1 紫色区域为贝塞尔曲线需要绘制的整体区域。 WechatIMG1 红色区域为上图紫色区域与圆的交集,也就是波浪要显示的区域 代码如下: 123456789101112131415161718192021222324//该方法必须在 Android 19以上的版本才能使用(Path.op())@TargetApi(Build.VERSION_CODES.KITKAT)private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) { mWaveLimitPath.reset(); mWavePath.reset(); //lockWave 用于判断波浪是否随进度条上涨下降 float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent; //moveTo和lineTo绘制出水波区域矩形 mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height); for (int i = 1; i < mAllPointCount; i += 2) { mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height, points[i + 1].x + waveOffset, points[i + 1].y + height); } mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height); //不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况 mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius); mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius); mWavePath.close(); mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW); //取该圆与波浪路径的交集,形成波浪在圆内的效果 mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT); canvas.drawPath(mWaveLimitPath, paint);} 以上,就实现了水波动态的效果,当然,你也可以通过配置,来设定水波是否随进度上涨下降。为了实现更好的效果,可以设置一个浅色的水波并支持设置水波的走向(R2L 和 L2R),通过设置浅色波浪和深色波浪动画的时间,从而实现长江后浪推前浪的效果,恩,效果很自然的~自己脑补从右至左波浪的实现和贝塞尔点的计算。 对获取坐标点的代码进行优化: 1234567891011121314151617181920212223242526/*** 从左往右或者从右往左获取贝塞尔点** @return*/private Point[] getPoint(boolean isR2L, float waveWidth) { Point[] points = new Point[mAllPointCount]; //第1个点特殊处理,即数组的中点 points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -mRadius)), mCenterPoint.y); //屏幕内的贝塞尔曲线点 for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) { float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum); points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight)); points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y); points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight)); points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y); } //屏幕外的贝塞尔曲线点 for (int i = 0; i < mHalfPointCount; i++) { int reverse = mAllPointCount - i - 1; points[i] = new Point((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x, points[mHalfPointCount].y * 2 - points[reverse].y); } //对从右向左的贝塞尔点数组反序,方便后续处理 return isR2L ? MiscUtil.reverse(points) : points;} 至此,自定义圆形进度条相关的思路已全部讲述完成。代码已全部上传至 Git ,欢迎大家 Star 和 Fork,传送门:CircleProgress。 如有不清楚或者错误的地方,欢迎指出~","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"自定义View","slug":"自定义View","permalink":"http://www.littlejie.com/tags/自定义View/"},{"name":"圆形进度条","slug":"圆形进度条","permalink":"http://www.littlejie.com/tags/圆形进度条/"},{"name":"水波纹进度条","slug":"水波纹进度条","permalink":"http://www.littlejie.com/tags/水波纹进度条/"}]},{"title":"自定义仿 QQ 健康计步器进度条","slug":"自定义仿-QQ-健康计步器进度条","date":"2017-02-22T10:16:50.000Z","updated":"2017-02-22T10:18:05.000Z","comments":true,"path":"2017/02/22/自定义仿-QQ-健康计步器进度条/","link":"","permalink":"http://www.littlejie.com/2017/02/22/自定义仿-QQ-健康计步器进度条/","excerpt":"","text":"自定义仿 QQ 健康计步器进度条 版权声明:本文为博主原创文章,未经博主允许不得转载。微博:厉圣杰源码:CircleProgress文中如有纰漏,欢迎大家留言指出。 闲着没事,趁上班时间偷偷撸了一个圆形进度条,可以实现仿 QQ 健康计步器的圆形进度条,虽然网上这类控件很多,但毕竟是别人写的代码,总没自己写的用起来爽,所以还是选择再造一次轮子。该控件基本满足日常需求,但不支持设置圆弧半径,半径由 View 自行计算得出。 效果首先上控件的效果图,没图说个屁啊,是不? cap 实现主要实现逻辑如下: 文字定位及绘制 圆弧的绘制及角度的计算 圆弧颜色渐变 动画效果的实现 具体逻辑请见代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370public class CircleProgress extends View { private static final String TAG = CircleProgress.class.getSimpleName(); private Context mContext; //默认大小 private int mDefaultSize; //是否开启抗锯齿 private boolean antiAlias; //绘制标题 private TextPaint mHintPaint; private CharSequence mHint; private int mHintColor; private float mHintSize; //绘制单位 private TextPaint mUnitPaint; private CharSequence mUnit; private int mUnitColor; private float mUnitSize; //绘制数值 private TextPaint mValuePaint; private float mValue; private float mMaxValue; private int mPrecision; private String mPrecisionFormat; private int mValueColor; private float mValueSize; //绘制圆弧 private Paint mArcPaint; private int mArcColor1, mArcColor2, mArcColor3; private float mArcWidth; private float mStartAngle, mSweepAngle; private RectF mRectF; //渐变 private Matrix mRotateMatrix; //渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色 private SweepGradient mSweepGradient; private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED}; //当前进度,[0.0f,1.0f] private float mPercent; //动画时间 private long mAnimTime; //属性动画 private ValueAnimator mAnimator; //绘制背景圆弧 private Paint mBgArcPaint; private int mBgArcColor; private float mBgArcWidth; //圆心坐标,半径 private float mFloatX, mFloatY, mRadius; //在屏幕上的坐标 private int[] mLocationOnScreen = new int[2]; public CircleProgress(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } private void init(Context context, AttributeSet attrs) { mContext = context; mDefaultSize = MiscUtil.dipToPx(mContext, 150); mAnimator = new ValueAnimator(); getLocationOnScreen(mLocationOnScreen); mRectF = new RectF(); initAttrs(attrs); initPaint(); setValue(mValue); } private void initAttrs(AttributeSet attrs) { TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, false); mHint = typedArray.getString(R.styleable.CircleProgressBar_hint); mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK); mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize, 15); mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, 0); mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue, 0); //内容数值精度格式 mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0); mPrecisionFormat = getPrecisionFormat(mPrecision); mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK); mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, 60); mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit); mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK); mUnitSize = typedArray.getDimension(R.styleable.CircleProgressBar_unitSize, 15); // 设置渐变色 mArcColor1 = typedArray.getColor(R.styleable.CircleProgressBar_arcColor1, Color.GREEN); mArcColor2 = typedArray.getColor(R.styleable.CircleProgressBar_arcColor2, Color.YELLOW); mArcColor3 = typedArray.getColor(R.styleable.CircleProgressBar_arcColor3, Color.RED); mGradientColors = new int[]{mArcColor1, mArcColor2, mArcColor3}; mArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, 15); mStartAngle = typedArray.getFloat(R.styleable.CircleProgressBar_startAngle, 270); mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, 360); mBgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.WHITE); mBgArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_bgArcWidth, 15); //mPercent = typedArray.getFloat(R.styleable.CircleProgressBar_percent, 0); mAnimTime = typedArray.getInt(R.styleable.CircleProgressBar_animTime, 1000); typedArray.recycle(); } private String getPrecisionFormat(int precision) { return \"%.\" + precision + \"f\"; } private void initPaint() { mHintPaint = new TextPaint(); // 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。 mHintPaint.setAntiAlias(antiAlias); // 设置绘制文字大小 mHintPaint.setTextSize(mHintSize); // 设置画笔颜色 mHintPaint.setColor(mHintColor); // 从中间向两边绘制,不需要再次计算文字 mHintPaint.setTextAlign(Paint.Align.CENTER); mValuePaint = new TextPaint(); mValuePaint.setAntiAlias(antiAlias); mValuePaint.setTextSize(mValueSize); mValuePaint.setColor(mValueColor); // 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等 mValuePaint.setTypeface(Typeface.DEFAULT_BOLD); mValuePaint.setTextAlign(Paint.Align.CENTER); mUnitPaint = new TextPaint(); mUnitPaint.setAntiAlias(antiAlias); mUnitPaint.setTextSize(mUnitSize); mUnitPaint.setColor(mUnitColor); mUnitPaint.setTextAlign(Paint.Align.CENTER); mArcPaint = new Paint(); mArcPaint.setAntiAlias(antiAlias); mArcPaint.setColor(mArcColor1); // 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE mArcPaint.setStyle(Paint.Style.STROKE); // 设置画笔粗细 mArcPaint.setStrokeWidth(mArcWidth); // 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式 // Cap.ROUND,或方形样式 Cap.SQUARE mArcPaint.setStrokeCap(Paint.Cap.ROUND); mRotateMatrix = new Matrix(); mBgArcPaint = new Paint(); mBgArcPaint.setAntiAlias(antiAlias); mBgArcPaint.setColor(mBgArcColor); mBgArcPaint.setStyle(Paint.Style.STROKE); mBgArcPaint.setStrokeWidth(mBgArcWidth); mBgArcPaint.setStrokeCap(Paint.Cap.ROUND); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //设置默认内边距,防止圆弧与边界重叠 int padding = MiscUtil.dipToPx(mContext, 5); setPadding(padding, padding, padding, padding); //因为是画圆,所以宽高相等 int measuredWidth = MiscUtil.measure(widthMeasureSpec, mDefaultSize); int measuredHeight = MiscUtil.measure(heightMeasureSpec, mDefaultSize); //求最小值作为实际值 int size = Math.min(measuredWidth, measuredHeight); setMeasuredDimension(measuredWidth + getPaddingLeft() + getPaddingRight(), measuredHeight + getPaddingTop() + getPaddingBottom()); //获取圆的相关参数 mFloatX = mLocationOnScreen[0] + size / 2 + getPaddingLeft(); mFloatY = mLocationOnScreen[1] + size / 2 + getPaddingTop(); //求圆弧和背景圆弧的最大宽度 float maxArcWidth = Math.max(mArcWidth, mBgArcWidth); //减去圆弧的宽度,否则会造成部分圆弧绘制在外围,通过clipPadding属性可以解决 mRadius = size / 2 - maxArcWidth; //绘制圆弧的边界 mRectF.left = mLocationOnScreen[0] + getPaddingLeft(); mRectF.top = mLocationOnScreen[1] + getPaddingTop(); mRectF.right = mRectF.left + size; mRectF.bottom = mRectF.top + size; updateArcPaint(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawText(canvas); drawArc(canvas); } /** * 绘制内容文字 * * @param canvas */ private void drawText(Canvas canvas) { // 计算文字宽度,由于Paint已设置为居中绘制,故此处不需要重新计算 // float textWidth = mValuePaint.measureText(mValue.toString()); // float x = mFloatX - textWidth / 2; float y = mFloatY - (mValuePaint.descent() + mValuePaint.ascent()) / 2; canvas.drawText(String.format(mPrecisionFormat, mValue), mFloatX, y, mValuePaint); if (mHint != null) { float hy = mFloatY * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2; canvas.drawText(mHint.toString(), mFloatX, hy, mHintPaint); } if (mUnit != null) { float uy = mFloatY * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2; canvas.drawText(mUnit.toString(), mFloatX, uy, mUnitPaint); } } private void drawArc(Canvas canvas) { // 绘制背景圆弧 // 从进度圆弧结束的地方开始重新绘制,优化性能 float currentAngle = mSweepAngle * mPercent; canvas.drawArc(mRectF, mStartAngle, mSweepAngle, false, mBgArcPaint); // 第一个参数 oval 为 RectF 类型,即圆弧显示区域 // startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数 // 3点钟方向为0度,顺时针递增 // 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360 // useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形 canvas.drawArc(mRectF, mStartAngle, currentAngle, false, mArcPaint); } /** * 更新圆弧画笔 */ private void updateArcPaint() { // 设置渐变 mSweepGradient = new SweepGradient(mFloatX, mFloatY, mGradientColors, null); // 矩阵变化,-5是因为开始颜色可能会与结束颜色重叠 mRotateMatrix.setRotate(mStartAngle - 5, mFloatX, mFloatY); mSweepGradient.setLocalMatrix(mRotateMatrix); mArcPaint.setShader(mSweepGradient); } public boolean isAntiAlias() { return antiAlias; } public CharSequence getHint() { return mHint; } public void setHint(CharSequence hint) { mHint = hint; } public CharSequence getUnit() { return mUnit; } public void setUnit(CharSequence unit) { mUnit = unit; } public float getValue() { return mValue; } /** * 设置当前值 * * @param value */ public void setValue(float value) { if (value > mMaxValue) { value = mMaxValue; } float start = mPercent; float end = value / mMaxValue; startAnimator(start, end, mAnimTime); } private void startAnimator(float start, float end, long animTime) { mAnimator.setDuration(animTime); mAnimator = ValueAnimator.ofFloat(start, end); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPercent = (float) animation.getAnimatedValue(); mValue = mPercent * mMaxValue; if (BuildConfig.DEBUG) { Log.d(TAG, \"onAnimationUpdate: percent = \" + mPercent + \";currentAngle = \" + (mSweepAngle * mPercent) + \";value = \" + mValue); } invalidate(); } }); mAnimator.start(); } /** * 获取最大值 * * @return */ public float getMaxValue() { return mMaxValue; } /** * 设置最大值 * * @param maxValue */ public void setMaxValue(float maxValue) { mMaxValue = maxValue; } /** * 获取精度 * * @return */ public int getPrecision() { return mPrecision; } public void setPrecision(int precision) { mPrecision = precision; mPrecisionFormat = getPrecisionFormat(precision); } public int[] getGradientColors() { return mGradientColors; } /** * 设置渐变 * * @param gradientColors */ public void setGradientColors(int[] gradientColors) { mGradientColors = gradientColors; updateArcPaint(); } public long getAnimTime() { return mAnimTime; } public void setAnimTime(long animTime) { mAnimTime = animTime; } /** * 重置 */ public void reset() { startAnimator(mPercent, 0.0f, 1000L); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); //释放资源 }} XML 配置通过 XML 可以配置出多种效果,支持配置属性如下: 12345678910111213141516171819202122232425262728293031323334353637<declare-styleable name=\"CircleProgressBar\"> <!-- 是否开启抗锯齿 --> <attr name=\"antiAlias\" format=\"boolean\" /> <!-- 绘制内容相应的提示语 --> <attr name=\"hint\" format=\"string|reference\" /> <attr name=\"hintSize\" format=\"dimension\" /> <attr name=\"hintColor\" format=\"color|reference\" /> <!-- 绘制内容的单位 --> <attr name=\"unit\" format=\"string|reference\" /> <attr name=\"unitSize\" format=\"dimension\" /> <attr name=\"unitColor\" format=\"color|reference\" /> <!-- 绘制内容的数值 --> <attr name=\"maxValue\" format=\"float\" /> <attr name=\"value\" format=\"float\" /> <!-- 精度,默认为0 --> <attr name=\"precision\" format=\"integer\" /> <attr name=\"valueSize\" format=\"dimension\" /> <attr name=\"valueColor\" format=\"color|reference\" /> <!-- 圆弧颜色,设置多个可实现渐变 --> <attr name=\"arcColor1\" format=\"color|reference\" /> <attr name=\"arcColor2\" format=\"color|reference\" /> <attr name=\"arcColor3\" format=\"color|reference\" /> <!-- 圆弧宽度 --> <attr name=\"arcWidth\" format=\"dimension\" /> <!-- 圆弧起始角度,3点钟方向为0,顺时针递增,小于0或大于360进行取余 --> <attr name=\"startAngle\" format=\"float\" /> <!-- 圆弧度数 --> <attr name=\"sweepAngle\" format=\"float\" /> <!-- 当前进度百分比 --> <!--<attr name=\"percent\" format=\"float\"/>--> <!-- 设置动画时间 --> <attr name=\"animTime\" format=\"integer\" /> <!-- 背景圆弧颜色 --> <attr name=\"bgArcColor\" format=\"color|reference\" /> <!-- 背景圆弧宽度 --> <attr name=\"bgArcWidth\" format=\"dimension\" /></declare-styleable> 代码配置考虑到圆弧大小、圆弧起始角度等属性一般不可能动态改变,所以通过代码并不能设置 CircleProgress 的全部属性。具体支持方法可以查看 CircleProgress.java 中的 setter 方法。 后记该控件还有许多值得改进的地方,如:对重绘的优化。","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"自定义View","slug":"自定义View","permalink":"http://www.littlejie.com/tags/自定义View/"},{"name":"进度条","slug":"进度条","permalink":"http://www.littlejie.com/tags/进度条/"}]},{"title":"Android 获取浏览器当前分享页面的截屏","slug":"Android-获取浏览器当前分享页面截屏","date":"2017-02-20T10:16:50.000Z","updated":"2017-03-09T09:22:01.000Z","comments":true,"path":"2017/02/20/Android-获取浏览器当前分享页面截屏/","link":"","permalink":"http://www.littlejie.com/2017/02/20/Android-获取浏览器当前分享页面截屏/","excerpt":"","text":"Android 获取浏览器当前分享页面的截屏 版权声明:本文为博主原创文章,未经博主允许不得转载。微博:厉圣杰源码:AndroidDemo/BrowserScreenShotActivity文中如有纰漏,欢迎大家留言指出。 今天在项目中碰见这么一个需求:获取 Chrome 浏览器分享时,页面的截屏。静下来一想,既然是分享,那么肯定得通过 Intent 来传递数据,如果真的能获取到 Chrome 分享页面时的截屏,那么 Intent 的数据中,一定有 .jpg 或者 .png 结尾的数据。说干就干,Demo 写起来。 首先,新建一个 BrowserScreenShotActivity.java,在 AndroidManifest.xml 注册一下 <intent-filter>。 12345678910111213141516171819202122232425262728293031323334353637<?xml version=\"1.0\" encoding=\"utf-8\"?><manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.littlejie.demo\"> <!-- 读写权限 --> <!-- 用于读取浏览器分享时生成的屏幕截图 --> <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/> <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/> <application android:name=\".modules.DemoApplication\" android:allowBackup=\"true\" android:icon=\"@mipmap/ic_launcher\" android:label=\"@string/app_name\" android:supportsRtl=\"true\" android:theme=\"@style/AppTheme\"> <!-- some other thing --> <!-- 注册 Intent,用于接受浏览器分享 --> <activity android:name=\".modules.advance.BrowserScreenShotActivity\" android:launchMode=\"singleTask\"> <intent-filter> <action android:name=\"android.intent.action.SEND\"/> <!-- 发送多个数据 --> <action android:name=\"android.intent.action.SEND_MULTIPLE\"/> <category android:name=\"android.intent.category.DEFAULT\"/> <data android:mimeType=\"*/*\"/> </intent-filter> </activity> </application></manifest> 接下去,在浏览器中随便打开一个页面,分享至 Demo,这里有个问题,就是:屏幕截图数据在 Intent 中对应的 Key 我们并不知道,那怎么办呢?打断点啊! 2017-02-20 通过断点查看 Intent 的数据结构,发现 Intent 中的 mMap 成员变量含有一个 Uri,格式如下:content://com.android.chrome.FileProvider/BlockedFile_33215122012582,一眼看去就猜测这个 Uri 是 Chrome 通过 ContentProvider 供其他程序调用的,虽然与一开始猜测有已 .jpg 和 .png 结尾的数据不太一致,但好歹是有所发现。 恩,现在还有一个问题,那就是 mMap.value[3] 对应的 key 值是多少?在上述断点界面根本就差看不到,但是 Android Studio 是很强大的,只是你没发现而已,既然 mMap 是一个 Map,那么久能通过 keySet() 方法获取 Map 的 key。接下来就是 Android Studio 大展拳脚的时间。 2017-02-20 如上图所示的,在 Debug 界面,点击最后一个图标:Evaluate Expression(快捷键:option + f8)。在弹出的对话框中输入如下内容,回车,你会发现 Map 的 key 都出来了: 屏幕快照 通过与第一幅图对比,发现下标为3的值(share_screenshot_as_stream)为我们需要的 key。 布局比较简单,这里就不贴了,简单截取 BrowserSrceenShotActivity.java 中的代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879/** * 获取浏览器分享网页时的屏幕图片 * <p> * 默认情况下,图片都是以Uri形式保存在Intent的Map中,但不同浏览器的key不一样 * Firefox不支持屏幕截图、百度只能获取浏览器图标 * <p> * 测试时,请在AndroidManifest.xml中将ShareActivity相关配置注释 */@Description(description = \"浏览器截屏获取\")public class BrowserScreenShotActivity extends BaseActivity { //chrome/内置浏览器/UC/QQ //通过getContentResolver().query(),可以得知Chrome有_display_name、_size、_data三个字段来保存截屏图片相关数据 //但是通过Cursor查询时,_data字段为null,所以只能通过输入流来统一处理 private static final String[] KEY_BROWSER_SCREENSHOT = {\"share_screenshot_as_stream\", \"share_full_screen\", \"file\", \"android.intent.extra.STREAM\"}; @BindView(R.id.iv_screen_shot) ImageView mIvScreenShot; @Override protected int getPageLayoutID() { return R.layout.activity_browser_screen_shot; } @Override protected void initData() { if (getIntent() == null) { return; } } @Override protected void initView() { } @Override protected void initViewListener() { } @Override protected void process() { } @Override protected void onResume() { super.onResume(); if (getIntent() == null) { return; } Uri screenShot = null; //循环遍历,获取截屏的Uri for (String key : KEY_BROWSER_SCREENSHOT) { screenShot = getIntent().getExtras().getParcelable(key); if (screenShot != null) { break; } } if (screenShot == null) { ToastUtil.showDefaultToast(\"获取浏览器截屏失败~\"); return; } try { //授权Uri的读取权限 //若不授权,在 Android 6.0 以上测试崩溃 //https://thinkandroid.wordpress.com/2012/08/07/granting-content-provider-uri-permissions/ //第一个参数为需要授权的apk包名 grantUriPermission(\"com.littlejie.demo\", screenShot, Intent.FLAG_GRANT_READ_URI_PERMISSION); //通过ContentProvider获取截屏图片的输入流 InputStream is = getContentResolver().openInputStream(screenShot); mIvScreenShot.setImageBitmap(BitmapFactory.decodeStream(is)); } catch (FileNotFoundException e) { e.printStackTrace(); } }} 运行结果如下: screenshot 对于获取 Chrome 浏览器分享页面的截屏就告一段落,闲着没事,自己又测试了几个浏览器,包括系统内置浏览器、QQ浏览器、UC浏览器、百度浏览器、火狐浏览器,发现每个浏览器的差异很大。 系统浏览器、UC浏览器与 Chrome 相差不大,只是 key 变成了 share_full_screen 和 file QQ浏览器的分享行为与分享文件很相似,其 key 为 android.intent.extra.STREAM(Intent.EXTRA_STREAM)。 百度浏览器是个什么鬼就不知道了,默默的把应用图标给分享过来了 火狐浏览器不支持分享页面截图 恩,就这么多,获取浏览器分享页面截屏主要还是靠浏览器的支持,真的市面上这么多浏览器适配起来还真麻烦。这次主要对 Android Studio 强大的 Debug功能进行了学习。PS:Android Studio真是极其强大的工具,用好它事半功倍,唯一不足的就是太耗性能。 Demo 代码传送门","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"浏览器","slug":"浏览器","permalink":"http://www.littlejie.com/tags/浏览器/"}]},{"title":"Android 开发规范","slug":"Android-开发规范","date":"2017-02-13T12:15:23.000Z","updated":"2017-03-14T06:06:17.000Z","comments":true,"path":"2017/02/13/Android-开发规范/","link":"","permalink":"http://www.littlejie.com/2017/02/13/Android-开发规范/","excerpt":"","text":"个人总结的 Android 开发规范,其中控件缩写及 Android 资源文件命名部分不是很确定,有待商榷。Java 部分编程风格请参考:Google Java 命名规范。 2017-02-13 更新:2017年开春之际,诚意献上重磅大礼:阿里巴巴Java开发手册,首次公开阿里官方Java代码规范标准。这套Java统一规范标准将有助于提高行业编码规范化水平,帮助行业人员提高开发质量和效率、大大降低代码维护成本。点此下载 约定 统一调整 IDE 的编码方式为 UTF-8 01 统一调整 IDE 的 Tab 缩进为 4 个空格 0 花括号不要单独一行,和它前面的代码同一行。而且,花括号与前面的代码之间用一个空格隔开。 1234567891011public void method() { // Good } public void method(){ // Bad} public void method(){ // Bad } 空格的使用:if、else、for、switch、while等逻辑关键字与后面的语句留一个空格隔开。 12345678910111213141516171819202122// Goodif (booleanVariable) { // TODO while booleanVariable is true} else { // TODO else} // Badif(booleanVariable) { // TODO while booleanVariable is true}else { // TODO else}// 运算符两边各用一个空格隔开。int result = a + b; //Good, = 和 + 两边各用一个空格隔开int result=a+b; //Bad,=和+两边没用空格隔开// 方法的每个参数之间用一个空格隔开。public void method(String param1, String param2); // Good,param1后面的逗号与String之间隔了一个空格public void method(param1, param2); // Good,方法调用时,param1后面的逗号与param2之间隔了一个空格public void method(param1,param2); // Bad,没有用一个空格隔开 空行的使用,拒绝拖沓无分割,关联代码段放一块并与后面代码分割 两个方法之间 方法内的两个逻辑段之间 方法内的局部变量和方法的第一条逻辑语句之间 常量和变量之间 方法名和方法内第一条语句不要有空格 Activity.onCreate(),Fragment.onActivityCreated(),作为程序入口,不要写入太多代码,尽量保持只调用 initXXX() 方法,简单明了展示调用过程。如:initData(),initView()。可在 BaseActivity 、 BaseFragment 中实现 init 执行顺序,子类实现,即 模板方法模式。 Application 中只执行应用初始化相关操作,尽量不要涉及业务逻辑。如有,请单独剥离。参考第 6 条 单个方法体不要过长,最好不要超过一屏,竖屏显示器请无视~ 一行声明一个变量,不要一行声明多个变量,这样有利于写注释。 代码任何地方不要拼错单词 代码必须格式化 12Windows:CTRL + ALT + LMac:OPTION + COMMAND + L 文字大小的单位统一用 sp,元素大小的单位统一用 dp;应用中的字符串统一在 /values/strings.xml 中定义;颜色值统一在 /values/colors.xml 中定义;菜单定义统一放在 /menu/****.xml 中;自定义View 属性统一在 /values/attrs.xml 中;自定义drawable 文件统一在 /drawable/****.xml 中;自定义样式统一在 /values/styles.xml。 调用方法保持“临近原则”,被调用的方法,放在调用方法下方 用好 TODO 标记 记录想法,记录功能点,开发过程中可以利用 TODO 记录一下临时想法或为了不打扰思路留下待完善的说明 删除无用 TODO ,开发工具自动生成的 TODO ,或则已经完善的 TODO ,一定要删除。 处理“魔法数字”等看不懂的神秘数字 代码中不要出现数字,特别是一些标识不同类型的数字。 所有意义数字全部抽取到 Constant 公共类中,避免散布在各位类中。 所有有意义的字符串公共常量全部抽取到 Constant 公共类中 命名规范命名除了要遵守以下规范,还得见名知意。 类和接口命名使用大驼峰规则,用名词或名词词组命名,每个单词的首字母大写。以下为几种常用类的命名: Activity 类,命名以 Activity 为后缀,如:LoginActivity Fragment 类,命名以 Fragment 为后缀,如:LoginFragment Service 类,命名以 Service 为后缀,如:DownloadService Adapter 类,命名以 Adapter 为后缀,如:CouponAdapter 工具类,命名以 Util 为后缀,如:EncryptUtil 模型类,命名以 Info 为后缀,如:UserInfo 接口实现类,命名以 Impl 为后缀,如:ApiImpl 方法命名使用小驼峰规则,用动词命名,第一个单词的首字母小写,其他单词的首字母大写。以下为几种常用方法的命名: 初始化方法,命名以 init 开头,例:initView() 按钮点击方法或 Activity 跳转方法,命名以 to 开头,例:toLogin()、toMainActivity() 设置方法,命名以 set 开头,例:setData() 具有返回值的获取方法,命名以 get 开头,例:getData() 通过异步加载数据的方法,命名以 load 开头,例:loadData() 布尔型的判断方法,命名以 is 或 has ,或具有逻辑意义的单词如 equals ,例:isEmpty() 常量命名全部为大写单词,单词之间用下划线分开。常量一般放在 Constant 类 Intent 参数以 PARAM_EXTRA 开头 1234// Intent 参数public final static String PARAM_EXTRA_ID = \"id\";public final static int PAGE_SIZE = 20; 变量命名使用驼峰规则,首字母必须小写,使用名词或名词词组。要求简单易懂,富于描述,不允许出现无意义或错误单词。 普通成员变量命名以 mCamelCase 样式命名,静态变量以 sCamelCase 命名 boolean 类型的成员变量命名可以不遵循第一条,以 lowerCamelCase 样式命名 控件变量命名都已 控件缩写 + 控件作用 来命名,如:登录按钮命名为 mBtnLogin; 参数变量、临时变量都已 lowerCamelCase 样式命名 123456789public class MainActivity extends Activity { private Button mBtnLogin; private boolean isLaunch; private boolean isEmpty(String text) { // Todo... }} 补充:如果你使用 Android Studio 为开发工具,则可以通过如下方式设置变量前缀 屏幕快照 控件缩写常见控件缩写约定如下: TextView: tv EditText: edt Button: btn RadioButton: rb ImageButton: ib ImageView: iv RelativeLayout/LinearLayout/FrameLayout: rl , ll , fl ListView: lv WebView: web CheckBox: cbx 控件 id 命名控件缩写_含义 123456789<!-- 这是标题栏的标题 --><TextView android:id=\"@+id/tv_header_title\" ... /> <-- 这是登录按钮 --><Button android:id=\"@+id/btn_login\" ... /> 布局文件命名 Activity 布局:activity_类名.xml,建议使用 Android Studio 生成(Command + N) Fragment 布局:fragment_类名.xml 控件布局:widget控件名.xml 或 layout控件名.xml Adapter Item 布局:item_适配器名.xml strings.xml 命名类型{范围}功能,范围可选。以下为几种常用的命名: 页面标题,命名格式为:title_页面 按钮文字,命名格式为:btn_按钮事件 标签文字,命名格式为:label_标签文字 选项卡文字,命名格式为:tab_选项卡文字 消息框文字,命名格式为:toast_消息 编辑框的提示文字,命名格式为:hint_提示信息 图片的描述文字,命名格式为:desc_图片文字 对话框的文字,命名格式为:dialog_文字 menu 的 item 文字,命名格式为:menu_文字 colors.xml 命名前缀{控件}{范围}{_后缀},控件、范围、后缀可选,但控件和范围至少要有一个。 背景颜色,添加 bg 前缀 文本颜色,添加 text 前缀 分割线颜色,添加 div 前缀 区分状态时,默认状态的颜色,添加 normal 后缀 区分状态时,按下时的颜色,添加 pressed 后缀 区分状态时,选中时的颜色,添加 selected 后缀 区分状态时,不可用时的颜色,添加 disable 后缀 如:bg_loading_selected drawable的命名 图标类,添加 ic 前缀 背景类,添加 bg 前缀 分隔类,添加 div 前缀 默认类,添加 def 前缀 区分状态时,默认状态,添加 normal 后缀 区分状态时,按下时的状态,添加 pressed 后缀 区分状态时,选中时的状态,添加 selected 后缀 区分状态时,不可用时的状态,添加 disable 后缀 多种状态的,添加 selector 后缀(一般为 ListView 的 selector 或按钮的 selector ) 如:ic_launcher_pressed 注释规范类和接口注释类和接口统一添加javadoc注释,格式如下: 123456789/** * 类或接口的描述信息 * * @author ${USER} * @date ${DATE} */ public interface Login { } 方法注释下面几种方法,都必须添加注释,说明该方法的用途和参数说明,以及返回值。如不添加注释,方法和参数命名都要见名知意 接口中定义的所有方法 抽象类中自定义的抽象方法 抽象父类的自定义公用方法 工具类的公用方法 12345678/** * 登录 * * @param loginName 登录名 * @param password 密码 * @param listener 回调监听器 */public void login(String loginName, String password, ActionCallbackListener listener); 变量和常量注释下面几种情况下的常量和变量,都要添加注释说明,优先采用 右侧// 来注释,若注释说明太长则在上方添加注释。 接口中定义的所有常量 公有类的公有常量 枚举类定义的所有枚举常量 实体类的所有属性变量 1234567public static final int TYPE_CASH = 1; // 现金券public static final int TYPE_DEBIT = 2; // 抵扣券public static final int TYPE_DISCOUNT = 3; // 折扣券 private int id; // 券idprivate String name; // 券名称private String introduce; // 券简介 发布及安全版本管理版本管理一般使用 Git 打包前必须pull一下代码 打包发版后,打上tag,push代码 打包后记得保存未加密过的包和mapping文件 建议使用 Git 进行版本管理,正式打包前先 pull 一下代码,保证发布版本为最新代码;版本发布后,打 tag 并 push 到服务器 正式版本需要打开 混淆,防止被反编译,gradle 项目在 buildTypes 中配置,混淆的配置文件为 proguard-android.txt 12345678910111213141516//必须在productFlavors之后buildTypes { release { //开启混淆 minifyEnabled true //打包时移除不用资源 shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //不同渠道使用不同的签名 signingConfig signingConfigs.sign } debug { signingConfig signingConfigs.sign }} 正式版本发布时需要关闭 Log ,防止 Log 调试信息的打印造成重要数据泄露。一般思路为封装 LogUtil,在其中根据 BuildConfig.Debug 判断是否输出日志 测试第三方SDK时,如对签名有要求,可以在 debug 时使用正式签名 正式发布版本时,可用第三方工具加密,如:360加密、爱加密,加密后最好测试下,可能会有兼容性问题 打包后记得保存未加密的包并保存 mapping 文件(列出了原始的类,方法和字段名与混淆后代码间的映射) 其它 控制语句 减少条件嵌套,不要超过3层 1234// Badif(obj != null) { doSomething(); }// Goodif(obj == null) { return; } doSomething(); if语句必须用{}包括起来,即便是只有一句 方法 拆分臃肿方法,每个方法只作一件事 做同一个逻辑的方法,尽量靠近放到一块,方便查看 尽量不要使用 try catch 处理业务逻辑 使用JSON工具类,不要手动解析和拼装数据,如:Gson 重构相关书籍 《重构-改善既有代码的设计》 参考关于 colors.xml 和 drawable 命名很久之前摘自某篇博文,忘记具体链接了,如有侵权,请联系我~ 邮箱:1025263614@qq.com","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"},{"name":"Java","slug":"Android/Java","permalink":"http://www.littlejie.com/categories/Android/Java/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"开发规范","slug":"开发规范","permalink":"http://www.littlejie.com/tags/开发规范/"}]},{"title":"Gradle 实现 Android 多渠道定制化打包","slug":"Gradle-实现-Android-多渠道定制化打包","date":"2016-10-01T04:12:43.000Z","updated":"2017-03-08T05:13:22.000Z","comments":true,"path":"2016/10/01/Gradle-实现-Android-多渠道定制化打包/","link":"","permalink":"http://www.littlejie.com/2016/10/01/Gradle-实现-Android-多渠道定制化打包/","excerpt":"","text":"版权声明:本文为博主原创文章,未经博主允许不得转载。 最近在项目中遇到需要实现 Apk 多渠道、定制化打包, Google 、百度查找了一些资料,成功实现了上述功能,在此记录以备不时之需,温故而知新,可以为师矣~ 需求可以总结如下: 多渠道打包 如何实现多个 Apk 安装在同一设备在之前的印象中,同一个应用在同一设备上只能安装一个,除非手动修改 AndroidManifest.xml 文件中的包名( package ),但这么做的后果就是新的应用真的是新的应用,旧版应用再也收不到更新。而现在你通过 Gradle,你可以轻松构建多个不同版本的应用,并且在同一设备上安装使用。 这里要用到 productFlavors ,productFlavors 可以用来自定义应用构建版本,我们可以用其 applicationId 属性来实现多个 Apk 安装在同一设备上。 build.gradle 中部分配置代码如下: 1234567891011121314151617181920212223242526272829303132333435android { compileSdkVersion 24 buildToolsVersion \"24.0.1\" //默认配置,所有 productFlavors 都会继承 defaultConfig 中配置的属性 defaultConfig { //默认的 applicationId,一般与 AndroidManifest.xml 文件 package属性相同 applicationId \"com.littlejie.multichannel\" minSdkVersion 15 targetSdkVersion 24 versionCode 1 versionName \"1.0\" } // productFlavors 定义了一个应用的自定义构建版本 //一个单一的项目可以同时定义多个不同的 flavor 来改变应用的输出。 // productFlavors 这个概念是为了解决不同的版本之间的差异非常小的情况,通常用于区分同一个应用的不同渠道/客户等,可包含少量业务功能差别。 // productFlavors 中的 flavor 不能跟 buildType 中的一样,否则会报: \"ProductFlavor names cannot collide with BuildType names\" productFlavors { //默认版本,不设置 applicationId ,继承 defaultConfig 中的配置 flavors_default { } //开发版本, applicationId 替换为 com.littlejie.multichannel.dev flavors_dev { applicationId \"com.littlejie.multichannel.dev\" } //发布版本, applicationId 替换为 com.littlejie.multichannel.release flavors_release { applicationId \"com.littlejie.multichannel.release\" } }} MainActivity.java: 123456789101112public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, \"package name = \" + this.getPackageName()); }} 在 Android Studio 中执行如下命令: 12//打 debug 包,gradle 命令会在后面 `gradle task`中详细讲述gradle clean assembleDebug 打包完成后,将 Apk 安装到模拟器(adb install name.apk),运行,log 如下: flavors_default: 109-17 22:43:55.390 19747-19747/com.littlejie.multichannel D/MainActivity: package name = com.littlejie.multichannel flavors_dev: 109-17 22:11:30.860 2638-2638/com.littlejie.multichannel.dev D/MainActivity: package name = com.littlejie.multichannel.dev flavors_release: 109-17 22:44:55.610 20650-20650/com.littlejie.multichannel.release D/MainActivity: package name = com.littlejie.multichannel.release 从这里可以看出,不同 flavor 的 package name 被 applicationId 替换掉了,而且同一个模拟器上可以同时安装以上三个应用。 下面我们再看看 AndroidManifest.xml 中发生了什么变化。这里需要用到 aapt 来查看 AndroidManifest.xml 的信息: 12//输出 apk 的 AndroidManifest.xml 文件的信息aapt dump xmltree ***.apk AndroidManifest.xml 关于 aapt 使用的更多用法,可以阅读这篇博文:使用 aapt 查看 apk 的各种信息 下面是 flavors_dev 版本的信息,可以看出 Java 源文件的包名并没有发生改变,而 package 属性的值被替换为 applicationId了。 如果在申请第三方 SDK 接入,则对应的包名应该填 applicationId ,而不是 AndroidManifest.xml 中的默认值 123456789101112131415161718192021222324252627lishengjiedeMacBook-Pro:apk littlejie$ aapt dump xmltree multichannel-flavors_dev-debug.apk AndroidManifest.xmlN: android=http://schemas.android.com/apk/res/android E: manifest (line=2) A: android:versionCode(0x0101021b)=(type 0x10)0x1 A: android:versionName(0x0101021c)="1.0" (Raw: "1.0") //此处 package 的值已替换成 applicationId 的值 A: package="com.littlejie.multichannel.dev" (Raw: "com.littlejie.multichannel.dev") A: platformBuildVersionCode=(type 0x10)0x18 (Raw: "24") A: platformBuildVersionName=(type 0x4)0x40e00000 (Raw: "7.0") E: uses-sdk (line=7) A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf A: android:targetSdkVersion(0x01010270)=(type 0x10)0x18 E: application (line=11) A: android:theme(0x01010000)=@0x7f08008e A: android:label(0x01010001)=@0x7f060020 A: android:icon(0x01010002)=@0x7f030000 A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff // Activity 的包名还是原来 AndroidManifest.xml 中申明的 E: activity (line=17) A: android:name(0x01010003)="com.littlejie.multichannel.MainActivity" (Raw: "com.littlejie.multichannel.MainActivity") E: intent-filter (line=18) E: action (line=19) A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN") E: category (line=21) A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER") applicationId 的原理可以理解为在 gradle 打包的时,动态合并属性,将 package 替换为 applicationId 指定的值,但并不会替换 Java 文件的包名,包括生成的 R 文件(可以去对应 module 下的 build/generated 目录下查看对应 flavor 的 R 文件)。 另外,由于最终生成的包中 AndroidManifest.xml 文件中的 package 属性被 applicationId 替换掉,故对于某些第三方 SDK ,如:微信、高德地图等需要验证包名的,就会碰到相当蛋疼的事,每个包都需要重新去生成 APPID 和 APPKEY,如果渠道很多,那么像微信就会出现问题微信账号申请的应用数就会超出微信的限制。 Android 官方文档原文如下: Therefore, we have decoupled the two usages of package name: The final package that is used in your built .apk’s manifest, and is the package your app is known as on your device and in the Google Play store, is the “application id”. The package that is used in your source code to refer to your R class, and to resolve any relative activity/service registrations, continues to be called the “package”. 补充:ApplicationId versus PackageName 替换 AndroidManifest.xml 中的属性这里可以参考友盟统计 SDK 中使用的方案。该方案通过在 AndroidManifest.xml 文件中 application 标签下指定 <mate-data> 设置占位符来实现动态替换属性值。 1<meta-data android:name=\"UMENG_CHANNEL\" android:value=\"${UMENG_CHANNEL}\" /> 占位符形如${name},在最终执行 AndroidManifest.xml 文件合并的时候,占位符会被 build.gradle 中对应值取代。 build.gradle 的配置需要用到上节讲到的 productFlavors 的 manifestPlaceholders 属性, manifestPlaceholders 属性直译过来就是清单文件占位符。 下面是 build.gradle 的节选代码: 123456789101112131415161718productFlavors { //将 AndroidManifest.xml 文件中的 ${UMENG_CHANNEL} 替换为 default flavors_default { manifestPlaceholders = [UMENG_CHANNEL: \"defalut\"] } flavors_dev { applicationId \"com.littlejie.multichannel.dev\" manifestPlaceholders = [UMENG_CHANNEL: \"dev\"] } flavors_release { applicationId \"com.littlejie.multichannel.release\" manifestPlaceholders = [UMENG_CHANNEL: \"release\"] }} 如果你要替换多个属性,则只需要将 manifestPlaceholders 的写法如下: 1manifestPlaceholders = [VALUE_NAME1 : \"value\" , VALUE_NAME2 : \"value\"] 补充:关于 AndroidManifest 文件合并规则可以查看 官方文档 替换资源文件多渠道打包的时候可能会碰到这种情况:每个应用市场的启动页图标、应用名称可能会有点小出入,更有甚者,连布局都不一样。这时候我们该怎么办呢? 有一种解决办法就是:在代码里进行判断,根据渠道的不一样,加载不同的图片和布局,这是一种解决办法。但是当渠道有很多时,代码就会变得很难维护,而且指定渠道用到的资源文件都会被打入所有 Apk 中。所以这个方法并不值得推荐。那么,有什么好的解决办法呢? 办法 Google 早就给我们想好了,而且相当简单,那就是:在 main 的同级目录下创建以渠道名命名的文件夹,然后创建资源文件(路径要与 main 中的一致),然后打包的时候 gradle 就会自己替换或者合并资源。 例如, App 的默认 icon 路径为 main\\res\\mipmap-hdpi\\ic_launcher.png ,那么 flavors_dev的路径就为 flavors_dev\\res\\mipmap-hdpi\\ic_launcher.png ,打包 flavors_dev 渠道的时候会自动替换图片。 对于资源合并,如果在 main 下的 strings.xml 内容为: 1234<resources> <string name=\"app_name\">MultiChannel</string> <string name=\"string_merge\">我是string,我暂时没被合并</string></resources> 在 flavors_dev 下的 strings.xml 内容为: 123<resources> <string name=\"string_merge\">我是dev_string,我会把string合并</string></resources> 当打 flavors_dev 渠道包时,最终 strings.xml 会变成: 1234<resources> <string name=\"app_name\">MultiChannel</string> <string name=\"string_merge\">我是dev_string,我会把string合并</string></resources> 以上特性可以用来替换 Apk 的应用名称和应用图标,这比使用前面讲到的占位符方便很多。同理,替换图片和合并颜色的原理也相似。 多渠道使用独立签名多渠道打包的时候,可能每个渠道包的签名都必须不一样,真正做到定制化,那么,怎么实现每个渠道包使用指定的签名呢? 平时我们打包的时候是这样的: 123456789101112131415161718192021222324signingConfigs { release { storeFile file(\"签名文件路径\") storePassword \"storePassword\" keyAlias \"keyAlias\" keyPassword \"keyPassword\" }}buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true //指定打 release 包时使用的签名文件 signingConfig signingConfigs.release } //如果 debug 包需要测试诸如微信、地图等第三方 sdk ,则可以指定 debug 包使用 release 包的签名 //debug { // signingConfig signingConfigs.release //}} 而给每个渠道包指定签名其实也差不多。 Google 官方原话: This enables either having all release packages share the same SigningConfig, by setting android.buildTypes.release.signingConfig, or have each release package use their own SigningConfig by setting each android.productFlavors.*.signingConfig objects separately. 大意就是,在 buildType 下指定签名的具体属性,形如 android.productFlavors.*.signingConfig signingConfigs.* ,前一个 * 指代在 productFlavors 中定义的 flavor ,后一个 * 指代在 signingConfigs 定义的属性。值得注意的是,signingConfigs 必须定义在 buildType 之前。 以下是 build.gradle 的配置节选: 123456789101112131415161718192021222324252627282930313233343536//定义签名属性signingConfigs { flavors_default { //如果签名文件在项目的根目录下,则可以这么写 storeFile file(\"../littlejie.jks\") storePassword \"******\" keyAlias \"******\" keyPassword \"*****\" } flavors_dev { storeFile file(\"../littlejie_dev.jks\") storePassword \"*****\" keyAlias \"*****\" keyPassword \"*****\" }}buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true //多个 flavor ,指定 flavor 使用指定 签名 productFlavors.flavors_default.signingConfig signingConfigs.flavors_default productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev } //如果 debug 包需要测试诸如微信、地图等第三方 sdk ,则可以指定 debug 包使用 release 包的签名 //debug 并不能设置多个签名 //debug { // productFlavors.flavors_default.signingConfig signingConfigs.flavors_default // productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev //}} 下面我们来验证下生成的包的签名是否正确,查看签名我们会用到如下两个命令: 123456//查看签名文件的属性keytool -list -keystore 签名文件//查看 apk 的签名,需要提前解压 apk ,获取 CERT.RSA(位于解压目录下 /META-INF 下)//以下命令行是在 apk 解压目录下执行keytool -printcert -file META-INF/CERT.RSA 更多 keytool 命令使用可以查看 官方文档 首先,我们来看下 littlejie.jks 的信息: 12345678910lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie.jks输入密钥库口令:密钥库类型: JKS密钥库提供方: SUN您的密钥库包含 1 个条目littlejie, 2016-9-18, PrivateKeyEntry,证书指纹 (SHA1): A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84 解压 multichannel-flavors_default-release.apk ,查看 CERT.RSA 信息 1234567891011lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_default-release/META-INF/CERT.RSA所有者: CN=littlejie发布者: CN=littlejie序列号: 71693e05有效期开始日期: Sun Sep 18 17:20:34 CST 2016, 截止日期: Thu Sep 12 17:20:34 CST 2041证书指纹: MD5: AC:12:83:51:44:FC:82:68:8B:23:7B:E9:12:24:AE:52 SHA1: A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84 SHA256: AD:04:19:5F:92:00:0D:FA:7C:E5:8A:12:57:72:4C:1E:0E:2E:FC:0D:92:28:05:D0:CC:42:FC:93:95:44:88:88 签名算法名称: SHA256withRSA 版本: 3 可以发现两者的 SHA1 值是相等的。 同理,可以查看 littlejie_dev.jks 和 multichannel-flavors_dev-release.apk 的签名信息 123456789101112131415161718192021222324//littlejie_dev.jks 的签名信息lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie_dev.jks输入密钥库口令:密钥库类型: JKS密钥库提供方: SUN您的密钥库包含 1 个条目littlejie, 2016-9-18, PrivateKeyEntry,证书指纹 (SHA1): B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2//multichannel-flavors_dev-release.apk 的签名信息lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_dev-release/META-INF/CERT.RSA所有者: CN=littlejie发布者: CN=littlejie序列号: 48346e15有效期开始日期: Sun Sep 18 17:21:23 CST 2016, 截止日期: Thu Sep 12 17:21:23 CST 2041证书指纹: MD5: 15:E9:E1:67:AB:33:8B:04:A4:C3:D0:05:8F:A6:35:37 SHA1: B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2 SHA256: 96:A5:14:EC:28:25:32:0D:3E:D0:DB:D0:84:06:E7:9C:17:D7:91:83:A4:51:93:AB:34:3E:D9:FD:C5:FA:A1:8E 签名算法名称: SHA256withRSA 版本: 3 但是这里有个问题,就是这种给某个 flavor 指定签名的方法对 debug 无效,有兴趣的同学可以看上述注释掉的 debug 签名部分配置。简单来说,debug 签名只能指定一个或者使用默认的 debug 签名。 若哪位大神有解决方案,欢迎指出~ 这里再做几点补充: 多渠道使用独立签名,打包时千万不要使用 Android Studio 中 Build 菜单下的 Generate Signed APK,因为当你使用这个打包的时候, Android Studio 会让你指定使用的签名文件, so 你就等着哭吧~楼主因为这个折腾了半天。解决方法就是使用 gradle tasks。传送门:Android Gradle Build Tasks 鉴于第一点中的传送门需要翻墙,所以在这里简单介绍一下 Android Gradle Build Tasks 的使用。 打全部包: gradle assemble 打全部 Debug 包: gradle assembleDebug ,可以简写为 gradle aD ,前提是没有相同缩写的参数 打全部 Release 包: gradle assembleRelease,可以简写为 gradle aR 打指定 flavor 包: gradle assemble(flavor)(Debug|Release) 打包完成后安装(设备上没有安装该 apk ,否则会失败,而且只能指定 flavor ,不然也会失败): gradle install(flavor)(Debug|Release) 打包前先 clean 一下(在测试的时候很必要,如果不 clean 的话,可能会导致某些小修改不会及时打入新包): gradle clean assembleDebug 补充有童鞋在评论中说:使用 productFlavors 打包效率太低,的确是这样, gradle 好用是好用,就是打包效率低。如果只是单纯生成渠道包,建议使用美团多渠道打包方案,另外 360 加固也是一种不错的选择,效率都比使用 gradle 来的高。但如果需要替换 Apk 中的图片、字符串、应用的 applicationId 、给指定渠道的包使用指定的签名,那么只能乖乖使用 gradle 打包了,慢你也得忍着~ 之前刚开始调研的时候,发现 Github 上有个 ApkCustomizationTool 项目,它是通过对 Apk 解包,替换图片、字符串,然后重新签名,不过这毕竟是事后诸葛亮,控制在打包的源头总是毕竟好的,有兴趣的同学可以去研究下。 不知大家有没有这种感受,每次发版上传渠道的时候想死有没有?o(╯□╰)o 总结以上就是自己在使用 Gradle 实现 Android 多渠道打包时碰到的问题, Android 官方关于使用 Gradle 的文档已经很详细了,自己总结的只是一点皮毛,有时间要去自习研读下。 读万卷书,行万里路~ 参考: Gradle Plugin User Guide Android Plugin DSL Reference Android Studio Gradle实践之多渠道自动化打包+版本号管理","categories":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/categories/Android/"},{"name":"Gradle","slug":"Android/Gradle","permalink":"http://www.littlejie.com/categories/Android/Gradle/"}],"tags":[{"name":"Android","slug":"Android","permalink":"http://www.littlejie.com/tags/Android/"},{"name":"Gradle","slug":"Gradle","permalink":"http://www.littlejie.com/tags/Gradle/"},{"name":"多渠道打包","slug":"多渠道打包","permalink":"http://www.littlejie.com/tags/多渠道打包/"}]}]}