使用SplashScreen实现闪屏页

前言

水篇小文章

一般我们实现闪屏页是通过自定义主题的windowBackground,然后在主页恢复正常主题。

而在Android12上,谷歌对闪屏页支持了更多特性(如动画Drawable等)。

并因此在Jetpack中引入了新成员——SplashScreen。

使用该库可以非常简单的实现闪屏页,并完成大部分Android版本的兼容。

API 文档

SplashScreen | Android Developers )

其实文档里的描述非常清晰。

依赖

1
implementation "androidx.core:core-splashscreen:1.0.0-beta01"

简单使用

步骤1:定制 themes

改为继承 Theme.SplashScreen.*,一般Icon背景可以使用"Theme.SplashScreen.IconBackground"

1
2
3
4
5
 <style name="Theme.Calendar_manager" parent="Theme.SplashScreen.IconBackground">  
<item name="postSplashScreenTheme">@style/Theme.Material3.DayNight</item>
<item name="windowSplashScreenBackground">@android:color/background_light</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
</style>

步骤2:将 application 或者启动页 theme 改为该 theme

步骤3:启动页 onCreate 前调用 installSplashScreen()

1
2
installSplashScreen()  
super.onCreate(savedInstanceState)

进阶使用

延长展示时间

可以使用KeepOnScreenCondition api 进行设置:

1
2
3
SplashScreen.KeepOnScreenCondition {  
return@KeepOnScreenCondition SystemClock.elapsedRealtime() - App.appCreateTime <= 150
}

简单示例

Feature 1.1.0 material you by Ayagikei · Pull Request #5 · Ayagikei/calendar-account-manager (github.com)

Android DialogFragment 相关代码段

设置软键盘弹出,自动上移

1
2
3
4
5
6
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
// 设置有软键盘时,窗口自动上移
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
...
}

设置全屏显示

1
2
3
4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen)
}
1
2
3
4
5
<style name="Dialog.FullScreen" parent="Theme.AppCompat.Dialog">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowIsFloating">false</item>
</style>

子Fragment

1
2
3
4
5
private fun replace(fragment: androidx.fragment.app.Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(R.id.fragment, fragment)
transaction.commit()
}
阅读更多

Android动画相关代码段

ValueAnimator 实现TextView打字机效果

1
2
3
4
5
6
7
8
9
10
11
fun TextView.startTypeAnimation(duration: Long = 300) {
val originText = this.text
val valueAnimator = ValueAnimator.ofInt(0, originText.length)
valueAnimator.duration = duration
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener { animation ->
val length = animation.animatedValue as Int
this.text = originText.subSequence(0, length)
}
valueAnimator.start()
}

SpringInterpolator 简易的弹性插值器

1
2
3
4
5
6
7
8
/**
* 弹性插值器
*/
class SpringInterpolator(private val factor: Float) : Interpolator {
override fun getInterpolation(input: Float): Float {
return (2.0.pow((-10 * input).toDouble()) * sin((input - factor / 4) * (2 * Math.PI) / factor) + 1).toFloat()
}
}
阅读更多

Android 5.0共享元素崩溃

简介

后台监测到了数个仅限于Android5的崩溃问题,日志大概如下:

1
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewParent android.view.View.getParent()' on a null object reference at android.view.ViewOverlay$OverlayViewGroup.add(ViewOverlay.java:164) at android.view.ViewGroupOverlay.add(ViewGroupOverlay.java:63) at android.app.EnterTransitionCoordinator.startRejectedAnimations(EnterTransitionCoordinator.java:598) at android.app.EnterTransitionCoordinator.startSharedElementTransition(EnterTransitionCoordinator.java:325) at android.app.EnterTransitionCoordinator.access$200(EnterTransitionCoordinator.java:42) at android.app.EnterTransitionCoordinator$5$1.run(EnterTransitionCoordinator.java:389) at android.app.ActivityTransitionCoordinator.startTransition(ActivityTransitionCoordinator.java:698) at android.app.EnterTransitionCoordinator$5.onPreDraw(EnterTransitionCoordinator.java:386) at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:847) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1985) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1077) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5845) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767) at android.view.Choreographer.doCallbacks(Choreographer.java:580) at android.view.Choreographer.doFrame(Choreographer.java:550) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:135) at android.app.ActivityThread.main(ActivityThread.java:5272) at java.lang.reflect.Method.invoke(Method.java) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:909) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:704)

