Android首页灰色实现方案

Activity设置灰色

使用ColorMatrix设置灰度

1
2
3
4
5
6
7
private fun setGrayPaint(view: View) {
val paint = Paint()
val cm = ColorMatrix()
cm.setSaturation(0f)
paint.colorFilter = ColorMatrixColorFilter(cm)
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
}

给首页Activity的decorView设置灰度Paint

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(...)

// 需要在 setContentView 之后
setGrayPaint(window.decorView)
}

需要特殊处理的控件

  • 弹框
  • WebView
  • SurfaceView

这些控件由于不是跟activity公用一个window,需要各自单独处理灰度Paint。调用 setGrayPaint(view) 即可。

相关资源

Android实现设置灰白模式效果

微信AndResGuard资源混淆工具

微信AndResGuard资源混淆工具

AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。

AndResGuard不涉及编译过程,只需输入一个apk(无论签名与否,debug版,release版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的apk(若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的apk)以及对应资源ID的mapping文件。

0x01 原理介绍

根据Android的编译流程,所有资源ID已经被编译成32位int值。这说明我们并不需要去修改xml与java,因为在编译过程已经被R.java所替换,我们直接修改resources.arsc的二进制数据,不改变打包流程,只要在生成resources.arsc之后修改它,同时重命名资源文件。

0x02 使用场景

  • 缩小APK体积
  • 保护res资源文件的可读性
  • 皮应用中减少跟主应用代码的重复率

0x03 资源混淆配置

  1. Project根目录的build.gradle中,添加插件的依赖
1
2
3
4
5
buildscript {
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
}
}
  1. app模块的build.gradle中添加Res相关配置
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
plugins {
id 'AndResGuard'
}

android {...}

andResGuard {
mappingFile = file("./resource_mapping.txt")
use7zip = false
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
"R.mipmap.ic_launcher",
"R.mipmap.ic_launcher_round",
"R.string.app_name",
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.21'
//path = "/usr/local/bin/7za"
}

/** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/
// finalApkBackupPath = "${project.rootDir}/final.apk"

/** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/
// digestalg = "SHA-256"
}

0x04 如何启动

使用Android Studio的同学可以在 andresguard 下找到相关的构建任务; 命令行可直接运行./gradlew resguard[BuildType | Flavor], 这里的任务命令规则和assemble一致。

0x05 配置7Zip压缩

在设置sevenzip时, 你只需设置artifact或path, 支持同时设置,总以path的值为优先。

0x06 配置apk输出

如果没有配置finalApkBackupPath,最终结果会覆盖assemble[BuildType | Flavor]的输出APK。如果配置则输出至finalApkBackupPath配置路径。

0x07 Font资源不支持混淆

如果项目中使用了font资源,需要配置mappingFile = file("./resource_mapping.txt"),同时在app目录的resource_mapping.txt文件中添加

1
2
res path mapping:
res/font -> res/font

0x08 一些需要注意的问题

  • 如果不是对APK size有极致的需求,请不要把resources.arsc添加进compressFilePattern.
  • 对于发布于Google Play的APP,建议不要使用7Zip压缩,因为这个会导致Google Play的优化Patch算法失效.
  • compress参数对混淆效果的影响
    若指定compess 参数.png、.gif以及*.jpg,resources.arsc会大大减少安装包体积。若要支持2.2,resources.arsc需保证压缩前小于1M。
  • 操作系统对7z的影响
    实验证明,linux与mac的7z效果更好
  • keepmapping方式对增量包大小的影响
    影响并不大,但使用keepmapping方式有利于保持所有版本混淆的一致性
  • 渠道包的问题(建议通过修改zip摘要的方式生产渠道包)
    在出渠道包的时候,解压重压缩会破坏7zip的效果,通过repackage命令可用7zip重压缩。
  • 通过getIdentifier方式获得资源,需要放置白名单中。
  • 部分手机桌面快捷图标的实现有问题,务必将程序桌面icon加入白名单
  • 第三方SDK的资源加入白名单。可以在white_list.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md查看更多sdk的白名单配置

0x09 相关资源

使用说明:https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md

原理介绍:https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=208135658&idx=1&sn=ac9bd6b4927e9e82f9fa14e396183a8f#rd

white_list.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md

链表(Java版)

链表(Java版)

在链表操作中,我们通常通过生成一个 dummy 哑节点,来避免空链表的判断。

19.删除链表的倒数第 N 个结点

方法一:计算链表长度

为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

方法三:双指针

我们可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

时间复杂度:O(L),其中 L 是链表的长度。
空间复杂度:O(1)。

21.合并两个有序链表

  1. 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里。
  2. 当 l1 和 l2 有一个是空的,只需要简单地将非空链表接在合并链表的后面。

23.合并K个升序链表

方法一:顺序合并

已经有合并两个有序链表的前提,直接遍历数组,依次合并两个链表得到结果。

方法二:分治合并

将 k 个链表分组并将同一组中的链表合并(递归实现)

进时间复杂度为 O(kn×logk)
空间复杂度,递归会使用到 O(logk) 空间代价的栈空间

Android动态权限申请

Android动态权限申请

0x01 介绍

由于 Android 动态权限申请是一个交互比较复杂的模块,整个申请的流程也比较长,所以,写了一个工具来封装了一个,也具体的实现了一个流程。

由于每个App的Ui风格不一致,所以没有把Toast和弹框封装进工具,等后期有好的想法再优化。

0x02 动态权限申请流程

  1. 检查授权状态
  2. 申请权限
  3. 处理权限申请结果
  4. 当用户‘拒绝且不再询问’,引导去手机设置
  5. 检查手机设置后的权限申请结果

0x03 使用说明

主要封装类 PermissionManager

根据业务需要用到安卓定义的高危权限,需要去动态申请权权限。通常是在Activity 和 Fragment 组件中发起。

1 检查权限授权状态

1
2
3
4
5
6
if (PermissionManager.hasPermissions(this, Manifest.permission.READ_PHONE_STATE)) {
// after permission
afterPermission()
} else {
showRequestPermissionDialog("申请手机权限的原因是因为我需要", Manifest.permission.READ_PHONE_STATE)
}

2 申请权限,根据合规化通常需要先弹框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun showRequestPermissionDialog(message: String, vararg perm: String) {
AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage(message)
.setPositiveButton("去授权") { dialog, which ->
dialog.dismiss()
PermissionManager.requestPermissions(this, *perm)
}
.setNegativeButton("取消") { dialog, which ->
dialog.dismiss()
ToastUtil.showToast(this, "已取消授权...")
}
.setCancelable(false)
.create()
.show()
}

3 处理权限申请结果

onRequestPermissionsResult 回调接口中,调用 PermissionManager.onRequestPermissionsResult 方法,并且实现 PermissionManager.OnPermissionResultCallback 这个回调接口。

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
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
PermissionManager.onRequestPermissionsResult(
this,
requestCode,
permissions,
grantResults,
this
)
}

override fun onPermissionsGranted(requestCode: Int, perms: List<String?>) {
ToastUtil.showToast(this, "授权成功")
afterPermission()
}

override fun onPermissionsDenied(requestCode: Int, perms: List<String?>) {
ToastUtil.showToast(this, "授权失败")
}

override fun onPermissionDeniedForever(requestCode: Int, perms: List<String?>) {
showSettingsDialog()
}

4 当用户‘拒绝且不再询问’,引导去手机设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun showSettingsDialog() {
AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage("已永久拒绝,需要去设置->权限设置打开")
.setPositiveButton("前往设置") { dialog, which ->
dialog.dismiss()
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null))
startActivityForResult(intent, REQUEST_CODE_FOR_PERMISSION_SETTINGS)
}
.setNegativeButton("取消") { dialog, which ->
dialog.dismiss()
ToastUtil.showToast(this, "已取消授权...")
}
.setCancelable(false)
.create()
.show()
}

5 检查手机设置后的权限申请结果

1
2
3
4
5
6
7
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_PERMISSION_SETTINGS) {
afterPermission()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

0x04 特别推荐 PermissionX

PermissionX

中文文档

PermissionX is an extension Android library that makes Android runtime permission request extremely easy. You can use it for basic permission request occasions or handle more complex conditions, like showing rationale dialog or go to app settings for allowance manually.

Android从现有的项目创建一个皮应用

Android从现有的项目创建一个皮应用

0x01 Copy一个新项目

  1. clean原项目,然后直接Copy原项目所有文件,等待完成
  2. 根据新项目重命名新文件夹名称
  3. 删除原项目的 .idea .git .gitlab 等文件夹,.gradle 可以不删
  4. 用编辑器打开 settings.gradle,修改项目名称 rootProject.name = "xxx"

0x02 重命名项目包名

这一步是要将包名从 com.sample1.android 重命名为 com.sample2.android

  1. 用AndroidStudio打开新项目,去掉 Compat Middle Packages 前面的勾
  2. 选择项目的 sample1 包名,Shift + F6重命名
  3. 一定选择 Rename Package
  4. 等待完成,项目大时间就比较长
  5. 修改 .aidl 文件的包名路径和import的路径
  6. 修改根目录下的 build.gradleapplicationId "com.sample2.android",并 sync

ps: 不清楚为啥Kotlin的扩展函数还需要重新手动导入

0x03 推到代码库

到上面这一步,不出意外的话已经可以编译成功并且跑起来了。编译成功之后可以先推送本地项目到代码库。

远程代码库新建项目,获取新项目git地址,然后执行下面操作。

1
2
3
4
5
6
$ cd existing_folder
$ git init
$ git remote add origin git://github.com/schacon/grit.git
$ git add .
$ git commit
$ git push -u origin master

0x04 后续

下面就是修改名称,URL,替换功能啥的。

  • Logo、名称、资源文件替换
  • H5协议替换
  • 域名修改
  • 第三方账号切换

0x05 审核相关

现在应用市场对新应用的审核非常严格。为了过审,可能需要做很多相关的工作

  • 混淆,或者改文件名
  • 资源混淆,或者资源改名
  • 增加新功能
  • 修改UI界面样式
  • 做审核版功能

0x06 问题

0x0601 如果Shift + F6重命名时没有Rename Package选项

如果Shift + F6重命名时没有Rename Package选项, 并且出现了以下提示,
Package 'example' contains directories in libraries which cannot be renamed. Do you want to rename current directory or all directories in project?
这是因为依赖包中也存在example这个包名,导致无法直接重命名包名。解决的方案分两种:

方案1. 暂时先移除包含example包名的依赖包,等重命名之后重新添加到项目中
方案2. 只能新建package,然后将要rename的包拖动到新的package中,或者F6移动

PS:因为无法重命名,刚刚经历了手动移动十几个module的package包路径的痛苦历程。

RoundImageView圆角控件

RoundImageView圆角控件

示例代码如下:

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
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;

public class RoundImageView extends AppCompatImageView {
private static final String TAG = "RoundImageView";

private int radius = 0;

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

public RoundImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup(context, attrs, defStyleAttr);
}

private void setup(Context context, AttributeSet attrs, int defStyleAttr) {
try {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
radius = a.getDimensionPixelSize(R.styleable.RoundImageView_riv_radius, 0);
Log.d(TAG, "RoundImageView: radius=" + radius);
a.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
protected void onDraw(Canvas canvas) {
if (radius > 0) {
Path path = new Path();
path.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radius, radius, Path.Direction.CW);
canvas.clipPath(path);//设置可显示的区域,canvas四个角会被剪裁掉
}
super.onDraw(canvas);
}
}

attrs.xml 文件中定义控件的圆角dp值属性:

1
2
3
<declare-styleable name="RoundImageView">
<attr name="riv_radius" format="dimension" />
</declare-styleable>

使用示例

1
2
3
4
5
6
<com.xx.ui.widget.RoundImageView
android:id="@+id/image_view"
android:layout_width="120dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
app:riv_radius="8dp" />
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×