查了下资料,发现这是 Android 5.0 系统的内置Bug。

在执行切换动画的时候,有一定条件下会导致崩溃问题。

并且,在 API level 22(即 Android 5.1)之后,这个 bug 已经被修复了。

最简单也是最有效的解决方法:

只有在API level 22的情况下,再执行切换动画。

其他

也有一些想要在 Android 5.0 继续使用切换动画,避免崩溃的解决方案。

这个 Android 系统 bug 与分享元素的选取排除(’rejected’)的处理有关。如果一个分享元素在执行动画的时候没有 attached to the window 就有可能被拒绝,也就是当 View 可见性为 GONE 的时候。

所以,解决方法就是在执行切换动画的时候,检查每一个共享元素View,并且将可见性改为 VISIBLE

Glenn Schmidt

参考

https://stackoverflow.com/questions/34658911/entertransitioncoordinator-causes-npe-in-android-5-0

PopupMenu弹出导致RecyclerView滚动的解决方法

参考: https://blog.csdn.net/qq_16445551/article/details/70213660

起因

这个问题其实一直存在,但是新版本增加了Toolbar收缩之后,就因为这个自动滚动直接导致了Toolbar收缩,现象更为明显。

解决办法

参考原文两个解决办法

如果你的应用的最小支持版本达到了Android KitKat 4.4的话,建议使用第一种方法。

简单总结一下:

  1. support.v7包的PopupMenu换成android.widget.PopupWindow包下的PopupMenu。

    对应到AndroidX的话,就是将androidx.appcompat.widget.PopupMenu换成import android.widget.PopupMenu

  1. 重写与PopupMenu绑定的AnchorViewrequestRectangleOnScreen(Rect rectangle, boolean immediate)方法,并且return false

Kotlin协程混淆规则

参考: https://github.com/Kotlin/kotlinx.coroutines/issues/799

起因

发布了《人升》新版本后,

线上突然出现了数个Kotlin协程相关的异常。

IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. ‘kotlinx-coroutines-android’

而我们肯定是已经依赖了kotlin的协程库的,问题不在于此。

不是很懂为什么在更新的好几天之后才集中爆发这个问题。

查询了一下发现是升级kotlin版本后的混淆bug,要增加几条混淆规则。

混淆规则

https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro

将以下混淆规则加入到项目的混淆规则文件中:

1
2
3
4
5
6
7
8
9
10
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}

然后重新编译打包发版吧~

Git修改已经提交的用户名信息

参考: https://www.jianshu.com/p/93bb4d049955

最近的一条记录

如果只是需要修改最近的一条记录的话,只需要

1
git commit --amend --author="作者名 <邮箱@xxxx.com>"

注意:这里的尖括号是要带上的。

多条记录

1
2
3
4
5
6
7
8
9
10
# 第一步,(n)代表提交次数
git rebase -i HEAD~n
# 第二步
然后按`i`编辑,把`pick` 改成 `edit`,按'Esc'退出编辑,按`:wq`保存退出
# 第三步
git commit --amend --author="作者 <邮箱@xxxx.com>" --no-edit
# 第四步(多条记录的话,会跳转到下一条记录,返回第三步)
git rebase --continue
# 第五步(全部完成后,覆盖远程git记录,危险操作要谨慎操作)
git push --force

非常多条记录更改

转自网络:

如果是多个修改,那么就需要使用到git filter-branch这个工具来做批量修改
为了方便大家使用,封装了一个简单的shell脚本,直接修改[XXX]中的变量为对应的值即可

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
#!/bin/sh

git filter-branch --env-filter '

an="$GIT_AUTHOR_NAME"
am="$GIT_AUTHOR_EMAIL"
cn="$GIT_COMMITTER_NAME"
cm="$GIT_COMMITTER_EMAIL"

if [ "$GIT_COMMITTER_EMAIL" = "[Your Old Email]" ]
then
cn="[Your New Author Name]"
cm="[Your New Email]"
fi
if [ "$GIT_AUTHOR_EMAIL" = "[Your Old Email]" ]
then
an="[Your New Author Name]"
am="[Your New Email]"
fi

export GIT_AUTHOR_NAME="$an"
export GIT_AUTHOR_EMAIL="$am"
export GIT_COMMITTER_NAME="$cn"
export GIT_COMMITTER_EMAIL="$cm"
'

[Android]多语言、Google授权登陆

以此文总结一下《LifeUp》上架 GooglePlay 的全过程~

多语言

App默认会根据系统语言加载不同的字符串资源文件,这是我们实现多语言的基础。

新建资源文件

首先是建立相应的资源文件,可以手动建立,也可以用 Android Studio 的选项建立:

右键 res 文件夹选择File–>New–>Android resource file选择 Locale

可以见到下图

选择你要新建的语言,并且改文件名为strings即可。

实际上,就是新建一个value-(语言缩写)的文件夹(简体中文的话就是value-zh),然后在其中放上string.xml文件。

提取代码、Layout中的字符串

点击相应的字符串,按下ALT+ENTER,然后选择 Extract string resouces

然后输入字符串资源的名称,勾选相应的资源文件:

按下确定后,AS就会提取该字符串到资源文件内了,同时代码会被替换成:

Context getString(resouceId)

在Layout中操作同理,利用这个操作可以替换掉App内大部分的静态的字符串。

那么需要动态赋值的字符串该怎么办呢?

阅读更多

Toolbar减少标题和 Navigation icon 的间距

Toolbar 默认配置下,标题和Navigation icon(比如返回按钮)之间的间隔会迷之过长,

可以通过配置toolbar的属性调整:

1
app:contentInsetStartWithNavigation="0dp"

这个间隔的设计好像是配合没有Navigation icon的情况的,默认值为16dp。

有Navigation icon的情况下应该手动调整。

完整的Toolbar配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:contentInsetStartWithNavigation="0dp"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</com.google.android.material.appbar.AppBarLayout>

桌面小部件-IntentService-Oreo的那些事

简介

之前在《人升》的桌面小部件,实现ListView中的点击事件监听的方式是:

使用fillInIntent发送广播到Widget类中,并在onReceive方法中拦截,处理业务逻辑。

但是,

Widget的本质是个广播接收器,不适宜在里面处理耗时操作。

(完成团队事项的时候需要发送网络请求,普通事项需要更改数据库,都可以视为是耗时操作。)

所以,我决定改用IntentService处理完成事项的业务逻辑。

IntentService的特点是后台运行、自动销毁、异步运行。

首先尝试直接用fillInIntent启动服务失败了。

然后改成了先发送广播,然后在Widget类中,并在onReceive方法拦截再启动IntentService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)

if (...) {
...
} else if (intent.action == FINISH_TASK) {
// 将耗时操作交给IntentService完成
val finishIntent = Intent(context, FinishTaskIntentService::class.java)
if (intent.extras != null) {
finishIntent.putExtras(intent.extras!!)
}

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(finishIntent)
} else {
context.startService(finishIntent)
}
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShortToast("完成事项似乎出现了一些问题,请尝试刷新下。", LifeUpApplication.getLifeUpApplication())
}
}
}

然后就遇到了坑。

阅读更多