-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 422 KB
/
content.json
1
{"posts":[{"title":"APK签名之4种签名方案","text":"4种APK签名方案应用签名: https://source.android.google.cn/docs/security/features/apksigning?hl=zh-cn 0x01 V1签名Android 7.0以下版本, 只能用旧签名方案 V1 scheme (JAR signing) 来自JDK(jarsigner), 对zip压缩包的每个文件进行验证, 签名后还能对压缩包修改(移动/重新压缩文件) 对V1签名的apk/jar解压,在META-INF存放签名文件(MANIFEST.MF, CERT.SF, CERT.RSA), 其中MANIFEST.MF文件保存所有文件的SHA1指纹(除了META-INF文件), 由此可知: V1签名是对压缩包中单个文件签名验证 0x02 V2签名Android 7.0开始, 谷歌增加新签名方案 V2 Scheme (APK Signature) 来自Google(apksigner), 对zip压缩包的整个文件验证, 签名后不能修改压缩包(包括zipalign), 对V2签名的apk解压,没有发现签名文件,重新压缩后V2签名就失效, 由此可知: V2签名是对整个APK签名验证 V2签名优点很明显: 签名更安全(不能修改压缩包) 签名验证时间更短(不需要解压验证),因而安装速度加快 0x03 V3签名Android 9 支持 APK 密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮替,APK 必须指示新旧签名密钥之间的信任级别。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构体的信息,以允许使用新旧密钥。 0x04 V4签名Android 11 添加了对 APK 签名方案 v4 的支持。此方案会在单独的文件 (apk-name.apk.idsig) 中生成一种新的签名。此方案支持 ADB 增量 APK 安装,这样会加快 APK 安装速度。v4 签名需要 v2 或 v3 签名作为补充。 ADB 增量 APK 安装在设备上安装大型(2GB 以上)APK 可能需要很长的时间,即使应用只是稍作更改也是如此。ADB(Android 调试桥)增量 APK 安装可以安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加速这一过程。如果设备支持该功能,并且您安装了最新的 SDK 平台工具,adb install 将自动使用此功能。如果不支持,系统会自动使用默认安装方法。https://developer.android.google.cn/about/versions/11/features 运行以下 adb 命令以使用该功能。如果设备不支持增量安装,该命令将会失败并输出详细的解释。 adb install –incremental 在运行 ADB 增量 APK 安装之前,您必须先为 APK 签名并创建一个 APK 签名方案 v4 文件。必须将 v4 签名文件放在 APK 旁边,才能使此功能正常运行。 0x05 总结综上,可以看到APK v4是面向ADB即开发调试的,而如果我们没有签名变动的需求也可以不考虑APK v3,所以目前国内大部分还停留在APK v2。 0x06 使用 在项目app的build.gradle中配置支持的签名方案 12345678910111213141516android { signingConfigs { release { keyAlias 'test' keyPassword 'pwd123' storeFile file("test.jks") storePassword 'pwd123' enableV1Signing true enableV2Signing true enableV3Signing true enableV4Signing true } }} Eclipse或Android Studio在Debug时,对App签名都会使用一个默认的密钥库: 默认在C:\\Users\\用户名\\.android\\debug.keystore 密钥库名: debug.keystore 密钥别名: androiddebugkey 密钥库密码: android","link":"/blog/2023/08/17/APK%E7%AD%BE%E5%90%8D%E4%B9%8B4%E7%A7%8D%E7%AD%BE%E5%90%8D%E6%96%B9%E6%A1%88/"},{"title":"APK签名之apksigner签名工具","text":"APK签名之apksigner签名工具0x00 参考apksigner官网:https://developer.android.google.cn/studio/command-line/apksigner?hl=zh-cnAndroid中APK签名工具之jarsigner和apksigner详解:https://www.jb51.net/article/141954.htmAndroid的:https://zhuanlan.zhihu.com/p/541983756 0x01 V1和V2签名工具 jarsigner 是JDK提供的针对jar包签名的通用工具,位于 JDK/bin/jarsigner.exe,仅支持V1签名 apksigner 是Google 官方提供的针对 Android apk 签名及验证的专用工具,位于 Android SDK/build-tools/SDK版本/apksigner.bat,支持apk的多种签名方案 不管是 apk 包,还是 jar 包,本质都是 zip 格式的压缩包,所以它们的签名过程都差不多(仅限V1签名),以上两个工具都可以对 Android apk 包进行签名. 注意:apksigner 工具默认同时使用V1+V2+V3签名方案,以兼容 Android7.0以下系统版本 0x02 签名命令进入Android SDK/build-tools/SDK版本,输入: 1.\\apksigner sign --v1-signing-enabled true --v2-signing-enabled true --ks D:\\apk\\test.jks --in D:\\apk\\xiaomi-unsign.apk --out D:\\apk\\xiaomi-signed.apk 参数说明: –v1-signing-enabled <true | false>确定 apksigner 是否会使用基于 JAR 的传统签名方案为给定的 APK 软件包签名。默认情况下,该工具会使用 –min-sdk-version 和 –max-sdk-version 的值来决定何时采用此签名方案。–v2-signing-enabled <true | false>确定 apksigner 是否会使用 APK 签名方案 v2 为给定的 APK 软件包签名。默认情况下,该工具会使用 –min-sdk-version 和 –max-sdk-version 的值来决定何时采用此签名方案。–v3-signing-enabled <true | false>确定 apksigner 是否会使用 APK 签名方案 v3 为给定的 APK 软件包签名。默认情况下,该工具会使用 –min-sdk-version 和 –max-sdk-version 的值来决定何时采用此签名方案。–v4-signing-enabled <true | false | only>确定 apksigner 是否会使用 APK 签名方案 v4 为给定的 APK 软件包签名。此方案会在单独的文件 (apk-name.apk.idsig) 中生成签名。如果为 true 并且 APK 未签名,则系统会根据 –min-sdk-version 和 –max-sdk-version 的值生成 v2 或 v3 签名。然后,该命令会根据已签名的 APK 的内容生成 .idsig 文件。使用 only 仅生成 v4 签名,而不会修改 APK 及其在调用前具有的任何签名。如果 APK 没有 v2 或 v3 签名,或者签名使用的密钥不同于为当前调用提供的密钥,则 only 会失败。默认情况下,该工具会使用 –min-sdk-version 和 –max-sdk-version 的值来决定何时采用此签名方案。 0x03 验证命令123.\\apksigner verify -v D:\\apk\\xiaomi-signed.apkkeytool -printcert -jarfile D:\\apk\\xiaomi-signed.apk","link":"/blog/2023/08/15/APK%E7%AD%BE%E5%90%8D%E4%B9%8Bapksigner%E7%AD%BE%E5%90%8D%E5%B7%A5%E5%85%B7/"},{"title":"APK签名之jarsigner签名工具","text":"APK签名之jarsigner签名工具使用JDK签名工具jarsigner签名APK文件 jarsigner -verbose -keystore [签名文件路径] -signedjar [签名后的apk文件路径] [未签名的apk文件路径] [证书别名] 1jarsigner -verbose -keystore D:\\xxx\\xxx.jks -signedjar D:\\xxx\\xxx_signed.apk D:\\xxx\\***.apk keyAlias","link":"/blog/2023/01/10/APK%E7%AD%BE%E5%90%8D%E4%B9%8Bjarsigner%E7%AD%BE%E5%90%8D%E5%B7%A5%E5%85%B7/"},{"title":"APK签名之签名文件的生成和查看","text":"APK签名之签名文件的生成和查看0x01 keytool生成签名文件进入 JDK/bin,输入命令: 1keytool -genkey -alias 密钥别名 -keyalg RSA -keysize 1024 -validity 36500 -keystore D:\\test.jks -storetype pkcs12 参数说明: -genkeypair 生成一条密钥对(由私钥和公钥组成)-keystore 密钥库名字及存储位置(默认当前目录)-alias 密钥对的别名(密钥库可以存在多个密钥对,用于区分不同密钥对)-validity 密钥对的有效期(单位:天)-keyalg 生成密钥对的算法(常用 RSA/DSA ,DSA 只用于签名,默认采用DSA ) 提示:可重复使用此命令,在同一密钥库中创建多条密钥对 0x02 使用AndroidStudio工具生成jks签名文件0x03 查看签名文件信息0x0301 keytool工具查看签名信息进入 JDK/bin,输入命令: 1keytool -v -list -keystore D:\\test.jks 0x0302 signingReport查看签名MD5部分应用商店需要签名文件的md5,在 AndroidStudio 中执行gradlew命令 123gradlew signingReportgradlew :app:signingReport ## 只打印APP的签名信息","link":"/blog/2021/11/23/APK%E7%AD%BE%E5%90%8D%E4%B9%8B%E7%AD%BE%E5%90%8D%E6%96%87%E4%BB%B6%E7%9A%84%E7%94%9F%E6%88%90%E5%92%8C%E6%9F%A5%E7%9C%8B/"},{"title":"FileProvider 的使用","text":"FileProvider 的使用Dev Doc https://developer.android.google.cn/reference/androidx/core/content/FileProvider 0x01 定义一个FileProvider在 androidx 包提供的 FileProvider 提供了 生成文件Uri 的功能。 在 manifest 文件中,声明一个 provider 123456789101112131415161718<manifest> ... <application> ... <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" tools:replace="android:authorities"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" tools:replace="android:resource" /> </provider> ... </application></manifest> 0x02 可用文件路径配置在 res/xml/file_paths.xml 下配置可用的文件路径,FileProvider 只能生成配置了的文件Uri。每个你想要生成Uri的文件路径都需要在 paths 下面定义。 123456789<paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="my_images" path="images/"/> <cache-path name="name" path="path" /> <external-path name="name" path="path" /> <external-files-path name="name" path="path" /> <external-cache-path name="name" path="path" /> <external-media-path name="name" path="path" /> ...</paths> 0x03 生成一个Uri和其他 app 共享一个文件,你需要生成一个Uri。 123456789File imagePath = new File(Context.getFilesDir(), "my_images");File newFile = new File(imagePath, "default_image.jpg");Uri uriForFile;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { uriForFile = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", newFile);} else { uriForFile = Uri.parse("file://" + newFile.toString());} getUriForFile() 返回一个 content URI content://com.mydomain.fileprovider/my_images/default_image.jpg. 0x04 授予权限123shareContentIntent.setClipData(ClipData.newRawUri("", contentUri));shareContentIntent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); Put the content URI in an Intent by calling setData(). Call the method Intent.setFlags() with either Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or both. Send the Intent to another app. Most often, you do this by calling setResult(). 0x05 提供Uri给其他app12345// 使用uriIntent i = new Intent(Intent.ACTION_VIEW);i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);i.setDataAndType(uriForFile, "application/vnd.android.package-archive");mContext.startActivity(i);","link":"/blog/2021/12/07/Android%E4%B8%93%E6%A0%8F-FileProvider/"},{"title":"Android专栏-BaseQuickAdapterHelper","text":"Android专栏-BaseQuickAdapterHelper0x01 自动加载更多-LoadingFooterView12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import android.view.Viewimport android.view.ViewGroupimport com.chad.library.adapter.base.loadmore.BaseLoadMoreViewimport com.chad.library.adapter.base.loadmore.LoadMoreStatusimport com.chad.library.adapter.base.util.getItemViewimport com.chad.library.adapter.base.viewholder.BaseViewHolderclass LoadingFooterView : BaseLoadMoreView() { private var loadingView: LoadingPagView? = null override fun getRootView(parent: ViewGroup): View { val rootView = parent.getItemView(R.layout.ui_footer_adapter_load_more) loadingView = rootView.findViewById(R.id.loadingView) return rootView } override fun getLoadingView(holder: BaseViewHolder): View { return holder.getView(R.id.loadingView) } override fun getLoadComplete(holder: BaseViewHolder): View { return holder.getView(R.id.fakeView) } override fun getLoadEndView(holder: BaseViewHolder): View { return holder.getView(R.id.endView) } override fun getLoadFailView(holder: BaseViewHolder): View { return holder.getView(R.id.fakeView) } override fun convert(holder: BaseViewHolder, position: Int, loadMoreStatus: LoadMoreStatus) { super.convert(holder, position, loadMoreStatus) when (loadMoreStatus) { LoadMoreStatus.Complete -> { loadingView?.stopPlay() } LoadMoreStatus.Loading -> { loadingView?.startPlay() } LoadMoreStatus.Fail -> { } LoadMoreStatus.End -> { loadingView?.stopPlay() } } }}","link":"/blog/2021/05/25/Android%E4%B8%93%E6%A0%8F-BaseQuickAdapterHelper/"},{"title":"Android专栏-JavaCrash默认处理","text":"Java Crash 默认处理CrashHandler 处理 Java 异常流程: 区分Debug模式和Release模式、主进程和子进程、主线程和子线程来处理 捕获Activity的生命周期内异常,并主动杀死Activity View绘制流程异常捕获 自定义上报 CrashHandler 源码如下 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402import android.os.Buildimport android.os.Environmentimport android.os.Handlerimport android.os.Looperimport android.text.TextUtilsimport android.util.Logimport com.alibaba.ha.adapter.AliHaAdapterimport com.rrtv.action.manager.ThreadPoolManagerimport com.rrtv.utils.utils.UiUtilsimport java.io.ByteArrayInputStreamimport java.io.Fileimport java.io.FileOutputStreamimport java.io.IOExceptionimport java.lang.reflect.Fieldimport java.text.DateFormatimport java.text.SimpleDateFormatimport java.util.*/** * Java Crash 默认处理 */class CrashHandler : Thread.UncaughtExceptionHandler { private var isDebug = false private var isMainProcess = false private var sActivityKiller: IActivityKiller? = null companion object { private const val TAG = "CrashHandler" private var instance = CrashHandler() @Volatile @JvmStatic private var hasInit = false /** * 初始化 * * 在 @link Application#onCreate() 方法中 install() * * @param isDebug 是否debug模式 BuildConfig.DEBUG * @param isMainProcess 是否是主进程 android.os.Process.myPid() */ @JvmStatic fun install(isDebug: Boolean, isMainProcess: Boolean) { if (!hasInit) { synchronized(CrashHandler::class.java) { if (!hasInit) { Log.d(TAG, "install: isDebug = $isDebug, mainPid = $isMainProcess") hasInit = true instance.setup(isDebug, isMainProcess) Log.d(TAG, "install success.") } } } } } private fun setup(isDebug: Boolean, isMainProcess: Boolean) { Log.d(TAG, "setup:") this.isDebug = isDebug this.isMainProcess = isMainProcess Thread.setDefaultUncaughtExceptionHandler(this) if (!isDebug && isMainProcess) { // release 模式防止主线程奔溃 Handler(Looper.getMainLooper()).post { while (true) { try { Looper.loop() } catch (e: Exception) { handleLooperException(e) } } } try { // 生命周期的 ActivityKiller initActivityKiller() } catch (e: Exception) { Log.e(TAG, "拦截生命周期失败", e) } } } /** * 替换ActivityThread.mH.mCallback,实现拦截Activity生命周期,直接忽略生命周期的异常的话会导致黑屏,目前 * 会调用ActivityManager的finishActivity结束掉生命周期抛出异常的Activity */ private fun initActivityKiller() { Log.d(TAG, "initActivityKiller: Build.VERSION.SDK_INT=${Build.VERSION.SDK_INT}") //各版本android的ActivityManager获取方式,finishActivity的参数,token(binder对象)的获取不一样 if (Build.VERSION.SDK_INT >= 28) { sActivityKiller = ActivityKillerV28() } else if (Build.VERSION.SDK_INT >= 26) { sActivityKiller = ActivityKillerV26() } else if (Build.VERSION.SDK_INT == 25 || Build.VERSION.SDK_INT == 24) { sActivityKiller = ActivityKillerV24_V25() } else if (Build.VERSION.SDK_INT in 21..23) { sActivityKiller = ActivityKillerV21_V23() } else if (Build.VERSION.SDK_INT in 15..20) { sActivityKiller = ActivityKillerV15_V20() } else if (Build.VERSION.SDK_INT < 15) { sActivityKiller = ActivityKillerV15_V20() } try { hookMH() Log.e(TAG, "hookMH Success.") } catch (e: Throwable) { Log.e(TAG, "hookMH 失败: ", e) } } @Throws(Exception::class) private fun hookMH() { Log.d(TAG, "hookMH: ") val LAUNCH_ACTIVITY = 100 val PAUSE_ACTIVITY = 101 val PAUSE_ACTIVITY_FINISHING = 102 val STOP_ACTIVITY_HIDE = 104 val RESUME_ACTIVITY = 107 val DESTROY_ACTIVITY = 109 val NEW_INTENT = 112 val RELAUNCH_ACTIVITY = 126 val activityThreadClass = Class.forName("android.app.ActivityThread") val activityThread = activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null) val mhField: Field = activityThreadClass.getDeclaredField("mH") mhField.setAccessible(true) val mhHandler = mhField.get(activityThread) as Handler val callbackField: Field = Handler::class.java.getDeclaredField("mCallback") callbackField.setAccessible(true) callbackField.set(mhHandler, Handler.Callback { msg -> if (Build.VERSION.SDK_INT >= 28) { //android P 生命周期全部走这 val EXECUTE_TRANSACTION = 159 if (msg.what === EXECUTE_TRANSACTION) { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishLaunchActivity(msg) handleLifecycleException(throwable) } return@Callback true } return@Callback false } when (msg.what) { LAUNCH_ACTIVITY -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishLaunchActivity(msg) handleLifecycleException(throwable) } return@Callback true } RESUME_ACTIVITY -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishResumeActivity(msg) handleLifecycleException(throwable) } return@Callback true } PAUSE_ACTIVITY_FINISHING -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishPauseActivity(msg) handleLifecycleException(throwable) } return@Callback true } PAUSE_ACTIVITY -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishPauseActivity(msg) handleLifecycleException(throwable) } return@Callback true } STOP_ACTIVITY_HIDE -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { sActivityKiller?.finishStopActivity(msg) handleLifecycleException(throwable) } return@Callback true } DESTROY_ACTIVITY -> { try { mhHandler.handleMessage(msg) } catch (throwable: Throwable) { handleLifecycleException(throwable) } return@Callback true } } false }) } /** * 生命周期异常处理 */ private fun handleLifecycleException(e: Throwable) { Log.e(TAG, "lifecycleException: ", e) reportCustomThrowable(Thread.currentThread(), RuntimeException("Activity生命周期出现异常", e)) // 给个Toast提示 try { UiUtils.showToastSafe("小猿刚刚捕获了一只BUG") } catch (e: Exception) { e.printStackTrace() } } /** * 主线程Looper异常 */ private fun handleLooperException(e: Exception) { // 主线程内发生异常 主动 catch Log.e(TAG, "handleLooperException: ", e) reportCustomThrowable(Thread.currentThread(), e) handleMainThreadException(e) } /** * 本版本不处理 * * view measure layout draw时抛出异常会导致Choreographer挂掉 * 建议直接杀死app。以后的版本会只关闭黑屏的Activity * * @param e */ private fun isChoreographerException(e: Throwable?): Boolean { val elements = e?.stackTrace ?: return false for (i in elements.size - 1 downTo -1 + 1) { if (elements.size - i > 20) { return false } val element = elements[i] if ("android.view.Choreographer" == element.className && "Choreographer.java" == element.fileName && "doFrame" == element.methodName) { //View 绘制流程出的问题 return true } } return false } override fun uncaughtException(t: Thread, e: Throwable) { Log.e(TAG, "uncaughtException: ") if (isDebug) { handleDebugException(t, e) } else { handleReleaseException(t, e) } } /** * 处理 Debug 模式的异常 */ private fun handleDebugException(t: Thread, e: Throwable) { Log.e(TAG, "handleDebugException: $t", e) // 记录本地日志 saveThrowableMessage(Log.getStackTraceString(e)) } /** * 处理 !Debug 模式的异常 */ private fun handleReleaseException(t: Thread, e: Throwable) { Log.e(TAG, "handleReleaseException: $t", e) // 自定义 Bug 上报 reportCustomThrowable(t, e) // 根据情况来处理异常 if (isMainProcess) { // 为主进程 if (Looper.myLooper() == Looper.getMainLooper()) { // 主线程异常处理 handleMainThreadException(e) } else { // 非主线程 Log.e(TAG, "子线程异常: finish.") } } else { // 如果是子进程发生异常 直接殺掉子進程 Log.e(TAG, "子进程异常: killProcess.") android.os.Process.killProcess(android.os.Process.myPid()) } } /** * 主线程异常处理 * * Looper.loop & MainThreadUncaughtException */ private fun handleMainThreadException(e: Throwable) { Log.e(TAG, "handleMainThreadException: ") try { // 主线程 when (e) { is IllegalArgumentException, is IllegalStateException, is IndexOutOfBoundsException, is UnsupportedOperationException, is ArithmeticException, is NumberFormatException, is NullPointerException, is ClassCastException, is AssertionError, is NoSuchElementException -> { Log.e(TAG, "主线程异常: handle finish.") if (isChoreographerException(e)) { UiUtils.showToastSafe("界面刷新出了个小问题") } } else -> { Log.e(TAG, "主线程未知异常:System exit.") // 这里也可以只给个提示,反正不会程序不会奔溃 android.os.Process.killProcess(android.os.Process.myPid()) System.exit(0) } } } catch (e: Exception) { e.printStackTrace() } } private val logFilePath = Environment.getExternalStorageDirectory().toString() + File.separator + "Example" + File.separator + "CrashLog" private fun saveThrowableMessage(errorMessage: String) { if (TextUtils.isEmpty(errorMessage)) { return } val file = File(logFilePath) if (file.exists() && file.isDirectory) { // fall through } else { file.mkdirs() } writeToFile(errorMessage, file) } private val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA) private fun writeToFile(errorMessage: String, file: File) { ThreadPoolManager.getShortPool()?.execute { var outputStream: FileOutputStream? = null try { val timestamp = System.currentTimeMillis() val time = formatter.format(Date()) val fileName = "crash-$time-$timestamp" val inputStream = ByteArrayInputStream(errorMessage.toByteArray()) outputStream = FileOutputStream(File(file, "$fileName.txt")) var len: Int val bytes = ByteArray(1024) while (inputStream.read(bytes).also { len = it } != -1) { outputStream.write(bytes, 0, len) } outputStream.flush() Log.e(TAG, "异常奔溃日志成功写入本地文件:${file.absolutePath}") } catch (e: Exception) { Log.e(TAG, "异常奔溃日志写入本地文件失败: ", e) } finally { if (outputStream != null) { try { outputStream.close() } catch (e: IOException) { // nothing } } } } } /** * 自定义 Bug 上报 */ private fun reportCustomThrowable(t: Thread, e: Throwable) { Log.e(TAG, "reportCustomThrowable: ") try { AliHaAdapter.getInstance().reportCustomError(e) //配置项:自定义错误 } catch (ex: Exception) { Log.e(TAG, "上报自定义异常Error: ", ex) } }} 使用方式: 在 Application 的 onCreate() 方法中, 调用 RrCrashHandler.install(BuildConfig.DEBUG, AppUtils.isMainProgress(this)) 参考链接: https://github.com/android-notes/Cockroach https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md","link":"/blog/2021/11/19/Android%E4%B8%93%E6%A0%8F-JavaCrash%E9%BB%98%E8%AE%A4%E5%A4%84%E7%90%86/"},{"title":"Android-JsBridge实现本地H5混合开发","text":"Android JsBridge 混合开发框架0x01 Java 调用 Js我们知道,native层调用h5,在WebView中,如果java要调用js的方法, 0x0101 loadUrl <4.4Android4.4以前使用WebView.loadUrl("javascript:function()")。 0x0102 evaluateJavascript >4.4Android4.4以后,使用以下方式 123456webView.evaluateJavascript("javascript:function()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { Toast.makeText(MainActivity.this, "onReceiveValue From JS: " + value, Toast.LENGTH_SHORT).show(); }}); 0x02 Js 调用 Javah5层如何调native,有以下几种形式 0x0201 addJavascriptInterface >4.2在4.2之前有安全隐患,JS可以动态获取到整个底层的实例信息,漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface注解。 0x0202 shouldOverrideUrlLoading拦截自定义scheme0x0203 onJsAlert,onJsConfirm,onJsPromptWebChromeClient对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的alert,confirm,prompt,WebChromeClient对象中的三个方法对应的就会被触发,进行拦截处理。 推荐使用onJsPrompt,使用频次最少,支持返回值。 1234567891011override fun onJsPrompt( view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean { val msg = handleMessage(message) result?.confirm("Java 处理之后的 Json 数据:$msg") return true} 0x0204 onConsoleMessage这是Android提供给Js调试在Native代码里面打印日志信息的API,同时这也成了其中一种Js与Native代码通信的方法。在Js代码中调用console.log(‘xxx’)方法。 0x03 自定义通信协议jsbridge://className:callbackId/methodName?json 假设我们需要调用native层的Logger类的log方法,参数是msg,执行完成后js层要有一个回调,那么地址就如下 jsbridge://Logger:callbackId/log?{"msg":"message from js."}","link":"/blog/2020/09/19/Android%E4%B8%93%E6%A0%8F-JsBridge%E5%AE%9E%E7%8E%B0%E6%9C%AC%E5%9C%B0H5%E6%B7%B7%E5%90%88%E5%BC%80%E5%8F%91/"},{"title":"JsBridge 开源库","text":"JsBridge 开源库项目地址:https://github.com/lzyzsd/JsBridge使用参考:https://www.jianshu.com/p/7aea03838f19 0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]解决方案:App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。 1234567891011121314// js-bridge register error// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]// so you have to destroy webView and rebuild one.private void reloadWebView() { if (mWebView != null) { mWebView.destroy(); mWebView = null; } initView(); initWebView(); webView.loadUrl(url);}","link":"/blog/2022/06/08/Android%E4%B8%93%E6%A0%8F-JsBridge%E5%BC%80%E6%BA%90%E5%BA%93/"},{"title":"Android专栏-SmartRefreshLayout","text":"Android专栏-SmartRefreshLayout0x01 加载结束之后底部多出一段空白位置SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。 解决方案: 1smartRefreshLayout.setEnableScrollContentWhenLoaded(false) 0x02 下拉刷新+PAG动画自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169import android.content.Context;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ImageView;import android.widget.RelativeLayout;import android.widget.TextView;import androidx.annotation.ColorInt;import androidx.annotation.NonNull;import com.scwang.smartrefresh.layout.api.RefreshHeader;import com.scwang.smartrefresh.layout.api.RefreshKernel;import com.scwang.smartrefresh.layout.api.RefreshLayout;import com.scwang.smartrefresh.layout.constant.RefreshState;import com.scwang.smartrefresh.layout.constant.SpinnerStyle;import org.libpag.PAGFile;import org.libpag.PAGView;public class CommonRefreshHeader extends RelativeLayout implements RefreshHeader { public static final String DEFAULT_LOADING_FILE = "load_bubble.pag"; protected View mView; protected ImageView sdv_background; private int mFinishDuration = 300; private ViewGroup rootLayout; private PAGView pagView; private TextView toastTv; public CommonRefreshHeader(Context context) { super(context); initView(context); } public CommonRefreshHeader(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public CommonRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } public int getLayoutId() { return R.layout.layout_refresh_head; } protected void initView(Context context) { mView = LayoutInflater.from(context).inflate(getLayoutId(), this); toastTv = mView.findViewById(R.id.tv_toast); rootLayout = mView.findViewById(R.id.rootLayout); sdv_background = mView.findViewById(R.id.sdv_background); PAGFile file = PAGFile.Load(context.getAssets(), DEFAULT_LOADING_FILE); pagView = new PAGView(getContext()); LayoutParams params = new LayoutParams(UiUtils.dip2px(39), UiUtils.dip2px(39)); params.addRule(RelativeLayout.CENTER_IN_PARENT); pagView.setLayoutParams(params); pagView.setFile(file); pagView.setRepeatCount(0); rootLayout.addView(pagView); } public void setMarginTop(int marginTop) { if (mView == null) return; ViewGroup root = mView.findViewById(R.id.rootLayout); if (root != null && root.getLayoutParams() instanceof MarginLayoutParams) { MarginLayoutParams params = (MarginLayoutParams) root.getLayoutParams(); params.topMargin = marginTop; root.setLayoutParams(params); } } @NonNull @Override public View getView() { return mView; } @Override public SpinnerStyle getSpinnerStyle() { return SpinnerStyle.Translate; } @Override public void setPrimaryColors(@ColorInt int... colors) { } @Override public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) { } @Override public void onMoving(boolean b, float v, int i, int i1, int i2) { /** * 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing) * @param isDragging true 手指正在拖动 false 回弹动画 * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight ) * @param offset 下拉的像素偏移量 0 - offset - (footerHeight+maxDragHeight) * @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1) * @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight */ if (pagView != null && !pagView.isPlaying()) { pagView.setProgress(v); pagView.flush(); } } @Override public void onReleased(@NonNull RefreshLayout refreshLayout, int i, int i1) { } @Override public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) { } @Override public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) { LogUtils.e("RefreshHeader", "onStartAnimator"); if (pagView != null) pagView.play(); } @Override public int onFinish(RefreshLayout layout, boolean success) { LogUtils.e("RefreshHeader", "onFinish"); if (pagView != null) pagView.stop(); return mFinishDuration;//延迟500毫秒之后再弹回 } @Override public boolean isSupportHorizontalDrag() { return false; } @Override public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) { switch (newState) { case None: case PullDownToRefresh: case Refreshing: if (pagView != null) pagView.setVisibility(VISIBLE); if (toastTv != null) toastTv.setVisibility(GONE); break; case ReleaseToRefresh: break; case RefreshFinish: if (pagView != null) pagView.setVisibility(GONE); if (toastTv != null) toastTv.setVisibility(VISIBLE); break; } } public void setToastText(String str) { if (StringUtils.isEmpty(str)) return; if (toastTv != null) { toastTv.setText(str); } } public void setFinishDuration(int finishDuration) { this.mFinishDuration = finishDuration; }}","link":"/blog/2021/09/26/Android%E4%B8%93%E6%A0%8F-SmartRefreshLayout/"},{"title":"Android专栏-WebView","text":"Android专栏-WebView0x00 常规WebViewActivity123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231package com.dench.webviewlibimport android.annotation.SuppressLintimport android.graphics.Bitmapimport android.net.http.SslErrorimport android.os.Buildimport android.os.Bundleimport android.text.TextUtilsimport android.util.Logimport android.view.Gravityimport android.webkit.*import android.widget.TextViewimport android.widget.Toastimport androidx.appcompat.app.AppCompatActivityimport androidx.databinding.DataBindingUtilimport com.alibaba.android.arouter.facade.annotation.Autowiredimport com.alibaba.android.arouter.facade.annotation.Routeimport com.alibaba.android.arouter.launcher.ARouterimport com.dench.baselib.provider.WebViewServiceimport com.dench.baselib.utlis.StatusBarHelperimport com.dench.webviewlib.bridge.JsInterfaceimport com.dench.webviewlib.databinding.ActivityWebViewBindingimport kotlinx.android.synthetic.main.activity_web_view.*@Route(path = WebViewService.activityPath)class WebViewActivity : AppCompatActivity() { @Autowired lateinit var title: String @Autowired lateinit var url: String private lateinit var dataViewBinding: ActivityWebViewBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ARouter.getInstance().inject(this) StatusBarHelper.fitSystemBar(this, false) dataViewBinding = DataBindingUtil.setContentView(this, R.layout.activity_web_view) initToolbar() initWebView() } override fun onBackPressed() { if (webView.canGoBack()) { webView.goBack() return } super.onBackPressed() } private fun initToolbar() { dataViewBinding.toolbar.titleTv.text = title val backIv = dataViewBinding.toolbar.leftIv backIv.setImageResource(R.drawable.ic_close) backIv.setOnClickListener {// 关闭 finish() } } @SuppressLint("JavascriptInterface") private fun initWebView() { // webView Settings initWebViewSetting() // init Client initClient() // add Javascript Interface webView.addJavascriptInterface(JsInterface(this), JsInterface.NAME) // register Scroll Listener registerScrollListener() loadUrl() } // webView Settings @SuppressLint("SetJavaScriptEnabled") private fun initWebViewSetting() { //声明WebSettings子类 val webSettings = webView.settings //如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript webSettings.javaScriptEnabled = true webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 //设置自适应屏幕,两者合用 webSettings.useWideViewPort = true //将图片调整到适合webView的大小 webSettings.loadWithOverviewMode = true // 缩放至屏幕的大小 //缩放操作 webSettings.setSupportZoom(true)//支持缩放,默认为true。是下面那个的前提。 webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放 webSettings.displayZoomControls = false //隐藏原生的缩放控件 // 缓存 webSettings.cacheMode = WebSettings.LOAD_DEFAULT //webView缓存策略 webSettings.domStorageEnabled = true webSettings.databaseEnabled = true webSettings.setAppCacheEnabled(true) //其他 webSettings.allowFileAccess = true //设置可以访问文件 webSettings.loadsImagesAutomatically = true //支持自动加载图片 webSettings.defaultTextEncodingName = "utf-8" //设置编码格式 // 在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW } } // init Client private fun initClient() { webView.webViewClient = object : WebViewClient() { // 在网页上的所有加载都经过这个方法 override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? ): Boolean { Log.i(_tag, "shouldOverrideUrlLoading()") request?.url?.let { return when (it.scheme) { "http", "https" -> { // 加载网络html super.shouldOverrideUrlLoading(view, request) } "file", "content" -> { // 加载本地html super.shouldOverrideUrlLoading(view, request) } else -> { // 特殊 scheme 不处理 showToast("$it") true } } } return true } // 加载页面的服务器出现错误时(如404)调用 override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError? ) { Log.i(_tag, "onReceivedError()") super.onReceivedError(view, request, error) } // ssl 证书错误 override fun onReceivedSslError( view: WebView?, handler: SslErrorHandler?, error: SslError? ) { Log.i(_tag, "onReceivedSslError()") handler?.proceed() //表示等待证书响应 // handler?.cancel() //表示挂起连接,为默认方式 // handler?.handleMessage(null) //可做其他处理 } // 开始载入页面调用的。我们可以设定一个loading的页面,告诉用户程序在等待网络响应 override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { Log.i(_tag, "onPageStarted()") Log.d( _tag, "onPageStarted() called with: view = $view, url = $url, favicon = $favicon" ) super.onPageStarted(view, url, favicon) webViewProgress.show() } // 在页面加载结束时调用。我们可以关闭loading 条,切换程序动作 override fun onPageFinished(view: WebView?, url: String?) { Log.i(_tag, "onPageFinished()") super.onPageFinished(view, url) webViewProgress.hide() } // 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次 override fun onLoadResource(view: WebView?, url: String?) { Log.d(_tag, "onLoadResource()") Log.d(_tag, "onLoadResource() called with: view = $view, url = $url") super.onLoadResource(view, url) } } webView.webChromeClient = object : WebChromeClient() { // 加载进度 override fun onProgressChanged(view: WebView?, newProgress: Int) { webViewProgress.progress = newProgress } // Title override fun onReceivedTitle(view: WebView?, title: String?) { if (!TextUtils.isEmpty(title)) { findViewById<TextView>(R.id.titleTv).text = title } } } } // 加载URL private fun loadUrl() { //方式1. 加载一个网页 webView.loadUrl(url) // //方式2:加载apk包中的html页面 // webView.loadUrl("file:///android_asset/test.html") // //方式3:加载手机本地的html页面 // webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html") } // 注册监听 private fun registerScrollListener() { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // webView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> // Log.d(_tag, "scrollY: $scrollY, oldScrollY: $oldScrollY") // } // } } private val _tag = "WebViewActivity" private fun showToast(info: String?) { val toast = Toast.makeText(this, info, Toast.LENGTH_SHORT) toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0) toast.show() }} 0x01 白屏问题情景一:如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript 1234val webSettings = webView.settings//如果访问的页面中要与Javascript交互,则webView必须设置支持JavascriptwebSettings.javaScriptEnabled = truewebSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 情景二:在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置 1234// 在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW} 其他情景: 设置domStorageEnabled 和背景色需要验证,暂时没遇到。 0x02 卡顿问题由于设置了 android:layerType="software"导致的 webview 卡顿。 关于三个layerType属性介绍:https://blog.csdn.net/a345017062/article/details/7478667 解决: 开启 Activity 硬件加速 ``android:hardwareAccelerated=”true”, 并且设置 webview 的android:layerType=”none”`。","link":"/blog/2020/12/26/Android%E4%B8%93%E6%A0%8F-WebView/"},{"title":"adb常用指令","text":"adb常用指令指引官网下载: http://adbshell.com/downloads 命令参考: http://adbshell.com/commands Android官网adb介绍: https://developer.android.google.cn/studio/command-line/adb?hl=zh_cn Mumu adb常用指令指引: https://mumu.163.com/help/20210513/35047_947512.html 0x01 查询和连接设备12345678910## 连接/断开连接网络电视adb connect 170.2.10.20adb disconnect 170.2.10.20## 连接/断开本地模拟器端口adb connect 127.0.0.1:7555adb disconnect 127.0.0.1:7555## 查询已连接设备列表adb devices -l 0x02 adb服务器在某些情况下,您可能需要终止 adb 服务器进程,然后重启才能解决问题。例如,如果 adb 不响应命令,就可能会发生这种情况。 12345678## 停止adb服务adb kill-server## 启动adb服务adb start-server## 以root权限重启adb服务(需要可root设备)adb root 0x03 指定设备操作命令格式:adb -s <serialNumber> command,如:adb -s 127.0.0.1:7555 shell pm list package可以通过 adb devices 获取目标设备的serialNumber 123456$ adb devicesList of devices attachedemulator-5554 deviceemulator-5555 device$ adb -s emulator-5555 install helloWorld.apk 0x04 安装与卸载apk12345## 安装apkadb install C:\\\\xx.apk## 卸载apkadb uninstall C:\\\\xx.apk 0x05 已安装应用列表所有应用包名列表 adb shell pm list packages 第三方应用包名列表 adb shell pm list packages -3 系统应用包名列表 adb shell pm list packages -s 根据某个关键字查找包 adb shell pm list packages |grep tencent 查看包安装位置 adb shell pm list packages -f |grep tencent 0x06 获取设备当前显示应用的包名和Activity123456789101112## 正在运行应用包名和Activity$ adb shell dumpsys window | findstr mCurrentFocusmCurrentFocus=nullmCurrentFocus=Window{fe571d0 u0 com.zhongduomei.rrmj.society/com.rrtv.rrtvtrunk.main.presentation.MainActivity}## 设备当前显示应用的包名和Activity名称$ adb shell dumpsys window w |findstr \\/ |findstr name=mSurface=Surface(name=NavigationBar0)/@0xe825312mSurface=Surface(name=StatusBar)/@0x2a132d5mSurface=Surface(name=com.tencent.qqlive/com.tencent.qqlive.ona.activity.SplashHomeActivity)/@0xc6af35bmSurface=Surface(name=com.android.systemui.ImageWallpaper)/@0x65b6a37 0x07 启动应用adb shell am start -n 应用包名/应用Activity类名 若想查看启动应用耗时,则可使用adb shell am start -W 应用包名/应用Activity类名 0x08 关闭应用adb shell am force-stop 应用包名 0x09 查看应用版本号adb shell dumpsys package 应用包名 | findstr version 0x10 清理应用数据adb shell pm clear 应用包名 0x11 模拟输入按键输入 adb shell input keyevent 键值 如:adb shell input keyevent 3表示按下HOME键,其他键值对应键位可网上搜索 字符输入 adb shell input text 字符 如:adb shell input text test则表示输入了test字符串 ps:字符不支持中文 鼠标点击 adb shell input tap X Y X Y分别为当前屏幕下的x和y轴坐标值 鼠标滑动 adb shell input swipe X1 Y1 X2 Y2 X1 Y1 和X2 Y2分别为滑动起始点的坐标 0x12 从电脑上传文件至模拟器adb push myfile.txt /sdcard/myfile.txt 0x13 从模拟器复制文件至电脑adb pull /data/test.apk D:\\ 0x14 截图将模拟器当前显示截图 adb shell screencap /data/screen.png 将截图文件下载至电脑 adb pull /data/screen.png C:\\ 0x15 录制视频开始录制 adb shell screenrecord /data/test.mp4 结束录制 可按CTRL+C结束录制 导出视频文件 adb pull /data/test.mp4 C:\\ 0x16 查看设备信息123456789101112131415161718192021222324## 设备型号$ adb shell getprop ro.product.model## 设备品牌$ adb shell getprop ro.product.brand## 设备处理器型号$ adb shell getprop ro.product.board## 设备安卓版本号$ adb shell getprop ro.build.version.release## 设备abi$ adb shell getprop ro.product.cpu.abi## 设备cpu信息$ adb shell cat /proc/cupinfo## 设备引擎渲染模式$ adb shell dumpsys SurfaceFlinger|findstr "GLES" ## grep product关键字(设备支持的abi列表) $ adb shell getprop |grep product 0x17 管理设备 命令 功能 adb get-state 判断设备状态 adb devices 显示连接到计算机的设备 adb get-serialno 获取设备的序列号 adb reboot 重启设备 adb reboot bootloader 重启设备进入fastboot模式 adb reboot recovery 重启设备进入recovery模式","link":"/blog/2021/11/25/Android%E4%B8%93%E6%A0%8F-adb/"},{"title":"自定义Notification遇到的坑","text":"自定义Notification 实现: 123456789101112131415161718192021222324252627282930313233343536373839// RemoteViews for notificationprivate var rv: RemoteViews? = nullprivate var rvExpanded: RemoteViews? = nullprivate fun customNotification(process: Int) { // custom RemoteViews if (rv == null) rv = RemoteViews(packageName, R.layout.notification_small) rv?.setTextViewText(R.id.notification_title, "这是一个小标题") if (rvExpanded == null) rvExpanded = RemoteViews(packageName, R.layout.notification_large) rvExpanded?.setTextViewText(R.id.large_notification_title, "这是一个大标题,支持很多的内容: $process%") // PendingIntent val intentNotification = Intent(this, PlayMusicActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP } val pendIntent = PendingIntent.getActivity( this, 0, intentNotification, PendingIntent.FLAG_UPDATE_CURRENT ) // build notification val customNotification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) .setSmallIcon(R.drawable.bravo) // small Icon .setStyle(NotificationCompat.DecoratedCustomViewStyle()) // 自定义contentView .setCustomContentView(rv!!) .setContent(rvExpanded!!) .setCustomBigContentView(rvExpanded!!) .setCustomHeadsUpContentView(rvExpanded!!) .setOngoing(true) // 一直显示 .setAutoCancel(false) // 点击后消失 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏显示,需要配合权限设置 .setPriority(NotificationCompat.PRIORITY_HIGH) // Priority .setOnlyAlertOnce(true) // 声音,震动,仅弹出一次 .setContentIntent(pendIntent) .build() startForeground(NOTIFICATION_ID, customNotification)} 1.使用 NotificationCompat 兼容各个版本差异性 2.RemoteViews 布局文件不支持 constraintlayout ,切记 3.在SDK 26之后必须要绑定Channel,所以通知要先创建Channel 123456789101112131415161718192021222324252627282930fun checkAndCreateChannel( context: Context, channelId: String, channelName: String, desc: String = channelName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager try { // 查找分组 val nc = notificationManager.getNotificationChannel(channelId) Log.d("ChannelHelper", "${nc.id} Notification Channel exist.") } catch (e: Exception) { Log.d("ChannelHelper", "empty channel need create.") // 创建分组 val mChannel = NotificationChannel( channelId, channelName, NotificationManager.IMPORTANCE_HIGH ).apply { description = desc enableLights(true) enableVibration(true) } notificationManager.createNotificationChannel(mChannel) } }}","link":"/blog/2020/09/23/Android%E4%B8%93%E6%A0%8F-%E9%80%9A%E7%9F%A5%E6%A0%8FNotification/"},{"title":"Android之FileProvider详解","text":"Android之FileProvider详解原文地址:https://juejin.cn/post/7009204672225345549 Android 7.0之前,文件的Uri以 file:///形式提供给其他app访问。 Android 7.0之后,分享文件的Uri发生了变化。为了安全起见, file:///形式的Uri不能正常访问。官方提供了 FileProvider,FileProvider生成的Uri会以 content://的形式分享给其他app使用。 在7.0以前,为了访问 file:///形式的Uri,我们必须修改文件的权限。修改后的权限对所有app都是有效的,这样的行为是不安全的。 content://形式的Uri让Android的文件系统更安全,对于分享的文件,接收方app只拥有临时的权限,减少了我们app内部的文件被其他app恶意操作的行为。 0x01 创建FileProvider在manifest文件 <application></application>标签中添加pvodier标签,配置如下。 12345678910111213<manifest> <application> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.FileProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application></manifest> android:name指定Provider的类名,使用官方提供的androidx.core.content.FileProvider。 android:authorities相当于一个用于认证的暗号,在分享文件生成Uri时,会通过它的值生成对应的Uri。。值是一个域名,一般格式为 <包名>.fileprovider</包名>。 android:exported设置为false,FileProvider不需要公开。 android:grantUriPermissions设置为true,这样就能授权接收端的app临时访问权限了。 0x02 配置共享目录file_paths.xml在res/xml中创建一个资源文件(如果xml目录不存在,先创建),名字随便(一般叫file_paths.xml)。 1234<paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="my_logs" path="logs/"/> ...</paths> <paths></paths>必须有1个或多个子标签,每个子标签代表要请求的私有文件目录。不同的子标签代表不同的目录类型。 在 <provider></provider>标签中添加 <meta-data></meta-data>子标签。 设置 <meta-data></meta-data>的属性 android:name值为 android.support.FILE_PROVIDER_PATHS,属性 android:resouce的值为刚才我们创建的path文件名。 配置paths<paths></paths>的每个子标签必须有 path属性,代表content Uris的路径。 name不需要和path保持一样,只是一个名称。 子标签有以下几种。 files-path12<files-path name="my_files" path="path" /> 代表内部存储的files目录,与 Context.getFilesDir()获取的路径对应。 最终生成的Uri格式为:authorities/pathname/filename 示例: 12content: cache-path12<cache-path name="name" path="path" /> 代表内部存储的cache目录,与 Context.getCacheDir()获取的路径对应。 external-path12<external-path name="name" path="path" /> 代表外部存储(sdcard)的cache目录,与 Environment.getExternalStorageDirectory()获取的路径对应。 external-files-path12<external-files-path name="name" path="path" /> 代表app的外部存储的根目录,与 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)获取的路径对应。 external-cache-path12<external-cache-path name="name" path="path" /> 代表app外部缓存区域的根目录,与 Context.getExternalCacheDir()获取的路径对应。 external-media-path12<external-media-path name="name" path="path" /> 代表app外部存储媒体区域的根目录,与 Context.getExternalMediaDirs()获取的路径对应。 注意: 这个目录只在API 21(也就是Android 5.0)以上的系统上才存在。 0x03 生成Content Uri文件为了让其他app使用Content Uri,我们的app必须提前生成Uri。 1234567File file = new File(Context.getFilesDir(), "my_log");Uri uri;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { uri = FileProvider.getUriForFile(getContext(), this.getPackageName() + ".FileProvider", file);} else { uri = Uri.fromFile(file);} 这里注意获取目录,在配置paths时我们讲了,paths的子标签必须和获取目录的代码保持对应。这里我们用的是 Context.getFilesDir(),所以paths文件中必须包含 files-path子标签,不然别的app获取uri时会出现异常。 最终生成Uri是使用的 FileProvider.getUriForFile()。第一个参数就是 provider中设置的 authorities属性值。 0x04 Content Uri的几种使用场景为邮箱app分享附件文件1intent.putExtra(Intent.EXTRA_STREAM, contentUri); 其他分享使用 Intent.setDate或 Intent.setClipData()。 1intent.setClipDataClipData.newRawUri("", contentUri) 最后使用 startActivity(intent)启动分享操作。 0x05 授权临时权限分享一般只有这读取和写入2种权限,根据需要传入 Intent.addFlags()中。 12Intent intent = new Intent(Intent.ACTION_SEND);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);","link":"/blog/2023/08/07/Android%E4%B9%8BFileProvider%E8%AF%A6%E8%A7%A3/"},{"title":"微信AndResGuard资源混淆工具","text":"微信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 资源混淆配置 Project根目录的build.gradle中,添加插件的依赖 12345buildscript { dependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21' }} app模块的build.gradle中添加Res相关配置 1234567891011121314151617181920212223242526272829303132333435363738plugins { 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文件中添加 12res 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.md:https://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md","link":"/blog/2022/11/26/Android%E4%B8%93%E6%A0%8F-%E5%BE%AE%E4%BF%A1AndResGuard%E8%B5%84%E6%BA%90%E6%B7%B7%E6%B7%86%E5%B7%A5%E5%85%B7/"},{"title":"Android从现有的项目创建一个皮应用","text":"Android从现有的项目创建一个皮应用0x01 Copy一个新项目 clean原项目,然后直接Copy原项目所有文件,等待完成 根据新项目重命名新文件夹名称 删除原项目的 .idea .git .gitlab 等文件夹,.gradle 可以不删 用编辑器打开 settings.gradle,修改项目名称 rootProject.name = "xxx" 0x02 重命名项目包名这一步是要将包名从 com.sample1.android 重命名为 com.sample2.android 用AndroidStudio打开新项目,去掉 Compat Middle Packages 前面的勾 选择项目的 sample1 包名,Shift + F6重命名 一定选择 Rename Package 等待完成,项目大时间就比较长 修改 .aidl 文件的包名路径和import的路径 修改根目录下的 build.gradle 的 applicationId "com.sample2.android",并 sync ps: 不清楚为啥Kotlin的扩展函数还需要重新手动导入 0x03 推到代码库到上面这一步,不出意外的话已经可以编译成功并且跑起来了。编译成功之后可以先推送本地项目到代码库。 远程代码库新建项目,获取新项目git地址,然后执行下面操作。 123456$ 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包路径的痛苦历程。","link":"/blog/2022/09/08/Android%E4%BB%8E%E7%8E%B0%E6%9C%89%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%9A%AE%E5%BA%94%E7%94%A8/"},{"title":"Android 分享功能","text":"Android 分享功能友盟分享SDKhttps://developer.umeng.com/docs 友盟分享,QQ和QQ空间分享成功了,却总是回调分享取消qqzone_id_value 配置跟当前应用对应不上,PlatformConfig.setQQZone(qqzone_id_value, qqzone_secret_id_value)","link":"/blog/2022/06/23/Android%E5%88%86%E4%BA%AB%E5%8A%9F%E8%83%BD/"},{"title":"Android动态权限申请","text":"Android动态权限申请0x01 介绍由于 Android 动态权限申请是一个交互比较复杂的模块,整个申请的流程也比较长,所以,写了一个工具来封装了一个,也具体的实现了一个流程。 由于每个App的Ui风格不一致,所以没有把Toast和弹框封装进工具,等后期有好的想法再优化。 0x02 动态权限申请流程 检查授权状态 申请权限 处理权限申请结果 当用户‘拒绝且不再询问’,引导去手机设置 检查手机设置后的权限申请结果 0x03 使用说明主要封装类 PermissionManager 根据业务需要用到安卓定义的高危权限,需要去动态申请权权限。通常是在Activity 和 Fragment 组件中发起。 1 检查权限授权状态 123456if (PermissionManager.hasPermissions(this, Manifest.permission.READ_PHONE_STATE)) { // after permission afterPermission()} else { showRequestPermissionDialog("申请手机权限的原因是因为我需要", Manifest.permission.READ_PHONE_STATE)} 2 申请权限,根据合规化通常需要先弹框 12345678910111213141516private 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 这个回调接口。 123456789101112131415161718192021222324252627override 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 当用户‘拒绝且不再询问’,引导去手机设置 123456789101112131415161718private 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 检查手机设置后的权限申请结果 1234567override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_CODE_FOR_PERMISSION_SETTINGS) { afterPermission() } else { super.onActivityResult(requestCode, resultCode, data) }} 0x04 特别推荐 PermissionXPermissionX 中文文档 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.","link":"/blog/2022/10/10/Android%E5%8A%A8%E6%80%81%E6%9D%83%E9%99%90%E7%94%B3%E8%AF%B7@2022/"},{"title":"Android深色模式适配指南","text":"Android 深色模式(夜间模式)适配指南Android 10 (API 级别 29) 及更高版本中提供深色主题背景。深色主题背景具有诸多优势: 可大幅减少耗电量(具体取决于设备的屏幕技术)。 为弱视以及对强光敏感的用户提高可视性。 让所有人都可以在光线较暗的环境中更轻松地使用设备。 0x01 DayNight 主题适配深色模式将应用的主题背景(通常可在 res/values/styles.xml 中找到)设置为继承 DayNight 主题背景。 1<style name="AppTheme" parent="Theme.AppCompat.DayNight"> 用到的资源和颜色需要在 -night 目录中重新配置一份。 在暗黑模式下,系统会优先从 -night 后缀的目录下找到对应的资源配置。 0x02 Force Dark 自动适配深色模式如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。 Force Dark 应用需要满足以下三个条件 使用系统及 AndroidX 提供的浅色主题背景(例如 Theme.Material.Light) 在其主题背景中设置 android:forceDarkAllowed="true" 手机系统启用深色模式,Android 10 (API 级别 29)以上 如果您的应用使用深色主题(例如 Theme.Material),或者继承自 DayNight 主题背景,则系统不会应用 Force Dark。 在特定 View 上停用 Force Dark,可以通过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed() 。 0x03 动态设置深色模式如要切换主题背景,请调用 AppCompatDelegate.setDefaultNightMode()。 每个选项直接映射到以下某个 AppCompat.DayNight 模式: 浅色 - MODE_NIGHT_NO 深色 - MODE_NIGHT_YES 由省电模式设置 - MODE_NIGHT_AUTO_BATTERY ( Android 9 或更低版本的设备上) 系统默认 - MODE_NIGHT_FOLLOW_SYSTEM ( Android 10 (API 级别 29) 及更高版本上) 注意:从 AppCompat v1.1.0 开始,setDefaultNightMode() 会自动重新创建任何已启动的 Activity。 0x04 切换深色模式不重建 Activity当应用的主题背景发生更改(无论是通过系统设置还是 AppCompat)时,会触发 uiMode 配置变更。这意味着系统会自动重新创建 Activity。 在某些情况下,您可能希望应用处理配置变更。例如,您可能希望延迟配置变更时间,因为设备正在播放视频。 应用可以声明,每个 Activity 都可以处理 uiMode 配置变更,以自行处理深色主题背景的实现: 123<activity android:name=".MyActivity" android:configChanges="uiMode" /> 当某个 Activity 声明它会处理配置变更时,系统会在出现主题背景变更时调用该 Activity 的 onConfigurationChanged() 方法。 0x05 判断是否是深色模式0x0501 判断当前 APP 是否是深色模式如要检查当前采用的是哪种主题背景,应用可以运行如下代码: 12345val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASKwhen (currentNightMode) { Configuration.UI_MODE_NIGHT_NO -> {} // Night mode is not active, we're using the light theme Configuration.UI_MODE_NIGHT_YES -> {} // Night mode is active, we're using dark theme} 0x0502 判断当前手机系统是否是深色模式如果要判断当前手机系统是否是深色模式,可以使用以下代码: 1234private fun isSystemNightMode(activity: Activity): Boolean { val uiModeManager = activity.getSystemService(Context.UI_MODE_SERVICE) return if (uiModeManager is UiModeManager) uiModeManager.nightMode == UiModeManager.MODE_NIGHT_YES else false} 如何在 APP 内判断手机是否切换了系统深色模式? 前提是当前 APP 没有使用 android:configChanges="uiMode" 方式。由于直接通过手机系统快捷设置切换深色模式时,会触发 activity 的 recreate()方法,导致 APP 的 activity 重建,但是在 activity 的 onPause()方法或者 APP 切换到后台时,获取到的深色模式状态已经是切换之后的了。所以,如果要判断 APP 使用过程中,系统是否切换深色模式,目前使用的方式是在进入 activity 时就记录一下当前的系统深色模式状态,然后,onStop 方法去检查系统的设置是否改变,并且把是否切换的值缓存在 APP 全局缓存(注意不能是 Activity 中)里。然后在 App 切换到前台的时候,再去获取缓存中的值,来判断上一次切换到后台时是否是因为系统切换了深色模式。 0x06 老项目深色模式适配指南 compileSdkVersion 升级到 29 以上 使用 Force Dark 自动适配深色模式 针对有问题布局建立-night资源文件夹,配置对应的 color,drawable 和 layout 等 APP 中的关键页面使用 DayNight 主题适配,以追求极致的 UI 体验 特别感谢官网深色主题背景: https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme?hl=zh-cn","link":"/blog/2023/12/15/Android%E6%B7%B1%E8%89%B2%E6%A8%A1%E5%BC%8F%E9%80%82%E9%85%8D%E6%8C%87%E5%8D%97/"},{"title":"Android获取文件MD5","text":"Android获取文件MD5通过 java.security 包下的MessageDigest工具,可以简单快捷的直接计算出文件的MD5值 12345678910111213141516171819fun getFileMd5(path: String?): String? { if (path == null || path.isEmpty()) { return "" } try { val messagedigest = MessageDigest.getInstance("MD5") val buf = ByteArray(4096) var n: Int val fis = FileInputStream(path) while (fis.read(buf, 0, 4096).also { n = it } > 0) { messagedigest.update(buf, 0, n) } fis.close() return HexUtils.toHexString(messagedigest.digest()) } catch (e: Exception) { e.printStackTrace() } return ""} 12345678910111213private static char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};static String toHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(2 * bytes.length); for (int l = 0; l < bytes.length; l++) { char c0 = hexDigits[(bytes[l] & 0xf0) >> 4]; char c1 = hexDigits[bytes[l] & 0xf]; sb.append(c0); sb.append(c1); } return sb.toString();}","link":"/blog/2023/10/20/Android%E8%8E%B7%E5%8F%96%E6%96%87%E4%BB%B6MD5/"},{"title":"Android首页灰色实现方案","text":"Activity设置灰色使用ColorMatrix设置灰度 1234567private 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 12345678override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(...) // 需要在 setContentView 之后 setGrayPaint(window.decorView)} 需要特殊处理的控件 弹框 WebView SurfaceView 这些控件由于不是跟activity公用一个window,需要各自单独处理灰度Paint。调用 setGrayPaint(view) 即可。 相关资源Android实现设置灰白模式效果","link":"/blog/2022/12/01/Android%E9%A6%96%E9%A1%B5%E7%81%B0%E8%89%B2%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%A1%88/"},{"title":"BottomFragment","text":"底部弹出控件 - Fragment 实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667import android.os.Bundleimport android.util.Logimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.fragment.app.Fragmentimport androidx.fragment.app.FragmentManagerimport com.dench.baselib.Rimport com.dench.baselib.databinding.FragmentBottomBindingclass BottomFragment : Fragment() { companion object { fun start(fm: FragmentManager, fragment: Fragment): BottomFragment { val bottomFragment = BottomFragment().apply { setFragment(fragment) } fm.beginTransaction() .setCustomAnimations( R.anim.fragment_bottom_enter, 0, 0, R.anim.fragment_bottom_exit ) .add(android.R.id.content, bottomFragment) .addToBackStack(null) .commitAllowingStateLoss() return bottomFragment } } private lateinit var fragment: Fragment private fun setFragment(fragment: Fragment) { this.fragment = fragment } private lateinit var binding: FragmentBottomBinding private val TAG = "BottomFragment" override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentBottomBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.run { binding.bottomFragmentRl.setOnClickListener(View.OnClickListener { Log.d(TAG, "bottom root view click.") dismissSelf() }) /** add fragment */ childFragmentManager.beginTransaction() .add(R.id.container, fragment) .commitAllowingStateLoss() } } private fun dismissSelf() { parentFragmentManager.popBackStack() }}","link":"/blog/2021/05/19/BottomFragment/"},{"title":"Chrome Shortcuts(mac)","text":"Shortcuts Action command+N 新的窗口 command+shift+N 新的无痕窗口 command+T 新的标签页 command+option+right 标签页切换(向右) command+option+left 标签页切换(向左) command+W 关闭当前标签页 control+Enter 在地址栏中的内容添加”www.”和”.com”然后打开 command+D 将当前网页加入书签 command+R 刷新 command+Shift+R 刷新(忽略缓存) command 和 + 放大 command 和 - 缩小 command+option+L 打开下载内容 command+option+B 打开书签管理","link":"/blog/2020/02/09/Chrome%E5%BF%AB%E6%8D%B7%E9%94%AE/"},{"title":"DataBinding踩坑指南","text":"0x01 ViewBinding1.使用 View Binding 先要在Module 的 build.gradle 文件注册 123456android { ... buildFeatures { viewBinding true }} 2.会根据布局文件,编译之后自动生成对应的Binding class,可以在Activity 和 Fragment 直接调用 inflate 使用 12345678910111213141516171819private var _binding: ResultProfileBinding? = null// This property is only valid between onCreateView and// onDestroyView.private val binding get() = _binding!!override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _binding = ResultProfileBinding.inflate(inflater, container, false) val view = binding.root return view}override fun onDestroyView() { super.onDestroyView() _binding = null} 0x02 DataBinding1.在 build.gradle 文件中开启lib 123456android { ... buildFeatures { dataBinding true }} 2.布局文件start with a root tag of layout followed by a data element 123456789<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewmodel" type="com.myapp.data.ViewModel" /> </data> <ConstraintLayout... /> <!-- UI layout's root element --></layout> 3.在Activity中使用 12345678910override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()) // or val binding: ActivityMainBinding = DataBindingUtil.setContentView( this, R.layout.activity_main) binding.user = User("Test", "User")} 在 Fragment, ListView, or RecyclerView adapter, you may prefer to use the inflate() 123val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)// orval listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false) 0x03 view binding 和 data binding 比较View binding and data binding both generate binding classes that you can use to reference views directly. However, view binding is intended to handle simpler use cases and provides the following benefits over data binding: Faster compilation: View binding requires no annotation processing, so compile times are faster. Ease of use: View binding does not require specially-tagged XML layout files, so it is faster to adopt in your apps. Once you enable view binding in a module, it applies to all of that module’s layouts automatically. Conversely, view binding has the following limitations compared to data binding: View binding doesn’t support layout variables or layout expressions, so it can’t be used to declare dynamic UI content straight from XML layout files. View binding doesn’t support two-way data binding. Because of these considerations, it is best in some cases to use both view binding and data binding in a project. You can use data binding in layouts that require advanced features and use view binding in layouts that do not. 0x04 遇到的坑 等标签,如果使用databinding ,子布局xml的 root tag 依旧需要layout 标签嵌套 data 标签。否者编译报错,找不到对应的属性","link":"/blog/2020/09/26/DataBinding%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97/"},{"title":"ExoPlayer简易播放器","text":"ExoPlayer简易播放器一个简单的基于ExoPlayer的播放器。ExoPlayer官网:https://exoplayer.dev/使用ExoPlayer版本:2.18.2 实现功能:通过url播放视频,简易自定义controller,监听播放器状态变化,首帧时间打印,错误信息打印。 如果需要深层次的UI定制,建议不要用exo_ui库下面的布局,用SurfaceView和TextureView完全自定义。具体代码如下: 1.播放器Fragment123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129import android.os.Bundleimport android.util.Logimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.fragment.app.Fragmentimport com.google.android.exoplayer2.ExoPlayerimport com.google.android.exoplayer2.MediaItemimport com.google.android.exoplayer2.PlaybackExceptionimport com.google.android.exoplayer2.Playerimport com.google.android.exoplayer2.Player.Listenerimport com.google.android.exoplayer2.ui.StyledPlayerViewclass ExoPlayerFragment : Fragment() { companion object { fun newInstance(url: String): ExoPlayerFragment { val args = Bundle() args.putString(EXO_URI, url) val fragment = ExoPlayerFragment() fragment.arguments = args return fragment } private const val TAG = "ExoPlayerFragment" private const val EXO_URI = "EXO_URI" } private var videoUri: String? = null private var player: ExoPlayer? = null private val listener = object : Listener { override fun onEvents(player: Player, events: Player.Events) { super.onEvents(player, events) Log.d(TAG, "onEvents->${events}:") } override fun onPlaybackStateChanged(playbackState: Int) { val stateString: String = when (playbackState) { ExoPlayer.STATE_IDLE -> "ExoPlayer.STATE_IDLE" ExoPlayer.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING" ExoPlayer.STATE_READY -> "ExoPlayer.STATE_READY" ExoPlayer.STATE_ENDED -> "ExoPlayer.STATE_ENDED" else -> "UNKNOWN_STATE" } Log.d(TAG, "onPlaybackStateChanged: state=$stateString") super.onPlaybackStateChanged(playbackState) printPlayerTimeLine("onPlaybackStateChanged:") } override fun onRenderedFirstFrame() { super.onRenderedFirstFrame() printPlayerTimeLine("onRenderedFirstFrame:") } override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) printPlayerTimeLine("onPlayerError:") error.printStackTrace() } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { videoUri = arguments?.getString(EXO_URI) val root = inflater.inflate(R.layout.exo_player_fragment, container, false) val playerView = root.findViewById<StyledPlayerView>(R.id.styled_player_view) initPlayerView(playerView) return root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) startPlay() } private fun initPlayerView(playerView: StyledPlayerView) { Log.d(TAG, "initPlayerView: ") player = ExoPlayer.Builder(requireContext()).build() player?.addListener(listener) // Bind the player to the view. playerView.player = player // back playerView.findViewById<View>(R.id.back).setOnClickListener { fragmentManager?.popBackStack() val ft = fragmentManager?.beginTransaction() ft?.remove(this@ExoPlayerFragment) ft?.commitAllowingStateLoss() } } private var prepareTime: Long = 0L private fun printPlayerTimeLine(method: String) { val c = System.currentTimeMillis() val duration = if (prepareTime == 0L) 0 else c - prepareTime prepareTime = c Log.i(TAG, "printPlayerTimeLine: method=$method, duration=$duration, uri=${videoUri}") } private fun startPlay() { Log.d(TAG, "startPlay: ") videoUri?.let { // Build the media item. val mediaItem: MediaItem = MediaItem.fromUri(it) // Set the media item to be played. player?.setMediaItem(mediaItem) // Prepare the player. player?.prepare() printPlayerTimeLine("player->prepare:") // Start the playback. player?.play()// calPlayerTimeLine("player->play:") } } override fun onDestroy() { player?.release() super.onDestroy() }} 2.播放器布局exo_player_fragment.xml 12345678910111213141516171819<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" android:clickable="true"> <com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/styled_player_view" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true" app:animation_enabled="false" app:controller_layout_id="@layout/custom_player_control_view" app:use_controller="true" /></FrameLayout> 3.自定义的controller布局custom_player_control_view.xml 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="bottom" android:background="#00000000" android:layoutDirection="ltr" android:orientation="vertical" tools:targetApi="28"> <ImageView android:id="@+id/back" android:layout_width="42dp" android:layout_height="42dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:background="#33000000" android:scaleType="centerInside" android:src="@mipmap/back" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <LinearLayout android:id="@id/exo_bottom_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#33000000" android:gravity="center_vertical" android:orientation="horizontal" android:paddingTop="4dp" android:paddingBottom="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"> <ImageButton android:id="@id/exo_prev" style="@style/ExoStyledControls.Button.Center.Previous" android:padding="8dp" /> <ImageButton android:id="@id/exo_play_pause" style="@style/ExoStyledControls.Button.Center.PlayPause" android:padding="8dp" /> <ImageButton android:id="@id/exo_next" style="@style/ExoStyledControls.Button.Center.Next" android:padding="8dp" /> <TextView android:id="@id/exo_position" android:layout_width="wrap_content" android:layout_height="wrap_content" android:includeFontPadding="false" android:paddingLeft="4dp" android:paddingRight="4dp" android:textColor="#FFBEBEBE" android:textSize="14sp" android:textStyle="bold" /> <com.google.android.exoplayer2.ui.DefaultTimeBar android:id="@id/exo_progress" android:layout_width="0dp" android:layout_height="26dp" android:layout_weight="1" /> <TextView android:id="@id/exo_duration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:includeFontPadding="false" android:paddingLeft="4dp" android:paddingRight="4dp" android:textColor="#FFBEBEBE" android:textSize="14sp" android:textStyle="bold" /> </LinearLayout></androidx.constraintlayout.widget.ConstraintLayout>","link":"/blog/2023/03/16/ExoPlayer%E7%AE%80%E6%98%93%E6%92%AD%E6%94%BE%E5%99%A8/"},{"title":"Git手册","text":"Git Hand BookGit 官方站点: http://git-scm.com/download 0x01 全局 Git 设置123456789101112## 用户名和邮箱$ git config --global user.name "git"$ git config --global user.email "[email protected]"## 避免每次git操作都需要输入用户名密码$ git config --global credential.helper store# 取消ssl认证$ git config --global http.sslVerify false## 查看当前配置$ git config --list 0x02 创建 Git 仓库1 从远程仓库克隆到本地123$ git clone git://github.com/schacon/grit.git$ git clone git://github.com/schacon/grit.git mygrit # local path$ git clone git://github.com/schacon/grit.git mygrit --recursive # 递归拉取子module 2 Create a new repository123456$ echo "Git Hand Book" >> README.md$ git init$ git add README.md$ git commit -m "add README"$ git remote add origin git://github.com/schacon/grit.git$ git push -u origin master 3 Existing folder123456$ cd existing_folder$ git init$ git remote add origin git://github.com/schacon/grit.git$ git add .$ git commit$ git push -u origin master 4 Existing Git repository1234$ cd existing_repo$ git remote add origin git://github.com/schacon/grit.git$ git push -u origin --all$ git push -u origin --tags 0x03 Git 分支1 查看分支12$ git branch -a # 查看所有分支$ git branch -v # 查看各个分支对应的版本 2 切换分支123$ git branch iss53 # 新建$ git checkout iss53 # 切换$ git checkout -b hotfix # 新建并切换 3 创建分支3.1 创建本地分支,远程分支,并关联 1234$ git checkout -b serverfix origin/serverfix # 新建本地开发分支$ git push origin serverfix # 推送本地分支到远程$ git branch --set-upstream-to=origin/serverfix # 设置关联远程分支$ git branch -a # 查看分支 3.2 创建独立分支 12$ git checkout --orphan <branch-name> # 创建独立分支(但是暂存了所有原分支的文件)$ git rm -rf . # 删除原项目分支的所有历史和文件 4 删除分支4.1 删除本地分支 1$ git branch -D develop # 删除本地develop分支 4.2 删除远程分支 1$ git push origin :develop # 删除远程develop分支 4.3 远程分支已经删除,本地通过 git branch -a 还能看到 12$ git remote show origin # 可以查看remote地址,远程分支,还有本地分支与之相对应关系等信息。$ git remote prune origin # 删除本地那些远程仓库不存在的分支 5 合并分支5.1 方式一 merge12$ git checkout master # 切换到 master$ git merge develop # 合并devlop分支 merge 遇到冲突,先解决冲突,然后 12$ git add <filename> # 添加已经解决冲突的文件$ git commit # 此处不需要 -m 标签 5.2 方式二 rebase12345$ git checkout develop # 切换到 develop$ git rebase master # 变基到 master## 以上两条命令等价于下面这条$ git rebase master develop # git rebase [base-branch] [topic-branch] 然后回到 master 分支进行快速合并 12$ git checkout master$ git merge develop # 合并devlop分支 rebase 遇到冲突,先解决冲突,然后 12$ git add <filename> # 添加已解决冲突的文件$ git rebase --continue 5.3 Git rebase 高级用法 5.3.1 解除特殊情境下两个分支的依赖关系 12345 H---I---J topicB / E---F---G topicA /A---B---C---D master 1$ git rebase --onto master topicA topicB 12345 H'--I'--J' topicB / | E---F---G topicA |/A---B---C---D master 5.3.2 删除部分提交 1E---F---G---H---I---J topicA 1git rebase --onto topicA~5 topicA~3 topicA 1E---H'---I'---J' topicA 5.3.3 可交互式变基 交互式变基意味着您有机会编辑被重基的提交。 您可以重新排序提交,并可以删除它们(清除坏的或不需要的补丁) 12345$ git rebase -i master # edit the current branch commit before merge into master$ git rebase -i <after-this-commit> # Start it with the last commit you want to retain$ git rebase -i HEAD~5 # edit the last 5 commits 然后根据 vim 提示操作,最后:wq保存退出就可以执行当前操作 0x04 Git remote 使用123456789101112$ git remote add origin git://github.com/paulboone/ticgit.git # 添加远程库$ git remote set-url origin <newurl> # 替换远程库地址$ git remote # 查看远程库简称$ git remote -v # 查看所有远程库$ git remote show origin # 查看origin仓库信息$ git fetch [remote-name]$ git pull # git fetch && git merge$ git push origin master$ git remote rename origin paul # 重命名$ git remote rm origin # 删除 0x05 Tags 标签1 常用标签管理命令12345$ git tag$ git tag v1.4-lw # 轻量级标签$ git tag -a v1.0 21f1f43 -m "my version 1.0" # 含附注标签(推荐)$ git show v1.0$ git push origin --tags # push 所有本地标签 2 仅使用远程仓库 tags1$ git tag -l | xargs git tag -d && git fetch -t # 使用远程仓库 tags 并且删除本地 tags 0x06 子模块123$ git submodule add http://framework.git framework$ git clone project.git project3 --recursive$ git submodule update --init --recursive 0x07 Git Stashing12345$ git stash$ git stash list$ git stash apply 0$ git stash pop$ git stash clear 0x08 Git 经常遇到的问题1 git pull 时遇到 error: cannot lock ref ‘xxx’git pull 时经常遇到 error: cannot lock ref 'refs/remotes/origin/dev': is at xxx but expected xxx 问题原因: 在 git push 的时候使用了 git push –force,导致远端分支被覆盖,导致你本地的 refs 与远端不一致。 删除掉这个远端分支又重新新建了这个分支也会出现同样的问题。 12$ git update-ref -d refs/remotes/origin/dev$ git pull -p 2 回退到指定版本12$ git reset --hard e418f25$ git push origin HEAD --force 3 撤销操作12$ git reset HEAD benchmarks.rb # 取消已经暂存的文件(本地修改还在)$ git checkout -- benchmarks.rb # 还原文件的修改(删除修改) 4 修补上一次提交遗漏(push 前使用)123$ git commit -m 'initial commit'$ git add forgotten_file$ git commit --amend 5 图形化日志1$ git log --oneline --decorate --graph 0x09 Git Flow 0x99 .gitignore 文件123456# 此为注释 – 将被 Git 忽略*.a # 忽略所有 .a 结尾的文件!lib.a # 但 lib.a 除外/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODObuild/ # 忽略 build/ 目录下的所有文件doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt","link":"/blog/2020/02/08/Git%E6%89%8B%E5%86%8C/"},{"title":"Gradle常用命令","text":"Gradle 常用命令0x01 快速构建命令1234567891011121314151617181920212223242526272829# 查看构建版本./gradlew -v# 清除build文件夹./gradlew clean# 检查依赖并编译打包./gradlew build# 编译并安装debug包./gradlew installDebug# 编译并打印日志./gradlew build --info# 译并输出性能报告,性能报告一般在 构建工程根目录 build/reports/profile./gradlew build --profile# 调试模式构建并打印堆栈日志./gradlew build --info --debug --stacktrace# 离线模式./gradlew aDR --offline# 守护进程./gradlew build --daemon# 并行编译模式./gradlew build --parallel --parallel-threads=N 0x02 构建并安装命令123456789101112131415161718192021222324# 编译并打Debug包./gradlew assembleDebug# 这个是简写 assembleDebug./gradlew aD# 编译并打Release的包./gradlew assembleRelease# 这个是简写 assembleRelease./gradlew aR# Debug模式打包并安装./gradlew install app:assembleDebug# Release模式打包并安装./gradlew installRelease# 卸载Release模式包./gradlew uninstallRelease# Flavor渠道包./gradlew install app:assemble<Flavor>Debug 0x03 查看包依赖12345678910111213141516./gradlew dependencies# 查看app模块依赖./gradlew app:dependencies# 检索依赖库./gradlew app:dependencies | grep CompileClasspath# windows 没有 grep 命令./gradlew app:dependencies | findstr "CompileClasspath"# 将检索到的依赖分组找到 比如flavorDebugCompileClasspath就是flavor渠道分发的开发编译依赖./gradlew app:dependencies --configuration <flavor>DebugCompileClasspath# 依赖树过长可以保存到本地文件方便查看./gradlew app:dependencies --configuration <flavor>DebugCompileClasspath >1.log 0x04 依赖包更新12345# 依赖包更新命令./gradlew build --refresh-dependencies# 强制更新最新依赖,清除构建并构建./gradlew clean build --refresh-dependencies","link":"/blog/2020/02/28/Gradle%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/"},{"title":"Gradle的环境配置","text":"Gradle的环境配置原文地址:https://www.cnblogs.com/baiqiantao/p/6890674.html Installing Gradle: https://docs.gradle.org/current/userguide/installation.html gradlew 和 gradlew.bat:封装 gradle 的脚本,目的是为了更方便的使用 gradle 环境变量 GRADLE_HOME:仅仅是为了可以在任意目录中执行 gradle 命令,没有特殊的意义 环境变量 GRADLE_USER_HOME:控制在命令行中执行 gradlew 命令时,gradle 下载的目录 IDEA 的 Gradle user home:控制在 IDEA 点击按钮执行各项 Task 等功能时,gradle 下载的目录 IDEA 的 User from gradle:控制在 IDEA 点击按钮执行各项 Task 等功能时,使用的 gradle 的版本 环境变量 GRADLE_HOME设置环境变量 GRADLE_HOME 的目的,仅仅是为了方便在 Path 中指定 gradle 的位置。GRADLE_HOME:D:_dev\\gradle_GRADLE_HOME\\gradle-6.7Path:%GRADLE_HOME%\\bin将 gradle 添加到 Path 的目的是为了,可以在任意目录中执行 gradle 命令。实际上,完全没必要设置环境变量 GRADLE_HOME,Do we really need GRADLE_HOME? 123456gradle -v # 查看版本gradle --help # 查看命令使用帮助λ where gradle # 查看 gradle 命令位置D:\\_dev\\gradle\\GRADLE_HOME\\gradle-6.7\\bin\\gradleD:\\_dev\\gradle\\GRADLE_HOME\\gradle-6.7\\bin\\gradle.bat gradlew 是干嘛的其实 gradlew 只是一个 gradle 的封装(wrapper),gradlew = gradle wrapper,因为在项目根目录有 gradlew 和 gradlew.bat 这两个可执行文件,所以 能且仅能 在项目根目录中执行 gradlew 命令。 123456D:\\_dev\\_code\\as\\Test> gradlew -v # 查看版本D:\\_dev\\_code\\as\\Test> gradlew --help # 查看命令使用帮助D:\\_dev\\_code\\as\\Test> where gradlew # 查看 gradlew 命令位置D:\\_dev\\_code\\as\\Test\\gradlewD:\\_dev\\_code\\as\\Test\\gradlew.bat 这两个文件头部的注释也说明了他们的作用: gradlew:Gradle start up script for UN*X gradlew.bat:Gradle startup script for Windows 之所以添加这个 gradlew 脚本,是为了: 统一项目所使用的 gradle 版本,避免不同开发人员使用不同的 gradle 版本导致的兼容性问题可以把 gradle-wrapper.properties 里面的下载 gradle 的地址切换到公司的公共空间上,以加快下载速度 1distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip 环境变量 GRADLE_USER_HOME设置环境变量 GRADLE_USER_HOME 的目的,是为了自定义下载 gradle 时的本地存储路径。在命令行中执行 gradlew 命令(注意不是 gradle 命令)时,会将对应版本的 gradle 下载到此目录中。下载 gradle 时,下载地址及版本由项目中的 /gradle/wrapper/gradle-wrapper.properties 决定。 123456#Mon Nov 16 00:55:48 CST 2020distributionBase=GRADLE_USER_HOMEdistributionPath=wrapper/distszipStoreBase=GRADLE_USER_HOMEzipStorePath=wrapper/distsdistributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip IDEA 的 Gradle user home在 IDEA 的 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 中,有一个 Gradle user home 的配置,其作用和环境变量 GRADLE_USER_HOME 类似,只不过该配置只是给 IDEA 使用的。譬如点击 gradle 窗口的各种 Task 按钮执行各项 Task 功能时。 注意:仅 IDEA 中的各种图形化操作会使用此配置,在 IDEA 的 Terminal 中执行 gradlew 命令时,使用的依旧是环境变量 GRADLE_USER_HOME。 IDEA 的 User from gradle在 IDEA 的 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 中,有一个 User from gradle 的配置,它也是仅提供给 IDEA 使用的(对 gradlew 无效)。 其作用是,指定当前工程中 IDEA 所使用的 gradle 版本: 当勾选 gradle-wrapper.properties 时,使用 gradle-wrapper.properties 中指定的 gradle 版本。为了防止和在 Terminal 中执行 gradlew 命令时使用的 gradle 版本不同,建议勾选此配置(也是默认配置)。当勾选 Specified location 时,使用指定目录下的 gradle 版本。如果 gradle 下载很慢,就可以勾选此配置,以便使用指定本地下载好的 gradle 版本。 不管在 IDEA 中怎么配置,在 Terminal 中执行 gradlew 命令时,所使用的 gradle 版本都是由 gradle-wrapper.properties 决定的,并且下载路径也都是由环境变量 GRADLE_USER_HOME 决定的。 org.gradle.java.home 配置这里的 JDK 指的是执行 Gradle 命令依赖的 JDK,并非 AndroidStudio 工程依赖的 JDK。 通过 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 设置的 JDK,是在运行 IDEA 图形化按钮时使用的。通过 gradle.properties 设置的 JDK,是在 Terminal 中执行 gradlew 命令时使用的。 123456# MacOS的路径写法org.gradle.java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home# Windows系统的路径写法参考如下# org.gradle.java.home=C:\\\\Program Files\\\\Java\\\\jdk1.8.0_144# org.gradle.java.home=C\\:/_dev/Android/Android Studio/jre 注意:AGP 从 7.0.0-alpha02 版本起,需要使用 Java 11","link":"/blog/2022/08/15/Gradle%E7%9A%84%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/"},{"title":"Gson 数据解析","text":"Gson 数据解析0x01 Kotlin Gson 解析 data class 两条黄金法则:1、 String 必须是可空类型 String?2、 需要使用默认值,则全部字段都必须给予默认值,以满足kotlin对象有空的构造函数0x02 手动解析Gson基础字段1、msg 可空String解析 jsonReader.peek() == JsonToken.NULL 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455import com.google.gson.Gsonimport com.google.gson.JsonSyntaxExceptionimport com.google.gson.TypeAdapterimport com.google.gson.reflect.TypeTokenimport com.google.gson.stream.JsonReaderimport com.google.gson.stream.JsonTokenimport okhttp3.ResponseBodyimport retrofit2.Converterimport java.lang.reflect.ParameterizedTypeimport java.lang.reflect.Typeclass ResponseBodyConverter(val gson: Gson, val type: Type) : Converter<ResponseBody, BaseResponse<Any>> { override fun convert(value: ResponseBody): BaseResponse<Any> { val dataType = GenericsUtils.getParameterUpperBound( 0, type as ParameterizedType ) val baseResponse = BaseResponse<Any>() val jsonReader = JsonReader(value.charStream()) try { jsonReader.beginObject() while (jsonReader.hasNext()) { val name: String = jsonReader.nextName() when (name) { "code" -> { baseResponse.code = jsonReader.nextString() } "msg" -> { // this works, but do not do this. if (jsonReader.peek() == JsonToken.NULL) { jsonReader.nextNull() baseResponse.msg = null } else { baseResponse.msg = jsonReader.nextString() } } "data" -> { val mapped: TypeAdapter<*>? = gson.getAdapter(TypeToken.get(dataType)) baseResponse.data = mapped?.read(jsonReader) } else -> { jsonReader.skipValue() } } } } catch (e: IllegalStateException) { throw JsonSyntaxException(e) } catch (e: IllegalAccessException) { throw AssertionError(e) } jsonReader.endObject() return baseResponse }}","link":"/blog/2021/09/13/Gson%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90/"},{"title":"Gradle配置构建多Module项目","text":"Gradle配置构建多Module项目0x01 配置远程代码库可以按如下方式声明特定的 Maven 或 Ivy 代码库: 12345678allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public' } // public仓是包含central仓和jcenter仓的聚合仓 maven { url 'https://maven.aliyun.com/repository/google' } // 阿里镜像库 maven { url "file://local/repo/" } // 本地文件代码库 ivy { url "https://repo.example.com/ivy" } // Ivy代码库 }} 0x02 统一配置Gradle依赖库版本随着项目采用模块化,组件化开发,moudle 的个数也会随着增加,统一管理配置gradle就显得比较重要了。 1、在 project 根目录创建一个 config.gradle 文件 123456789101112131415161718192021ext { // app 相关版本控制 versions = [ compileVersion : 26, buildVersion : "26.0.2", sdkMinVersion : 15, sdkTargetVersion : 26, appVersionCode : 520, appVersionName : "1.0.0" ] // support依赖 support = [ appcompat : "com.android.support:appcompat-v7:26.+", recyclerview: "com.android.support:recyclerview-v7:26.+" ] // 依赖 deps = [ glide : "com.github.bumptech.glide:glide:4.11.0" ] } 2、在 Project 根目录下的 build.gradle 添加apply 1apply from: 'config.gradle' 3、在相应Moudle中调用 123456789101112131415161718192021222324252627android { def versions = rootProject.ext.versions compileSdkVersion versions.compileVersion buildToolsVersion versions.buildVersion defaultConfig { applicationId "com.dench.wanandroid" minSdkVersion versions.sdkMinVersion targetSdkVersion versions.sdkTargetVersion versionCode versions.appVersionCode versionName versions.appVersionName } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { def dependencies = rootProject.ext.deps def support = rootProject.ext.support implementation support.appcompat implementation support.recyclerview implementation dependencies.glide } 0x03 配置Flavor创建产品变种与创建构建类型类似:将其添加到构建配置中的 productFlavors 代码块并添加所需的设置。产品变种支持与 defaultConfig 相同的属性,这是因为,defaultConfig 实际上属于 ProductFlavor 类。这意味着,您可以在 defaultConfig 代码块中提供所有变种的基本配置,每个变种均可更改其中任何默认值,如 applicationId。 1234567891011121314151617181920212223android { defaultConfig {...} buildTypes { debug{...} release{...} } // Specifies one flavor dimension. flavorDimensions "version" productFlavors { demo { dimension "version" applicationIdSuffix ".demo" versionNameSuffix "-demo" versionCode 30000 + android.defaultConfig.versionCode } full { dimension "version" applicationIdSuffix ".full" versionNameSuffix "-full" versionCode 20000 + android.defaultConfig.versionCode } }} 0x04 创建源代码集1、Gradle 要求: 在所有构建变体之间共享的所有内容创建 main/ 源代码集和目录。 将“debug”构建类型特有的 Java 类文件放在 src/debug/java/ 目录中。 12345678910111213debug----Compile configuration: compilebuild.gradle name: android.sourceSets.debugJava sources: [app/src/debug/java]Manifest file: app/src/debug/AndroidManifest.xmlAndroid resources: [app/src/debug/res]Assets: [app/src/debug/assets]AIDL sources: [app/src/debug/aidl]RenderScript sources: [app/src/debug/rs]JNI sources: [app/src/debug/jni]JNI libraries: [app/src/debug/jniLibs]Java-style resources: [app/src/debug/resources] 依次转到 MyApplication > Tasks > android,然后双击 sourceSets。Gradle 执行该任务后,系统应该会打开 Run 窗口以显示输出。 2、更改默认源代码集配置 123456789android { sourceSets { main { java.srcDirs = ['other/java'] res.srcDirs = ['other/res1', 'other/res2'] manifest.srcFile 'other/AndroidManifest.xml' } }} 0x05 声明依赖项123456dependencies { // Adds the local "mylibrary" module as a dependency to the "free" flavor. freeImplementation project(":mylibrary") testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'} 0x06 配置签名1、在项目的根目录下创建一个名为 keystore.properties 的文件,并使其包含以下信息: 1234storePassword=myStorePasswordkeyPassword=myKeyPasswordkeyAlias=myKeyAliasstoreFile=myStoreFileLocation 2、在 build.gradle 文件中,按如下方式加载 keystore.properties 文件(必须在 android 代码块前面): 12345678def keystorePropertiesFile = rootProject.file("keystore.properties")def keystoreProperties = new Properties()keystoreProperties.load(new FileInputStream(keystorePropertiesFile))// before androidandroid { } 3、输入存储在 keystoreProperties 对象中的签名信息: 123456789101112131415android { signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } }} 如需从环境变量获取这些密码,请添加以下代码: 12storePassword System.getenv("KSTOREPWD")keyPassword System.getenv("KEYPWD") 0x07 apk重命名123456789android { applicationVariants.all { variant -> if (variant.buildType.name == 'release') { variant.outputs.all { outputFileName = "app_v${variant.versionName}.${buildTime}_${variant.productFlavors[0].name}_${variant.buildType.name}.apk" } } }} or 123outputFileName = "app_v${versionName}.${buildTime}_${flavorName}_${buildType.name}.apk"outputFileName = "../../${outputFileName}"println outputFileName 0x08 将构建变量注入清单1、如果您需要将变量插入在 build.gradle 文件中定义的 AndroidManifest.xml 文件,可以使用 manifestPlaceholders 属性执行此操作。此属性采用键值对的映射,如下所示: 123456android { defaultConfig { manifestPlaceholders = [hostName:"www.example.com"] applicationId "com.example.myapp" }} 2、您可以将某个占位符作为属性值插入清单文件,如下所示: 1234<intent-filter ... > <data android:scheme="http" android:host="${hostName}" ... /> <action android:name="${applicationId}.TRANSMOGRIFY" /></intent-filter> 0x09 gradle自定义Java变量和资源值在构建时,Gradle 将生成 BuildConfig 类,以便应用代码可以检查与当前构建有关的信息。您也可以从 Gradle 构建配置文件中使用 buildConfigField() 方法将自定义字段添加到 BuildConfig 类中,然后在应用的运行时代码中访问这些值。同样,您也可以使用 resValue() 添加应用资源值。 12345678910111213def buildTime = new Data().format("yyyyMMddHHmm", TimeZone.getTimeZone("GTM+08:00"))android { buildTypes { release { buildConfigField("String", "BUILD_TIME", "\\"${buildTime}\\"") resValue("string", "build_time", "${buildTime}") } debug { buildConfigField("String", "BUILD_TIME", "\\"0\\"") resValue("string", "build_time", "0") } }} 在应用代码中,您可以按如下方式访问属性: 12Log.i(TAG, BuildConfig.BUILD_TIME);Log.i(TAG, getString(R.string.build_time)); 0x10 设定编码12345allprojects { tasks.withType(JavaCompile){ options.encoding = "UTF-8" }}","link":"/blog/2020/12/20/Gradle%E9%85%8D%E7%BD%AE%E6%9E%84%E5%BB%BA%E5%A4%9AModule%E9%A1%B9%E7%9B%AE/"},{"title":"Hexo & NexT 搭建个人网站","text":"1. 搭建环境(2020-2-6) Git(v2.21.1): https://www.git-scm.com/downloads/ Node.js(v12.14.1): https://nodejs.org/zh-cn/ Hexo(v4.2.0): https://hexo.io/ NexT(v7.7.1): https://theme-next.org/ 2. 安装 Hexo1$ npm install hexo-cli -g 3. 创建 hexo 工程新建工程1$ hexo init [folder] 新建post1$ hexo new [layout] <title> 编译工程1$ hexo generate 本地服务1$ hexo server 部署1$ hexo deploy 其他常用命令123$ hexo clean $ hexo g && hexo s $ hexo clean && hexo g && hexo d 4. 一键发布到 Github 和 Coding 4.1 安装 hexo-deployer-git. 4.2 配置 hexo/_config.yml, 添加代码: 1234567deploy: repo: # 下面两种写法都支持 github: https://github.com/user/project.git,branch coding: url: https://git.coding.com/user/project.git branch: branch_name 4.3 部署使用 hexo clean && hexo g && hexo d. 5. 配置工程hexo/_config.yml12345678910title: subtitle: author: description: #描述 SEO language: zh-CNtimezone: #建议不填skip_render: - README.md - CNAME 6. 安装主题 NexT 6.1 NexT 主题下载 12$ cd hexo$ git clone https://github.com/theme-next/hexo-theme-next themes/next 6.2 应用主题,配置hexo/_config.yml: 1theme: next 更多主题: https://github.com/hexojs/hexo/wiki/Themes 7. 配置主题 NexT以下配置都在 NexT 主题配置文件 next/_config.yml 。 7.1 选择 Scheme1234#scheme: Muse#scheme: Mist#scheme: Piscesscheme: Gemini 7.2 选择语言1language: zh-CN 7.3 配置 menu123456789menu: home: / || home #about: /about/ || user #tags: /tags/ || tags #categories: /categories/ || th archives: /archives/ || archive #schedule: /schedule/ || calendar #sitemap: /sitemap.xml || sitemap #commonweal: /404/ || heartbeat 7.4 配置 Tags 和 Categories menu 中配置 tags 和 categories,需要自定义 page。自定义Tags 7.5 配置 Sidebar1234567891011# Sidebar Avataravatar: # Replace the default image and set the url here. url: /images/avatar.png # If true, the avatar will be dispalyed in circle. rounded: true # If true, the avatar will be rotated with the cursor. rotated: false# Posts / Categories / Tags in sidebar.site_state: true 7.6 配置 Favicon1234567favicon: small: /images/favicon-16x16-next.png medium: /images/favicon-32x32-next.png apple_touch_icon: /images/apple-touch-icon-next.png safari_pinned_tab: /images/logo.svg android_manifest: /images/manifest.json ms_browserconfig: /images/browserconfig.xml 7.7 配置 Back2top123456back2top: enable: true # Back to top in sidebar. sidebar: false # Scroll percent label in b2t button. scrollpercent: true 7.8 配置字体123456font: enable: true global: external: true family: Menlo size: 1.125 7.9 GitHub Banner1234github_banner: enable: true permalink: https://github.com/yourname title: Follow me on GitHub 7.10 阅读全文(控制首页显示位置) 在 <_post>.md 中添加 <!-- more --> 标签 更多主题配置: https://theme-next.org/docs/theme-settings/","link":"/blog/2020/02/06/Hexo%E5%92%8CNexT%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/"},{"title":"Java内存模型JMM","text":"Java内存模型java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的。 0x01.主内存与工作内存java内存模型规定了所有的变量都存储在住内存。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的主内存中的变量的拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量传递均需要通过主内存来完成。当多个线程操作的变量涉及到同一个主内存区域,将可能导致各自的工作线程数据不一致,这样就导致变量同步回主内存的时候可能冲突导致数据丢失。 这里需要说明一下: 1.主内存、工作内存与java内存区域中的java堆、虚拟机栈、方法区并不是一个层次的内存划分。 2.这里的变量跟我们写java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。 0x02.主内存与工作线程交互的基本操作主内存与工作线程交互的基本操作有以下几种: lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态 unlock(解锁):作用于主内存的变量,释放锁定状态的变量 read(读取):作用于主内存的变量,把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用 load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。 use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作 assign(赋值):作用于工作内存的变量,把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作 store(存储):作用于工作内存的变量,把工作内存的一个变量值传送到主内存,以便随后的write操作使用 write(写入):作用于主内存的变量,把store操作从工作内存得到的变量的值放入主内存变量中 虚拟机实现必须保证上面的每一种操作都是原子的。 如果要把一个变量从主内存复制到工作内存,需要顺序执行read和load操作;如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。 0x03.交互规则还规定上述8中基本操作时必须满足如下规则: 1、read和load,store和write必须成对出现2、不允许一个线程丢弃它的最近的assign操作,变量在工作内存中改变后必须写回主内存3、不允许一个线程将没有assign过的数据从工作线程同步回主内存4、一个新的变量只能在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load或assgin)的变量,对一个变量进行use,store之前必须load,assign5、一个变量同一时刻只允许一条线程对其进行lock,同一个线程对lock操作可重复执行多次,之后需要执行相同次数的unlock变量才会被解锁6、对变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。7、一个变量未被lock不允许unlock,并且不能unlock一个被其他线程锁定住的变量8、对一个变量执行unlock操作前,必须先把此变量同步回主内存中 0x04.volatile的特殊规则1)Java内存模型对volatile变量定义了特殊规则: 假定T表示一个线程,V和W分别表示两个volatile型变量,那么进行read,load,use,assign,store,write操作时需要满足如下规则1、线程T对变量V执行的load和use操作必须成对执行,即执行的前一个动作是load,后一个动作才能是use。后一个动作是use,前一个动作才能是load,线程T对变量V的use动作可以认为是和线程T对变量V的load,read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值) 2、与1相对的,线程T对变量V执行的assign和store操作必须成对执行,线程T对变量V的assign动作可以认为是和线程T对变量V的store,write动作相关联,必须连续一个出现(这条规则要求在工作线程中,每次修改V后都必须立刻同步会主内存中,用于保证其他线程可以看到自己对变量V所做的修改) 3、假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同) java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。 2)volatile对指令重排序的实现: volatile修饰的变量,赋值后多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个屏障。lock的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,这种操作相当于对Cache中的变量做了一次前面介绍Java内存模式中所说的“store和write”操作。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。lock addl $0x0,(%esp。) 命令把修改同步回内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。","link":"/blog/2020/08/13/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8BJMM/"},{"title":"Kotlin单例","text":"Kotlin单例下面介绍一下kotlin 线程安全的几种单例写法。 0x01 饿汉模式123// Kotlin实现object Singleton {} 123456789101112// 反编译Kotlin实现的Java代码public final class Singleton { public static final Singleton INSTANCE; private Singleton() { } static { Singleton var0 = new Singleton(); INSTANCE = var0; }} 0x02 双重校验锁式双重校验锁式(Double Check) 12345678910111213141516171819// Kotlin实现class Singleton private constructor() { companion object { @Volatile private var instance: Singleton? = null @JvmStatic fun getInstance(): Singleton { if (instance == null) { synchronized(Singleton::class.java) { if (instance == null) { instance = Singleton() } } } return instance!! } }} 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354// 反编译Kotlin实现的Java代码public final class Singleton { private static volatile Singleton instance; public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null); private Singleton() { } // $FF: synthetic method public Singleton(DefaultConstructorMarker $constructor_marker) { this(); } @JvmStatic @NotNull public static final Singleton getInstance() { return Companion.getInstance(); } public static final class Companion { @JvmStatic @NotNull public final Singleton getInstance() { if (Singleton.instance == null) { Class var1 = Singleton.class; boolean var2 = false; boolean var3 = false; synchronized(var1) { int var4 = false; if (Singleton.instance == null) { Singleton.instance = new Singleton((DefaultConstructorMarker)null); } Unit var6 = Unit.INSTANCE; } } Singleton var10000 = Singleton.instance; if (var10000 == null) { Intrinsics.throwNpe(); } return var10000; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } }} 0x03 静态内部类123456789101112//Java实现public class SingletonDemo { private static class SingletonHolder{ private static SingletonDemo instance=new SingletonDemo(); } private SingletonDemo(){ System.out.println("Singleton has loaded"); } public static SingletonDemo getInstance(){ return SingletonHolder.instance; }} 1234567891011// Kotlin实现class Singleton private constructor() { companion object { @JvmStatic fun getInstance() = SingletonHolder.instance } private object SingletonHolder { @JvmStatic val instance: Singleton = Singleton() }} 12345678910111213141516171819202122232425262728293031323334// 反编译Kotlin实现的Java代码public final class Singleton { public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null); private Singleton() { } @JvmStatic @NotNull public static final Singleton getInstance() { return Companion.getInstance(); } private static final class SingletonHolder { @NotNull private static final Singleton instance; public static final Singleton.SingletonHolder INSTANCE; @NotNull public static final Singleton getInstance() { return instance; } static { Singleton.SingletonHolder var0 = new Singleton.SingletonHolder(); INSTANCE = var0; instance = new Singleton((DefaultConstructorMarker)null); } } public static final class Companion { @JvmStatic @NotNull public final Singleton getInstance() { return Singleton.SingletonHolder.getInstance(); } private Companion() { } }}","link":"/blog/2021/11/30/Kotlin%E5%8D%95%E4%BE%8B/"},{"title":"观察者模式Kotlin泛型实现消息中心","text":"观察者模式 + Kotlin 泛型实现的简易版消息中心 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class MessageCenter<T> { companion object { private val centers = mutableMapOf<String, Any>() fun <T> getInstance(clazz: Class<T>): MessageCenter<T> { return if (centers[clazz.simpleName] != null) { centers[clazz.simpleName] as MessageCenter<T> } else { val messageCenter = MessageCenter<T>() centers[clazz.simpleName] = messageCenter messageCenter } } } fun register(observer: Observer<T>) { observers.add(observer) } fun unregister(observer: Observer<T>) { if (observers.contains(observer)) { observers.remove(observer) } } fun post(t: T) { observers.forEach { it.receive(t) } } private var observers = mutableListOf<Observer<T>>()}interface Observer<T> { fun receive(t: T)}fun main() { val ob = object : Observer<String> { override fun receive(t: String) { println("result is: $t") } } MessageCenter.getInstance(String::class.java).register(ob) MessageCenter.getInstance(String::class.java).post("txt post.") MessageCenter.getInstance(String::class.java).unregister(ob) MessageCenter.getInstance(Int::class.java).post(123)}","link":"/blog/2021/05/21/MessageCenter/"},{"title":"PowerShell最佳实践","text":"PowerShell最佳实践Windows 10 系统自带 PowerShell,美化教程 0x01 安装Fluent Terminal在Windows 应用商店安装,或者Github 0x02 安装powershell模块1、安装posh-git、oh-my-posh和Get-ChildItemColor(美化ls命令): 在powershell管理员模式下: 123$ Install-Module posh-git -Scope CurrentUser$ Install-Module oh-my-posh -Scope CurrentUser$ Install-Module DirColors 2、设置修改powershell的配置文件: 12$ if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force }$ notepad $PROFILE 输入内容: 1234Import-Module DirColorsImport-Module posh-gitImport-Module oh-my-poshSet-PoshPrompt -Theme PowerLine 其中主题名可以在下面的路径里找到,可以自行切换主题。 1C:\\Program Files\\WindowsPowerShell\\Modules\\oh-my-posh\\3.163.0\\themes 0x03 安装Powerline字体在Fluent Terminal设置 powerline 字体和字体大小。 0x04 文件管理器命令在文件夹中打开: 12345678# I 在文件夹中打开$ explorer (gl)# II 在文件夹中打开$ start .# III 在文件夹中打开$ ii .# 打开当前根目录$ ii / 在当前文件夹打开PowerShell: 空白处 Shift + 鼠标右键,在弹出的菜单中选择PowerShell. 0x05 VS Code 命令1$ code .","link":"/blog/2021/06/17/PowerShell%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/"},{"title":"Problems专题:Dialog","text":"Problems专题:Dialog0x01 OnKeyDown部分机型无法监听问题分析 监听返回键和音量键,重载OnKeyDown()方法,部分机型会失效。 解决方案 给相应的Dialog监听setOnKeyListener()。 12345678910// 解决不同机型版本兼容问题,onKeyDown 可能被拦截setOnKeyListener { dialog, keyCode, event -> Log.d(TAG, "setOnKeyListener: keyCode = $keyCode, event = $event") if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { handleKeyEvent(keyCode) true } else { false }} 注意区分keycode,防止业务层重复处理 0x02 DialogFragment不能自动弹出软键盘方案一:延迟弹出软键盘在dialog显示之后,延迟200ms再显示软键盘 1234567891011121314151617181920//强制显示或者关闭系统键盘public static void toggleKeyboard(final EditText editText, final boolean status) { if (editText == null) return; Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { InputMethodManager m = (InputMethodManager) ApplicationExtKt.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); if (status) { m.showSoftInput(editText, InputMethodManager.SHOW_FORCED); } else { IBinder windowToken = editText.getWindowToken(); if (windowToken != null) { m.hideSoftInputFromWindow(windowToken, 0); } } } }, status ? 200 : 100);} 方案二:设置 SoftInputMode 为 SOFT_INPUT_STATE_ALWAYS_VISIBLE 12getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);inputEditText.requestFocus(); 0x03 关闭DialogFragment无法关闭软键盘问题分析 一般情况下,在onPause或者dismiss方法直接调用hideKeyboard就可以 1234override fun onPause() { KeyBoardUtils.hideKeyboard(binding.etSearch) super.onPause()} 但是,在某些时候还是会存在关闭不成功的情况。这是由于Dialog下面的Activity或Fragment存在EditText等抢占焦点,导致在DialogFragment在调用dismiss方法时,键盘已经被抢占焦点,所以无法关闭。 解决方案 在DialogFragment的dismiss方法回调 1234override fun onDismiss(dialog: DialogInterface) { listener?.onDialogDismiss() super.onDismiss(dialog)} 在前一个Activity或者Fragment中重新关闭键盘。 1234567// 消除弹框遗留下来的keyboardprivate fun onDialogDismiss() { // 消除弹框 Handler().postDelayed({ KeyBoardUtils.hideKeyboard(binding.root) }, 200)}","link":"/blog/2021/06/17/Problems%E4%B8%93%E9%A2%98%E4%B9%8BDialog/"},{"title":"Problems专题之Gradle","text":"Problems专题之Gradle0x01 More than one file was found with OS independent path ‘META-INF/webview_release.kotlin_module’这是因为第三方库中存在很多重名的META-INF文件,在打包的时候去除即可 123456789101112131415161718android { // ... packagingOptions { exclude 'META-INF/webview_release.kotlin_module' exclude 'META-INF/proguard/androidx-annotations.pro' exclude 'META-INF/gradle/incremental.annotation.processors' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/license.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE.txt' exclude 'META-INF/notice.txt' exclude 'META-INF/ASL2.0' // ... }} 0x02 Certificate for <x.x.x> doesn’t match any of the subject alternative names在执行gradlew命令打包时遇到这个错误,肯定是https证书有问题。 解决方案:如果支持http的话就使用http 123456789> Could not resolve com.bytedanceapi:ttsdk-ttmp:1.36.2.25.pcdn. Required by: project :player > com.bytedanceapi:ttsdk-player_premium:1.36.2.25.pcdn > com.bytedanceapi:ttsdk-ttplayer:1.36.2.25.pcdn > Could not resolve com.bytedanceapi:ttsdk-ttmp:1.36.2.25.pcdn. > Could not get resource 'https://artifact.bytedance.com/repository/Volcengine/com/bytedanceapi/ttsdk-ttmp/1.36.2.25.pcdn/ttsdk-ttmp-1.36.2.25.pcdn.pom'. > Could not GET 'https://artifact.bytedance.com/repository/Volcengine/com/bytedanceapi/ttsdk-ttmp/1.36.2.25.pcdn/ttsdk-ttmp-1.36.2.25.pcdn.pom'. > Certificate for <artifact.bytedance.com> doesn't match any of the subject alternative names: [*.alicdn.com, *.cmos.greencompute.org, cmos.greencompute.org, m.intl.taobao.com, *.mobgslb.tbcache.com, alikunlun.com, *.alikunlun.com, s.tbcdn.cn, *.django.t.taobao.com, alicdn.com] 把项目根目录的build.gradle文件中所有的https://artifact.bytedance.com/替换为http://artifact.bytedance.com/即可 Ox03 module java.base does not “opens java.io” to unnamed module升级到 Java 16 以上,AndroidStudio编译遇到错误。 Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not “opens java.io” to unnamed module @fb04536 解决方案一: 将 gradle-wrapper 属性中的 gradle 版本更改为 7.1.1(6.x 不支持 java 16) 在 gradle.properties 中添加以下行 123456org.gradle.jvmargs=-Xmx1536m \\--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \\--add-opens=java.base/java.lang=ALL-UNNAMED \\--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \\--add-opens=java.base/java.io=ALL-UNNAMED \\--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 解决方案二: 升级build-gradlew版本。 将项目根目录的 build.gradle文件中classpath 'com.android.tools.build:gradle:4.2.2'升级为classpath 'com.android.tools.build:gradle:7.2.1'","link":"/blog/2023/07/05/Problems%E4%B8%93%E9%A2%98%E4%B9%8BGradle/"},{"title":"Problems专题:Parcel","text":"Problems专题:Parcel0x01 Unmarshalling unknown type1234567891011121314151617181920212223242526Thread Name: 'main' java.lang.RuntimeException: Parcel android.os.Parcel@bbfcc04: Unmarshalling unknown type code 2131296928 at offset 1088 at android.os.Parcel.readValue(Parcel.java:2750) at android.os.Parcel.readSparseArrayInternal(Parcel.java:3126) at android.os.Parcel.readSparseArray(Parcel.java:2354) at android.os.Parcel.readValue(Parcel.java:2728) at android.os.Parcel.readArrayMapInternal(Parcel.java:3045) at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:288) at android.os.BaseBundle.unparcel(BaseBundle.java:232) at android.os.Bundle.getSparseParcelableArray(Bundle.java:1010) at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2133) at android.app.Activity.onRestoreInstanceState(Activity.java:1173) at android.app.Activity.performRestoreInstanceState(Activity.java:1128) at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1318) at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3025) at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180) at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1840) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:207) at android.app.ActivityThread.main(ActivityThread.java:6878) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876) 问题分析 情况1 Parcelable 对象为空,反序列化异常 情况2 Parcelable 序列化和反序列化的字段和顺序没有完全对应 情况3 自定义View的数据保存与恢复 解决方案 情况1 Parcelable 对象在序列化和反序列化增加 null 值判断 情况2 Parcelable 对象 read 和 write 字段的类型和顺序保持一直 1234567891011121314151617181920212223public class Account implements Parcelable { public Account(Parcel in) { this.name = in.readString(); this.type = in.readInt(); if (TextUtils.isEmpty(name)) { throw new android.os.BadParcelableException("the name must not be empty: " + name); } // ... this.accessId = in.readString(); // ... } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(type); dest.writeString(accessId); } // ...} 情况3 自定义View的数据保存与恢复 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849override fun onSaveInstanceState(): Parcelable? { Log.d("NavigationBar", "onSaveInstanceState: selectedId=${mSelectedId}") return SavedState(super.onSaveInstanceState(), mSelectedId)}override fun onRestoreInstanceState(state: Parcelable?) { if (state is SavedState) { val id = state.selectedId Log.d("NavigationBar", "onRestoreInstanceState: selectedId=${state.selectedId}") super.onRestoreInstanceState(state.superState) select(id) return } return super.onRestoreInstanceState(state)}internal class SavedState : BaseSavedState { var selectedId: Int = View.NO_ID constructor(source: Parcel) : super(source) { selectedId = source.readInt() Log.d("NavigationBar", "readFromParcel: selectedId=$selectedId") } constructor(superState: Parcelable?, selectedId: Int) : super(superState) { this.selectedId = selectedId } override fun writeToParcel(parcel: Parcel, flags: Int) { super.writeToParcel(parcel, flags) parcel.writeInt(selectedId) Log.d("NavigationBar", "writeToParcel: selectedId=$selectedId") } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator<SavedState> { override fun createFromParcel(parcel: Parcel): SavedState { return SavedState(parcel) } override fun newArray(size: Int): Array<SavedState?> { return arrayOfNulls(size) } }} 0x02 android.os.TransactionTooLargeException1234567891011121314151617181920212223242526272829303132333435java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 542688 bytes at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:160) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:207) at android.app.ActivityThread.main(ActivityThread.java:6878) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)Caused by: android.os.TransactionTooLargeException: data parcel size 542688 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:479) at android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:3941) at java.lang.reflect.Method.invoke(Native Method) at com.taobao.monitor.impl.common.c.invoke(ActivityManagerHook.java:89) at java.lang.reflect.Proxy.invoke(Proxy.java:1006) at $Proxy2.activityStopped(Unknown Source) at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144) ... 7 moreandroid.os.TransactionTooLargeException: data parcel size 542688 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:479) at android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:3941) at java.lang.reflect.Method.invoke(Native Method) at com.taobao.monitor.impl.common.c.invoke(ActivityManagerHook.java:89) at java.lang.reflect.Proxy.invoke(Proxy.java:1006) at $Proxy2.activityStopped(Unknown Source) at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:207) at android.app.ActivityThread.main(ActivityThread.java:6878) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876) 问题分析 1 Intent 传递的数据过大。 2 onSaveInstance 保存的数据过大。 解决方案 尽可能的使用少量的数据。 大数据考虑持久化和其他传递形式。","link":"/blog/2021/06/18/Problems%E4%B8%93%E9%A2%98%E4%B9%8BParcel/"},{"title":"Problems专题:RecyclerView","text":"Problems专题:RecyclerView0x01 Called attach on a child which is not detached12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364java.lang.IllegalArgumentException: Called attach on a child which is not detached: BaseViewHolder{2b241e1 position=12 id=-1, oldPos=-1, pLpos:-1} androidx.recyclerview.widget.RecyclerView{afecb06 VFED..... ......ID 0,0-1080,2055 #7f09236e app:id/recycler_view_xxx}, adapter:com.xxxx.adapter.XxxAdapter@cfc75c7, layout:androidx.recyclerview.widget.LinearLayoutManager@24af7f4, context:com.xxxx.XxxActivity@1ed75e2 at androidx.recyclerview.widget.RecyclerView$5.attachViewToParent(RecyclerView.java:917) at androidx.recyclerview.widget.ChildHelper.attachViewToParent(ChildHelper.java:239) at androidx.recyclerview.widget.RecyclerView.addAnimatingView(RecyclerView.java:1438) at androidx.recyclerview.widget.RecyclerView.animateDisappearance(RecyclerView.java:4377) at androidx.recyclerview.widget.RecyclerView$4.processDisappeared(RecyclerView.java:616) at androidx.recyclerview.widget.ViewInfoStore.process(ViewInfoStore.java:242) at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4210) at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3864) at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4410) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1131) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at androidx.viewpager.widget.ViewPager.onLayout(ViewPager.java:1775) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1131) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332) at android.widget.FrameLayout.onLayout(FrameLayout.java:270) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332) at android.widget.FrameLayout.onLayout(FrameLayout.java:270) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332) at android.widget.FrameLayout.onLayout(FrameLayout.java:270) at com.android.internal.policy.DecorView.onLayout(DecorView.java:905) at android.view.View.layout(View.java:22213) at android.view.ViewGroup.layout(ViewGroup.java:6340) at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3286) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2757) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1865) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7933) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1018) at android.view.Choreographer.doCallbacks(Choreographer.java:837) at android.view.Choreographer.doFrame(Choreographer.java:767) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1003) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:230) at android.app.ActivityThread.main(ActivityThread.java:7951) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:526) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034) 问题分析 对同一个 position 位置同时进行notifyItemRemoved(position) 和 notifyItemInserted(position) 操作导致。 解决方案 避免同时对同一个位置先 notifyItemRemoved 再 notifyItemInserted,使用 notifyItemChanged。 1adapter?.notifyItemChanged(position) 0x02 RecyclerView设置最大高度、宽度当RecyclerView属性设置为wrap_content+maxHeight时,maxHeight没有效果。 问题分析 当RecyclerView的LayoutManager#isAutoMeasureEnabled()返回true时,RecyclerView高度取决于children view的布局高度,并非取决于RecyclerView自身的测量高度。 解决方案 因此,我们只需要重写LayoutManager的public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)方法即可为RecyclerView设置最大宽高。 123456789recyclerView.layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, false) { override fun setMeasuredDimension(childrenBounds: Rect?, wSpec: Int, hSpec: Int) { val height = View.MeasureSpec.getSize(hSpec) val maxHeight = getScreenHeight() * 4 / 5 val realHeight = height.coerceAtMost(maxHeight) val realHeightSpec = View.MeasureSpec.makeMeasureSpec(realHeight, AT_MOST) super.setMeasuredDimension(childrenBounds, wSpec, realHeightSpec) }} 作者:猫爸iYao链接:https://www.jianshu.com/p/0dec79ff70df来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。","link":"/blog/2021/06/19/Problems%E4%B8%93%E9%A2%98%E4%B9%8BRecyclerView/"},{"title":"Problems专题:Provider","text":"Problems专题:Provider0x01 FileProvider:Failed to find configured root that contains /…/**.jpeg123456789101112131415161718192021java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/DCIM/Camera/**.jpeg at androidx.core.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:800) at androidx.core.content.FileProvider.getUriForFile(FileProvider.java:442) at com.luck.picture.lib.tools.PictureFileUtils.parUri(PictureFileUtils.java:533) at com.luck.picture.lib.PictureBaseActivity.startOpenCameraImage(PictureBaseActivity.java:705) at com.luck.picture.lib.PictureSelectorActivity.startCamera(PictureSelectorActivity.java:926) at com.luck.picture.lib.PictureSelectorActivity.onTakePhoto(PictureSelectorActivity.java:1473) at com.luck.picture.lib.adapter.PictureImageGridAdapter.lambda$onBindViewHolder$0$PictureImageGridAdapter(PictureImageGridAdapter.java:155) at com.luck.picture.lib.adapter.-$$Lambda$PictureImageGridAdapter$0EODmJcP4VP0lqmkEhQ1dzLbHi8.onClick(Unknown Source:2) at android.view.View.performClick(View.java:6608) at android.view.View.performClickInternal(View.java:6585) at android.view.View.access$3100(View.java:785) at android.view.View$PerformClick.run(View.java:25921) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:201) at android.app.ActivityThread.main(ActivityThread.java:6810) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)Back traces end. 问题分析 FileProvider 路径配置文件被互相覆盖 FileProvider 路径配置文件id重复,导致覆盖 解决方案","link":"/blog/2021/12/06/Problems%E4%B8%93%E9%A2%98%E4%B9%8BProvider/"},{"title":"Homebrew 入门","text":"Homebrew 是 macOS 或 Linux 缺失的软件包的管理器. Homebrew 官网: https://brew.sh/ 1. 安装1/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 2. 使用国内镜像brew update 卡死,没反应?原因一般都是国内获取资源太慢,可以使用国内镜像解决。 control-C 直接终止当前前台update进程。 2.1 替换 Homebrew 源12cd "$(brew --repo)"git remote set-url origin https://mirrors.ustc.edu.cn/brew.git 2.2 替换 Homebrew Core 源12cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git 2.3 替换 Homebrew Cask 源12cd "$(brew --repo)"/Library/Taps/homebrew/homebrew-caskgit remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git 2.4 修改 Homebrew Bottles 变量在运行 brew 前设置环境变量 HOMEBREW_BOTTLE_DOMAIN ,值为 https://mirrors.ustc.edu.cn/homebrew-bottles 。 对于 bash 用户: 12echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.bash_profilesource ~/.bash_profile 对于 zsh 用户: 12echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.zshrcsource ~/.zshrc 3. Homebrew 常用命令1234567891011Example usage: brew info [FORMULA...] brew install FORMULA... brew update brew upgrade [FORMULA...] brew uninstall FORMULA... brew list [FORMULA...]Troubleshooting: brew config brew doctor 4. 关于 Homebrew gem 和 npm 介绍Homebrew介绍Homebrew简称brew,是Mac OSX上的软件包管理工具,能在Mac中方便的安装软件或者卸载软件。相当于Linux听的yum、apt-get等软件管理工具。 RubyGems介绍RubyGems简称gem,RubyGems是一个包管理框架,提供了ruby社区的gem的托管服务,用于ruby软件包的下载、安装、使用;ruby的软件包被称为gem,包含了ruby应用或库。和brew不同,brew用于操作系统层面上的软件包的安装,而gem只是管理ruby软件 123456789gem --versiongem install rakegem list --localgem build package.gemspecgem help install npm介绍npm,是node.js界的程序/模块管理工具,也就是说npm只管理那些服务于JavaScript社区的程序。而且跨平台,windows和osx,以及其他unix like操作系统都可以用。 123456789npm versionnpm helpnpm list -g // 查看 npm 安装包列表npm outdated -g // 待更新安装包npm install hexo-cli -g // 安装,或者更新","link":"/blog/2020/02/10/Homebrew%E5%85%A5%E9%97%A8/"},{"title":"Problems专题:编译环境","text":"Problems专题:编译环境0x01 java.lang.AssertionError: Could not delete caches dir CreateProcess error=206, El nombre del archivo o la extensión es demasiado largo Caused by: java.lang.AssertionError: Could not delete caches dir YourProjectPath\\build\\kotlin\\compileDebugTestingKotlin 临时解决 打开任务管理器,结束 java.exe 或者 OpenJDK Platform Binary 降级 Android Studio Notice: This happens with the newer AndroidStudio 4.2.x. Google hasn’t provide us a fix, so you’ll need to downgrade to an older version which works for you. 4.1.3 seems to be working fine. 参考链接:https://stackoverflow.com/questions/65832868/caused-by-java-lang-assertionerror-could-not-delete-caches-dir-yourproject-bui 0x02 Please close other application using ADB:Monitor, DDMS, Eclip Warning:debug info can be unavailable. Please close other application using ADB:Monitor, DDMS, Eclipse. 方案一: 1$ adb usb 方案二: 打开任务管理器,结束adb.exe进程。 方案三: 重启 adb 服务 12$ adb kill-server$ adb start-server 0x03 能安装apk却无法查看log[真机偶现]能安装apk却无法在Logcat查看log,即使重启Android studio,重启adb服务都无法解决。最后通过重启手机搞定 0x04 platform-tools/api/api-versions.xml java.io.IOException: Stream closed在 android studio 更新到 v2020.3.1 后遇到 1cannot load api descriptions from ../Android/android-sdk/platform-tools/api/api-versions.xml java.io.IOException: Stream closed 问题的原因与类SdkUtils (请参阅the source file)相关。SdkUtils类具有对文件platform-tools/api/api-versions.xml的硬引用,但是在最新的平台工具(31.0.3)中,该文件不再存在。 从platforms/android-31/data/api-versions.xml复制文件到platform-tools/api/api-versions.xml。 如果是CI编译,可以尝试以下脚本: 12345steps: - bash: | echo Android sdk location: $ANDROID_SDK_ROOT mkdir $ANDROID_SDK_ROOT/platform-tools/api/ cp $ANDROID_SDK_ROOT/platforms/android-30/data/api-versions.xml $ANDROID_SDK_ROOT/platform-tools/api/ 0x05 Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.升级android sdk api 版本到31,适配android 12 ,遇到这个问题。当前开发环境: android studio 版本: 2020.3.1 AGP 版本: 4.1.2 (classpath "com.android.tools.build:gradle:4.1.2") SDK 版本 123456789android { compileSdkVersion 31 buildToolsVersion '31.0.0' defaultConfig { minSdkVersion 21 targetSdkVersion 31 }} SDK Manager更新对应版本都正常下载,编译过程出现异常 1Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager. 是 Build Tools 升级之后,DX 变成了 D8。而 AGP 4.x 的版本使用的还是DX。 解决的方案: 12345# change below to your Android SDK pathcd ~/Library/Android/sdk/build-tools/31.0.0 \\ && mv d8 dx \\ && cd lib \\ && mv d8.jar dx.jar 将 C:\\Users\\user\\AppData\\Local\\Android\\Sdk\\build-tools\\31.0.0\\d8.bat 改为 dx.bat将 C:\\Users\\user\\AppData\\Local\\Android\\Sdk\\build-tools\\31.0.0\\lib\\d8.jar 改为 dx.jar PS:也可以尝试升级 AGP 到 7.x","link":"/blog/2021/06/20/Problems%E4%B8%93%E9%A2%98%E4%B9%8B%E7%BC%96%E8%AF%91%E7%8E%AF%E5%A2%83/"},{"title":"Python爬取网络图片","text":"直接上源码 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455'''用Python爬某新闻网站的照片PS:仅测试爬虫功能,滥用后果自负'''import requestsfrom hashlib import md5import reimport osWEB_URL = 'https://new.qq.com/omn/20200207/20200207A0OSQX00.html'IMAGE_PATH='/Users/**/workspaces/pythonP/img1'# 通过正则找出网页中所有的图片地址def find_img_url(): r = requests.get(WEB_URL) r.raise_for_status() r.encoding = r.apparent_encoding demo = r.text list = [] # ".*?" :正则表达式匹配任意字符串 pattern1 = '<img src=".*?" class="content-picture">' results1 = re.findall(pattern1, demo) for res1 in results1: # <img src="//inews.gtimg.com/newsapp_bt/0/11299639206/1000" class="content-picture"> # https://inews.gtimg.com/newsapp_bt/0/11299639205/1000 res1 = res1.replace('<img src=\\"','https:') res1 = res1.replace('\\" class=\\"content-picture\\">','') list.append(res1) # print(res1) pattern2 = '\\"http://inews.gtimg.com/newsapp_bt/0/.*?/1000\\"' results2 = re.findall(pattern2, demo) for res2 in results2: res2 = res2.replace('\\"','') list.append(res2) # print(res2) return list# 下载图片到本地def download_image(img_url): r = requests.get(img_url) r.raise_for_status() content = r.content file_path = '{0}/{1}.{2}'.format(IMAGE_PATH, md5(content).hexdigest(), 'jpg') # print(file_path) if not os.path.exists(file_path):#os.path.exists(file_path)判断文件是否存在,存在返回1,不存在返回0 with open(file_path, 'wb') as f: f.write(content) f.close()list = find_img_url()for i in list: print(i, ' download...') download_image(i)print("一共下载{}条数据!".format(len(list)))","link":"/blog/2020/02/08/Python%E7%88%AC%E5%8F%96%E7%BD%91%E7%BB%9C%E5%9B%BE%E7%89%87/"},{"title":"implementation compileOnly和api","text":"implementation和apiimplementation和api是取代之前的compile的,其中api和compile是一样的效果,implementation有所不同,通过implementation依赖的库只能自己库本身访问,举个例子,A依赖B,B依赖C,如果B依赖C是使用的implementation依赖,那么在A中是访问不到C中的方法的,如果需要访问,请使用api依赖 compile onlycompile only和provided效果是一样的,只在编译的时候有效, 不参与打包 runtime onlyruntimeOnly 和 apk效果一样,只在打包的时候有效,编译不参与 跟编译环境相关 test implementation testImplementation和testCompile效果一样,在单元测试和打包测试apk的时候有效 debug implementation debugImplementation和debugCompile效果相同, 在debug模式下有效 release implementation releaseImplementation和releaseCompile效果相同,只在release模式和打包release包情况下有效","link":"/blog/2020/08/27/Implementation%E5%92%8Capi/"},{"title":"RecyclerView+SnapHelper实现ViewPager滑动效果","text":"RecyclerView+SnapHelper实现ViewPager滑动效果SnapHelper结合RecyclerView使用,能很方便的实现ViewPager滑动效果。SnapHelper是一个抽象类,Google内置了两个默认实现类,LinearSnapHelper和PagerSnapHelper。 LinearSnapHelper的使用方法使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动多个条目。 12345LinearLayoutManager manager = new LinearLayoutManager(getContext());manager.setOrientation(LinearLayoutManager.HORIZONTAL);recyclerView.setLayoutManager(manager);LinearSnapHelper snapHelper = new LinearSnapHelper();snapHelper.attachToRecyclerView(recyclerView); PagerSnapHelper的使用方法使RecyclerView像ViewPager一样的效果,每次只能滑动一页。 12345LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);recyclerView.setLayoutManager(linearLayoutManager);PagerSnapHelper snapHelper = new PagerSnapHelper();snapHelper.attachToRecyclerView(recyclerView); 原文地址:https://developer.aliyun.com/article/665537","link":"/blog/2023/06/28/RecyclerView+SnapHelper%E5%AE%9E%E7%8E%B0ViewPager%E6%BB%91%E5%8A%A8%E6%95%88%E6%9E%9C/"},{"title":"RecyclerViewHelper","text":"RecyclerViewHelper提供了注册加载更多,和判断是否不足一屏等工具方法 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101import androidx.recyclerview.widget.GridLayoutManagerimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport androidx.recyclerview.widget.StaggeredGridLayoutManagerobject RecyclerViewHelper { /** * RecyclerView 注册加载更多监听 */ @JvmStatic fun addOnScrollListener(recyclerView: RecyclerView, loadMore: () -> Unit) { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (newState == RecyclerView.SCROLL_STATE_IDLE) { val layoutManager = recyclerView.layoutManager if (layoutManager is LinearLayoutManager) { val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() val count = layoutManager.itemCount if (lastPosition >= count - 2) { loadMore() return@onScrollStateChanged } } else if (layoutManager is StaggeredGridLayoutManager) { val spanCount = layoutManager.spanCount val count = layoutManager.itemCount val result = IntArray(spanCount) layoutManager.findLastCompletelyVisibleItemPositions(result) for (it in result) { if (it >= count - spanCount - 1) { loadMore() return@onScrollStateChanged } } } else if (layoutManager is GridLayoutManager) { val spanCount = layoutManager.spanCount val count = layoutManager.itemCount val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() if (lastPosition >= count - spanCount - 1) { loadMore() return@onScrollStateChanged } } } } }) } /** * 判断是否一屏显示 * * 错误或者空了返回 false */ @JvmStatic fun isOneScreen(recyclerView: RecyclerView?): Boolean { recyclerView?.let { val layoutManager = recyclerView.layoutManager if (layoutManager is LinearLayoutManager) { // include GridLayoutManager val count = layoutManager.itemCount return count > 0 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && layoutManager.findLastCompletelyVisibleItemPosition() == count - 1 } else if (layoutManager is StaggeredGridLayoutManager) { val spanCount = layoutManager.spanCount val count = layoutManager.itemCount val last = IntArray(spanCount) val first = IntArray(spanCount) layoutManager.findLastCompletelyVisibleItemPositions(last) layoutManager.findFirstCompletelyVisibleItemPositions(first) return count > 0 && first.min() == 0 && last.max() == count - 1 } } return false } private fun IntArray.min(): Int { if (this.isNotEmpty()) { var result = this[0] this.forEach { if (result > it) result = it } return result } return -1 } private fun IntArray.max(): Int { if (this.isNotEmpty()) { var result = this[0] this.forEach { if (result < it) result = it } return result } return -1 }}","link":"/blog/2021/08/23/RecyclerViewHelper/"},{"title":"RecyclerView 根据滑动位置动态改变背景透明度","text":"RecyclerView 根据滑动位置动态改变背景透明度根据滑动位置动态改变背景透明度,直接上代码: 1234567891011121314151617181920212223242526272829303132333435363738binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) onRecyclerScrolled(recyclerView, dx, dy) } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) }})private val dp180 = dp2px(180)private var distanceY = 0private var current = 0private fun onRecyclerScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { distanceY += dy when { distanceY >= dp180 -> { if (current == 1) return recyclerView.setBackgroundColor(Color.argb(255, 246, 248, 250)) current = 1 } distanceY <= 0 -> { if (current == 0) return recyclerView.setBackgroundColor(Color.argb(0, 246, 248, 250)) current = 0 } else -> { recyclerView.setBackgroundColor( Color.argb( distanceY * 255 / dp180, 246, 248, 250 ) ) current = -1 } }}","link":"/blog/2022/06/27/RecyclerView%E6%A0%B9%E6%8D%AE%E6%BB%91%E5%8A%A8%E4%BD%8D%E7%BD%AE%E5%8A%A8%E6%80%81%E6%94%B9%E5%8F%98%E8%83%8C%E6%99%AF%E9%80%8F%E6%98%8E%E5%BA%A6/"},{"title":"RecyclerView实现瀑布流以及细节问题","text":"RecyclerView实现瀑布流以及细节问题1)使用 StaggeredGridLayoutManager 12345678910val layoutManager = StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_NONE// 禁止左右交换recyclerView.layoutManager = layoutManager// decorationrecyclerView.addItemDecoration(StaggeredDividerItemDecoration(16, true))// adaptermAdapter = PjListVPAdapter()recyclerView.adapter = mAdapter// animatorrecyclerView.itemAnimator = DefaultItemAnimator() 2)如果需要Item之间的间隔,就需要自定义 ItemDecoration 1234567891011121314151617181920212223242526272829303132333435363738/** * 瀑布流ItemDecoration * 必须配合RecyclerView的StaggeredGridLayoutManager一起使用 * * @author Dench * @data 2020-09-04 */class StaggeredDividerItemDecoration( space: Int, // 间隔 pix private val includeEdge: Boolean = false // 是否显示边距) : ItemDecoration() { private val space = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, space.toFloat(), Resources.getSystem().displayMetrics ).toInt() override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { val lp = view.layoutParams as StaggeredGridLayoutManager.LayoutParams val lm = parent.layoutManager as StaggeredGridLayoutManager val spanIndex: Int = lp.spanIndex val spanCount: Int = lm.spanCount if (includeEdge) { outRect.left = space * (spanCount - spanIndex) / spanCount outRect.right = space * (spanIndex + 1) / spanCount outRect.top = space } else { outRect.left = space * spanIndex / spanCount outRect.right = space * (spanCount - spanIndex - 1) / spanCount outRect.bottom = space } }} 3)滑动时闪烁,Item自动切换位置 解决方案 layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE) 4)设置了 GAP_HANDLING_NONE 属性之后,顶端空白 由于ViewHolder的回收机制,滑回到上方时Item需要重新自行绘制,因为item的尺寸并不确定,导致重新绘制之后跟原来的高度不一致,又禁止了左右切换的调整策略,才导致的顶端空白。通常是因为加载图片之后重新计算item高度导致,尽可能在load图片之前先确定ImageView的高度。这个时候只能给后台同学提需求了,在服务的Json数据需要包含图片的长宽信息。附根据屏幕计算ImageView显示的宽高 1234567private val width = (DensityUtil.getScreenWidth() - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 68f,// 间距和padding Resources.getSystem().displayMetrics).toInt()) / 2 // spanCountprivate val height = (width * 1.5).toInt()imageView.layoutParams.height = height 5)刷新,删除,插入同样会导致顶端空白 使用 notifyDataSetChanged()方法做刷新时,会触发StaggeredGridLayoutManager 的onItemsChanged 方法,导致item的spanIndex重现进行计算,item所在列的位置出现了变化,导致了顶部空白。 使用notifyItemRangeInserted,notifyItemRangeChanged做局部刷新 则不会引起变化。 6)注意item等宽问题 采用StaggeredDividerItemDecoration的等宽计算方法。","link":"/blog/2020/09/04/RecyclerView%E5%AE%9E%E7%8E%B0%E7%80%91%E5%B8%83%E6%B5%81%E4%BB%A5%E5%8F%8A%E7%BB%86%E8%8A%82%E9%97%AE%E9%A2%98/"},{"title":"RecyclerView滑动到指定Item并置顶","text":"RecyclerView滑动到指定Item并置顶0x01 TopLinearSmoothScroller123456789101112import android.content.Contextimport androidx.recyclerview.widget.LinearSmoothScrollerclass TopLinearSmoothScroller(context: Context?) : LinearSmoothScroller(context) { public override fun getVerticalSnapPreference(): Int { return SNAP_TO_START } override fun getHorizontalSnapPreference(): Int { return SNAP_TO_START }} 0x02 TopScrollLinearLayoutManager1234567891011121314151617import android.content.Contextimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewclass TopScrollLinearLayoutManager(context: Context?, orientation: Int, reverseLayout: Boolean) : LinearLayoutManager(context, orientation, reverseLayout) { override fun smoothScrollToPosition( recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int ) { val linearSmoothScroller = TopLinearSmoothScroller(recyclerView?.context) linearSmoothScroller.targetPosition = position startSmoothScroll(linearSmoothScroller) }} 0x03 RecyclerView中使用设置recyclerView的layoutManager为自定义的TopScrollLinearLayoutManager,然后直接调用 smoothScrollToPosition() 方法就可以滚动到指定的位置并且置顶了。 1234567recyclerView.layoutManager = TopScrollLinearLayoutManager( this, LinearLayoutManager.HORIZONTAL, false)// ...recyclerView.smoothScrollToPosition(1)","link":"/blog/2023/03/24/RecyclerView%E6%BB%9A%E5%8A%A8%E5%88%B0%E6%8C%87%E5%AE%9Aitem%E5%B9%B6%E7%BD%AE%E9%A1%B6/"},{"title":"RecyclerView Item 嵌套 ScrollView","text":"RecyclerView Item 嵌套 ScrollViewRecyclerView Item 嵌套 ScrollView 产生 Touch 事件冲突,通过自定义ScrollView来拦截和处理事件 自定义 ItemScrollView 代码如下 12345678910111213141516171819202122232425262728293031323334353637383940414243444546import android.content.Contextimport android.util.AttributeSetimport android.view.MotionEventimport android.widget.ScrollViewclass ItemScrollView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ScrollView(context, attrs, defStyleAttr) { override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { parent.requestDisallowInterceptTouchEvent(true) return super.onInterceptTouchEvent(ev) } private var lastY: Float = 0f override fun onTouchEvent(ev: MotionEvent?): Boolean { when (ev?.action) { MotionEvent.ACTION_DOWN -> { lastY = ev.y } MotionEvent.ACTION_MOVE -> { val currentY = ev.y this.scrollBy(0, (lastY - currentY).toInt()) lastY = currentY } MotionEvent.ACTION_UP -> { lastY = 0f } } return canScroll() } private fun canScroll(): Boolean { val child = getChildAt(0) child?.let { return height < child.height } return false }}","link":"/blog/2021/06/17/RecyclerView%E7%9A%84item%E5%B5%8C%E5%A5%97ScrollView/"},{"title":"RecyclerView的几种Decoration","text":"RecyclerView的几种Decoration1234567891011121314151617181920212223242526272829303132333435363738394041import android.content.res.Resourcesimport android.graphics.Rectimport android.util.TypedValueimport android.view.Viewimport androidx.recyclerview.widget.RecyclerViewclass SimplePaddingDecoration( spaceDp: Int, val orientation: Int = RecyclerView.VERTICAL) : RecyclerView.ItemDecoration() { private val dividerHeight: Int = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, spaceDp.toFloat(), Resources.getSystem().displayMetrics ).toInt() override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition if (orientation == RecyclerView.VERTICAL) { // 竖直 if (position + 1 != parent.adapter?.itemCount) { outRect.set(0, 0, 0, dividerHeight) } else { outRect.set(0, 0, 0, 0) } } else { // 水平 if (position + 1 != parent.adapter?.itemCount) { outRect.set(0, 0, dividerHeight, 0) } else { outRect.set(0, 0, 0, 0) } } }}","link":"/blog/2021/06/17/RecyclerView%E7%9A%84%E5%87%A0%E7%A7%8DDecoration/"},{"title":"Retrofit动态切换域名(BaseUrl)","text":"Retrofit动态切换域名(BaseUrl)Retrofit只有在创建Retrofit对象时能设置BaseUrl,没有提供动态修改的Api。 123456val retrofit = Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(ResultCallAdapterFactory.create()) .client(okHttpClient) .build() 根据不同的使用场景,动态修改url主要有一下几种方式 0x01 @Get,@Post注解配置全路径熟悉 Retrofit 的开发者应该知道 @Get , @Post 这些标注到每个接口方法上的注解不仅可以传相对路径,还可以传全路径,这样我们就可以做到不同的接口使用不同的 BaseUrl ,从而达到使用多个 BaseUrl 的需求,但是注解上的值只能是 Final 的常量,不能动态改变,所以我称这个解决方案为静态解决方案 12345@GET("https://developer.android.com/login")fun login( @Query("mobile") mobile: String, @Query("code") code: String): Call<ResponseBody> 0x02 @Url支持全路径地址熟悉 Retrofit 的开发者也同样知道 @Url 这个标注到每个接口方法参数上的注解,它可以将全路径作为参数传进接口作为每次请求的 Url 地址,每次请求接口都可以将不同的全路径作为参数,从而达到支持多个 BaseUrl 以及在运行时动态改变 BaseUrl ,所以很多请求图片等资源的接口都是使用这个方案 123456@GETfun login( @Url url: String, @Query("mobile") mobile: String, @Query("code") code: String): Call<ResponseBody> 0x03 多个Retrofit对象即不同的 BaseUrl 使用不同的 Retrofit 对象。缺点是只要新增一个BaseUrl,就需要创建一个新的 Retrofit 对象 0x04 OkHttpClient添加域名切换拦截器自定义域名切换拦截器HostInterceptor,动态切换BaseUrl 1234567891011121314151617181920212223242526272829class HostInterceptor: Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest: Request = chain.request() val httpUrl: HttpUrl = originalRequest.url val builder: Request.Builder = originalRequest.newBuilder() val envList: List<String> = originalRequest.headers("Host-Env") if (envList.isNotEmpty()) { builder.removeHeader("Host-Env") val env = envList[0] var baseURL: HttpUrl? = null //根据头信息中配置的value,来匹配新的base_url地址 if ("ANDROID" == env) { baseURL = "https://developer.android.com/".toHttpUrlOrNull() } if (baseURL != null) { //重建新的HttpUrl,需要重新设置的url部分 val newHttpUrl: HttpUrl = httpUrl.newBuilder() .scheme(baseURL.scheme) //http协议如:http或者https .host(baseURL.host) //主机地址 .port(baseURL.port) //端口 .build() //获取处理后的新newRequest val newRequest: Request = builder.url(newHttpUrl).build() return chain.proceed(newRequest) } } return chain.proceed(originalRequest) }} 创建OkHttpClient对象,并添加域名切换拦截器HostInterceptor 123456val okHttpClient = OkHttpClient.Builder() .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS) .addInterceptor(HostInterceptor.create()) .build() 正常使用retrofit对象生成ApiService,请求服务走到拦截器并切换BaseUrl 123456@Header("Host-Env", "ANDROID")@GET("login")fun login( @Query("mobile") phone: String, @Query("code") code: String): Call<ResponseBody> PS:通过自定义Header标签,可以根据标签的类型仅动态切换指定类型的域名 特别感谢特别感谢以下博文 解决Retrofit多BaseUrl及运行时动态改变BaseUrl:https://www.jianshu.com/p/2919bdb8d09a","link":"/blog/2023/10/17/Retrofit%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E5%9F%9F%E5%90%8D-BaseUrl/"},{"title":"Sublime Text3 Shortcuts(mac)","text":"Shortcuts Action command+return 在当前行后插入新行 command+shift+return 在当前行前插入新行 command+control+up&down 交换上下行 command+shift+D 复制(多)行 control+shift+K 删除行 command+L 选择行 command+/ 注释 command+] 向右缩进 command+[ 向左缩进 command+F 查找 command+option+F 替换 command+B 编译 control+G 跳转到行","link":"/blog/2020/02/09/SublimeText%E5%BF%AB%E6%8D%B7%E9%94%AE/"},{"title":"TargetSdkVersion升级到30后文件存储","text":"TargetSdkVersion升级到30后文件存储Android 存储目录 私有存储 (Private Storage) : 每个应用在都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录。 内部存储私有目录 (/data/data/packageName) ; 外部存储私有目录 (/sdcard/Android/data/packageName); 共享存储 (Shared Storage) : 存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等目录。 外部存储:Environment.getExternalStorageDirectory()获取sdcard下的任意文件夹,在SDK29以上已经过期、失效。 外部存储和TargetSdkVersion兼容情况: targetSdkVersion = 28,运行后正常读写所有文件,如果不是必须的需求并且是新创建的项目的话,建议把文件按照规范存储在外部存储私有目录 (/sdcard/Android/data/packageName) targetSdkVersion = 29,targetSdkVersion 由 低版本 修改到 29,覆盖安装,运行后正常读写。 targetSdkVersion = 29,卸载旧应用,重新安装新应用,如果读写外部存储,程序崩溃 (open failed: EACCES (Permission denied)) targetSdkVersion = 29,添加android:requestLegacyExternalStorage=”true”(不启用分区存储),读写正常不报错 targetSdkVersion = 30,targetSdkVersion 由 低版本 修改到 30,覆盖安装,读写报错,程序崩溃 (open failed: EACCES (Permission denied)) targetSdkVersion = 30,targetSdkVersion 由 低版本 修改到 30,覆盖安装,增加 android:preserveLegacyExternalStorage=”true”,读写正常不报错 targetSdkVersion = 30,卸载旧应用,重新安装新应用,不管设置任何配置,如果读写外部存储,程序崩溃 (open failed: EACCES (Permission denied)) 特别感谢特别感谢以下博文 targetSdkVersion 升级到29、30文件处理:https://www.jianshu.com/p/892a2ca5c41e","link":"/blog/2023/11/03/TargetSdkVersion%E5%8D%87%E7%BA%A7%E5%88%B030%E5%90%8E%E6%96%87%E4%BB%B6%E5%AD%98%E5%82%A8/"},{"title":"Terminal设置代理","text":"0x01 Terminal代理设置给终端terminal设置代理 12345$ vim ~/.zshrc## proxyexport http_proxy=http://127.0.0.1:8001export https_proxy=$http_proxy$ source ~/.zshrc 0x02 AndroidStudio代理设置Android Studio 的代理设置 Perference --> Appearance & Behavior --> System Settings --> Http Proxy 0x03 Gradle代理设置gradle代理可以配置在: 环境变量GRADLE_USER_HOME指定的gradle系统目录默认路径\\Users\\Xxx\\.gradle\\gradle.properties 项目根目录gradle.properties 前者优先级更高。 123456systemProp.http.proxyHost=127.0.0.1systemProp.http.proxyPort=1080systemProp.https.proxyHost=127.0.0.1systemProp.https.proxyPort=1080systemProp.socks.proxyHost=127.0.0.1systemProp.socks.proxyPort=1080 0x04 配置代理用户名和密码1234567891011121314# Project-wide Gradle settings....systemProp.http.proxyHost=proxy.company.comsystemProp.http.proxyPort=443systemProp.http.proxyUser=usernamesystemProp.http.proxyPassword=passwordsystemProp.http.auth.ntlm.domain=domainsystemProp.https.proxyHost=proxy.company.comsystemProp.https.proxyPort=443systemProp.https.proxyUser=usernamesystemProp.https.proxyPassword=passwordsystemProp.https.auth.ntlm.domain=domain... 0x05 过滤代理对于国内的仓库可以不走代理,还有部分内网地址也可以不走代理 123systemProp.http.nonProxyHosts=developer.huawei.com|maven.aliyun.com|192.168.*systemProp.https.nonProxyHosts=developer.huawei.com|maven.aliyun.com|192.168.*systemProp.socks.nonProxyHosts=developer.huawei.com|maven.aliyun.com|192.168.* 0x06 修改gradle.properties配置1org.gradle.jvmargs=-DsocksProxyHost=127.0.0.1 -DsocksProxyPort=10808","link":"/blog/2021/05/27/Terminal%E5%92%8CAndroidStudio%E4%BB%A3%E7%90%86%E8%AE%BE%E7%BD%AE/"},{"title":"TextView文字颜色渐变","text":"TextView文字颜色渐变1234567891011121314151617181920212223import android.graphics.LinearGradientimport android.graphics.Shaderimport android.widget.TextView/** * 左到右渐变 */fun TextView.setHorizontalGradientTextColor(startColor: Int, endColor: Int) { val x1 = this.paint.measureText(this.text.toString())//测量文本 宽度 val shader = LinearGradient(0f, 0f, x1, 0f, startColor, endColor, Shader.TileMode.CLAMP) this.paint.shader = shader this.invalidate()}/** * 上到下渐变 */fun TextView.setVerticalGradientTextColor(startColor: Int, endColor: Int) { val y1 = this.paint.textSize//测量文本 高度 val shader = LinearGradient(0f, 0f, 0f, y1, startColor, endColor, Shader.TileMode.CLAMP) this.paint.shader = shader this.invalidate()} 使用方式,调用扩展方法即可 textView.setHorizontalGradientTextColor(Color.RED, Color.GREEN)","link":"/blog/2023/09/11/TextView%E6%96%87%E5%AD%97%E9%A2%9C%E8%89%B2%E6%B8%90%E5%8F%98/"},{"title":"Vim入门 - Vim Tutor","text":"第一讲小结 光标在屏幕文本中的移动既可以用箭头键,也可以使用 hjkl 字母键。h (左移) j (下行) k (上行) l (右移) 欲进入 Vim 编辑器(从命令行提示符),请输入:vim 文件名 <回车> 欲退出 Vim 编辑器,请输入 `<ESC>` :q! `<回车>` 放弃所有改动。 `<ESC>` :wq `<回车>` 保存改动。 在正常模式下删除光标所在位置的字符,请按: x 欲插入或添加文本,请输入: i 输入欲插入文本 `<ESC>` 在光标前插入文本 I 输入欲插入文本 `<ESC>` 在一行前插入文本 a 输入欲添加文本 `<ESC>` 在光标后添加文本 A 输入欲添加文本 `<ESC>` 在一行后添加文本 特别提示:按下 <ESC> 键会带您回到正常模式或者撤消一个不想输入或部分完整的命令。 第二讲小结 欲从当前光标删除至下一个单词,请输入:dw 欲从当前光标删除至当前行末尾,请输入:d$ 欲删除整行,请输入:dd 欲重复一个动作,请在它前面加上一个数字:2w 在正常模式下修改命令的格式是: operator [number] motion其中: operator - 操作符,代表要做的事情,比如 d 代表删除 [number] - 数字,代表动作重复的次数 motion - 动作,代表在所操作的文本上的移动 w 后面一个word W 后面一个WORD b 前面一个word B 前面一个WROD e 代表单词末尾(end) ge 前面一个单词 0 行首 $ 行末 gg 文档首 G 文档尾 欲移动光标到行首,请按数字0键:0 欲撤消以前的操作,请输入:u (小写的u)欲撤消在一行中所做的改动,请输入:U (大写的U)欲撤消以前的撤消命令,恢复以前的操作结果,请输入:CTRL-R 第三讲小结 要重新置入已经删除的文本内容,请按小写字母 p 键。该操作可以将已删除的文本内容置于光标之后。如果最后一次删除的是一个整行,那么该行将置于当前光标所在行的下一行。 要替换光标所在位置的字符,请输入小写的 r 和要替换掉原位置字符的新字符即可。 更改类命令允许您改变从当前光标所在位置直到动作指示的位置中间的文本。比如输入 ce 可以替换当前光标到单词的末尾的内容;输入 c$ 可以替换当前光标到行末的内容。 更改类命令的格式是: c [number] motion 第四讲小结 CTRL-G 用于显示当前光标所在位置和文件状态信息。G 用于将光标跳转至文件最后一行。先敲入一个行号然后输入大写 G 则是将光标移动至该行号代表的行。gg 用于将光标跳转至文件第一行。 输入 / 然后紧随一个字符串是在当前所编辑的文档中正向查找该字符串。输入 ? 然后紧随一个字符串则是在当前所编辑的文档中反向查找该字符串。完成一次查找之后按 n 键是重复上一次的命令,可在同一方向上查找下一个匹配字符串所在;或者按大写 N 向相反方向查找下一匹配字符串所在。CTRL-O 带您跳转回较旧的位置,CTRL-I 则带您到较新的位置。 如果光标当前位置是括号(、)、[、]、{、},按 % 会将光标移动到配对的括号上。 在一行内替换头一个字符串 old 为新的字符串 new,请输入 :s/old/new在一行内替换所有的字符串 old 为新的字符串 new,请输入 :s/old/new/g在两行内替换所有的字符串 old 为新的字符串 new,请输入 :#,#s/old/new/g在文件内替换所有的字符串 old 为新的字符串 new,请输入 :%s/old/new/g进行全文替换时询问用户确认每个替换需添加 c 标志 :%s/old/new/gc 第五讲小结 :!command 用于执行一个外部命令 command。 请看一些实际例子:(MS-DOS) (Unix):!dir :!ls - 用于显示当前目录的内容。:!del FILENAME :!rm FILENAME - 用于删除名为 FILENAME 的文件。 :w FILENAME 可将当前 VIM 中正在编辑的文件保存到名为 FILENAME 的文件中。 v motion :w FILENAME 可将当前编辑文件中可视模式下选中的内容保存到文件FILENAME 中。 :r FILENAME 可提取磁盘文件 FILENAME 并将其插入到当前文件的光标位置后面。 :r !dir 可以读取 dir 命令的输出并将其放置到当前文件的光标位置后面。 第六讲小结 输入小写的 o 可以在光标下方打开新的一行并进入插入模式。输入大写的 O 可以在光标上方打开新的一行。 输入小写的 a 可以在光标所在位置之后插入文本。输入大写的 A 可以在光标所在行的行末之后插入文本。 e 命令可以使光标移动到单词末尾。 操作符 y 复制文本,p 粘贴先前复制的文本。 输入大写的 R 将进入替换模式,直至按 <ESC> 键回到正常模式。 输入 :set xxx 可以设置 xxx 选项。一些有用的选项如下:‘ic’ ‘ignorecase’ 查找时忽略字母大小写‘is’ ‘incsearch’ 查找短语时显示部分匹配‘hls’ ‘hlsearch’ 高亮显示所有的匹配短语选项名可以用完整版本,也可以用缩略版本。 在选项前加上 no 可以关闭选项: :set noic 第七讲小结 输入 :help 或者按 <F1> 键或 <Help> 键可以打开帮助窗口。 输入 :help cmd 可以找到关于 cmd 命令的帮助。 输入 CTRL-W CTRL-W 可以使您在窗口之间跳转。 输入 :q 以关闭帮助窗口 您可以创建一个 vimrc 启动脚本文件用来保存您偏好的设置。 当输入 : 命令时,按 CTRL-D 可以查看可能的补全结果。按 <TAB> 可以使用一个补全。","link":"/blog/2020/02/17/VimTutor/"},{"title":"Vim 编程实践","text":"1. 基础设置1234567891011121314"显示行set nu" 设置屏幕滚动时在光标上下方保留5行预览代码set so=5" 设置debug为 zdnnoremap zd :action Debug<CR>" 设置run 为 zrnnoremap zr :action Run<CR>" 插入模式下 jk 映射 Escinoremap jk <Esc> 2. 导航和查找hjkl 光标在屏幕文本中的移动 w 后面一个word W 后面一个WORD b 前面一个word B 前面一个WROD e 代表单词末尾(end) ge 前面一个单词 0 行首 $ 行末 gg 文档首 G 文档尾 :<行号> or <行号>G or <行号>gg - 快速定位到行 <行号><回车> - 往下跳转行数 <行号>[j/k] - 往下、往上跳转行数 2.1 查找fa - 查找该行从光标位置首次出现a的位置/text - 查找text,按n健查找下一个,按N健查找前一个。?text - 查找text,反向查找,按n健查找下一个,按N健查找前一个。% - 括号匹配vim中有一些特殊字符在查找时需要转义 .*[]^%/?~$ 2.2 替换ra - 将当前字符替换为a,当期字符即光标所在字符。s/old/new/ - 用old替换new,替换当前行的第一个匹配s/old/new/g - 用old替换new,替换当前行的所有匹配%s/old/new/ - 用old替换new,替换所有行的第一个匹配%s/old/new/g - 用old替换new,替换整个文件的所有匹配:10,20 s/^/ /g - 在第10行知第20行每行前面加四个空格,用于缩进。 2.3 滚屏C-B - 滚动屏幕上一屏 C-F - 滚动屏幕下一屏 Ctrl + F 屏幕向下滚动一屏Ctrl + B 屏幕向上滚动一屏Ctrl + E 屏幕向下滚动一行Ctrl + Y 屏幕向上滚动一行Ctrl + D 屏幕向下滚动半屏Ctrl + U 屏幕向上滚动半屏 3. 编辑o - 在当前行下方插入新行并自动缩进 O - 在当前行上方插入新行并自动缩进 i - 在当前字符左方开始插入字符 a - 在当前字符右方开始插入字符 I - 光标移动到行首并进入插入模式 A - 光标移动到行尾并进入插入模式 s - 删除光标所在字符并进入插入模式 S - 删除光标所在行并进入插入模式 r - 修改光标所在字符,然后返回普通模式 R - 进入覆盖模式 p - 粘贴 3.1 范围指令c<范围> - 删除光标所在位置周围某个范围的文本并进入插入模式。 常用的组合有:caw - 删除一个单词包括它后面的空格并开始插入;ciw - 删除一个单词并开始插入;ci” - 删除一个字符串内部文本并开始插入;ct字符 − 从光标位置删除本行某个字符之前(保留该字符)并开始插入;C − 删除光标位置到行尾的内容并进入插入模式(相当于c$) d<范围> - 删除一定范围内的文本 y<范围> - 将范围内的文本放入0号和”号注册栏 v<范围> - 选择范围内的文本 =<范围> - 自动缩进范围内的文本 gU<范围> - 将范围内的字符转换为大写 gu<范围> - 将范围内的字符转换为小写 3.2 范围$ - 从光标位置到行尾 ^ - 从光标位置到行首,不包含缩进空白 0 - 从光标位置到行首,包含缩进空白 gg - 从光标位置到文件开头 G - 从光标位置到文件结尾 % - 从光标位置到另一边匹配的括号 f<字符> - 从光标位置到光标右边某个字符首次出现的位置,包括该字符 F<字符> - 从光标位置到光标左边某个字符首次出现的位置,包括该字符 t<字符> - 从光标位置到光标右边某个字符首次出现的位置,包括该字符 T<字符> - 从光标位置到光标左边某个字符首次出现的位置,包括该字符 <空格> - 光标所在位置 (gU空格 表示将光标位置字符转为大写) 范围 a & ia 可理解为“一个”, i 可理解为 in. aw - 一个单词加一个空格 iw - 一个单词 a” - 一个字符串包括双引号 i” - 一个字符串内部文本 a< - 一组< >包含的文本,包括< >号本身 i< - 一组< >内部文本 同理类推: a[, i[, a(, i(, a{, i{ 3.3 对光标所在行操作。dd 删除一行 yy 复制一行 cc 删除一行文本并开始插入 == 自动缩进当前行 >> 当前行缩进一格 << 当前行减少缩进 4. 常用命令daw 删除一个单词ndd 删除当前行之后的n行xp 交换当前字符和其后一个字符ddp 交换行yyp 复制行ci” 删除””内的字符串并插入ca{ 删除{}内容并插入,包含{}本身:1,10d 删除1-10行,利用p命令可将剪切后的内容进行粘贴:1,$d 删除所有行J 删除两行之间的空行,实际上是合并两行P 在当前行前粘贴:1,10 co 20 将1-10行插入到第20行之后:1,$ co $ 将整个文件复制一份并添加到文件尾部:1, 10 m 20 将第1-10行移动到第20行之后","link":"/blog/2020/03/14/Vim%E7%BC%96%E7%A8%8B%E5%AE%9E%E8%B7%B5/"},{"title":"Windows 上配置 Git SSH","text":"Windows 上配置 Git SSHhttps://support.atlassian.com/bitbucket-cloud/docs/set-up-an-ssh-key/ 0x01 Set up SSH for Git on WindowsUse this section to create a default identity and SSH key when you’re using Git on Windows. By default, the system adds keys for all identities to the /Users/<username>/.ssh directory. Step 1. Set up your default identityFrom the command line, enter ssh-keygen. ssh-keygen -t rsa -b 4096 -C "email" List the contents of .ssh to view the key files. 12$ dir .ssh id_rsa id_rsa.pub Step 2. Add the key to the ssh-agentIf you don’t want to type your password each time you use the key, you’ll need to add it to the ssh-agent. 12$ eval $(ssh-agent) $ ssh-add ~/.ssh/id_rsa Step 3. Add the public key to your Account settingsOpen your .ssh/id_rsa.pub file and copy its contents. From Bitbucket, Paste the copied public key into the SSH Key field. Click Save. Step 4. Test connectionReturn to the command line and verify your configuration and username by entering the following command: 1$ ssh -T [email protected] 0x02 Working with non-default SSH key pair pathsIf you used a non-default file path for your GitLab SSH key pair,you must configure your SSH client to find your GitLab SSH private keyfor connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients this is configured in the ~/.ssh/config file.Below are two example host configurations using their own key: 123456789# GitLab.com serverHost gitlab.comRSAAuthentication yesIdentityFile ~/.ssh/config/private-key-filename-01# Private GitLab serverHost gitlab.company.comRSAAuthentication yesIdentityFile ~/.ssh/config/private-key-filename 0x03 使用ed25519方式生成SSH秘钥如果使用rsa加密方式,出现秘钥无效,依旧提示输入用户名密码验证,请通过ed25519加密方式,生成ssh秘钥。 ssh-keygen -t ed25519 -C "email" -b 4096 对应生成的私钥 id_ed25519 和公钥 id_ed25519.pub, 其他配置步骤同rsa生成方式一致。","link":"/blog/2022/06/20/Windows%E4%B8%8A%E9%85%8D%E7%BD%AESSH/"},{"title":"Vim 升级和配置","text":"1. Vim 升级1brew install vim --with-lua --with-override-system-vim 以后想更新 vim 版本,直接输入 brew upgrade vim 。 2. 配置自己的 .vimrc 文件1234567set nu " 显示行号set showcmd " 显示输入的命令set ruler " 显示标尺syntax on " 自动语法高亮set nobackup " 不自动备份set hlsearch " 搜索高亮set vb t_vb= " 没有错误提示音 3. vundle 管理插件3.1 安装1git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim 3.2 配置插件1234567891011121314151617181920212223242526272829303132333435363738394041424344454647set nocompatible " be iMproved, requiredfiletype off " required" set the runtime path to include Vundle and initializeset rtp+=~/.vim/bundle/Vundle.vimcall vundle#begin()" alternatively, pass a path where Vundle should install plugins"call vundle#begin('~/some/path/here')" let Vundle manage Vundle, requiredPlugin 'VundleVim/Vundle.vim'" The following are examples of different formats supported." Keep Plugin commands between vundle#begin/end." plugin on GitHub repoPlugin 'altercation/vim-colors-solarized'Plugin 'Lokaltog/vim-powerline'Plugin 'octol/vim-cpp-enhanced-highlight'Plugin 'Raimondi/delimitMate'" plugin from http://vim-scripts.org/vim/scripts.html" Plugin 'L9'" Git plugin not hosted on GitHubPlugin 'git://git.wincent.com/command-t.git'" git repos on your local machine (i.e. when working on your own plugin)Plugin 'file:///home/gmarik/path/to/plugin'" The sparkup vim script is in a subdirectory of this repo called vim." Pass the path to set the runtimepath properly.Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}" Install L9 and avoid a Naming conflict if you've already installed a" different version somewhere else." Plugin 'ascenator/L9', {'name': 'newL9'}" All of your Plugins must be added before the following linecall vundle#end() " requiredfiletype plugin indent on " required" To ignore plugin indent changes, instead use:"filetype plugin on"" Brief help" :PluginList - lists configured plugins" :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate" :PluginSearch foo - searches for foo; append `!` to refresh local cache" :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal"" see :h vundle for more details or wiki for FAQ" Put your non-Plugin stuff after this line 123456" 配色方案"set background=dark"colorscheme solarized" 设置状态栏主题风格let g:Powerline_colorscheme='solarized256' 3.3 插件管理命令123:PluginInstall:PluginClean :PluginUpdate","link":"/blog/2020/02/19/Vim%E5%8D%87%E7%BA%A7%E5%92%8C%E9%85%8D%E7%BD%AE/"},{"title":"Windows 部署 AIGC 图片生成服务——基于 stable-diffusion","text":"Windows 部署 AIGC 图片生成服务——基于 stable-diffusion0x01 系统环境Windows 10 专业版 64 位操作系统 11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz (8 核) Intel(R) UHD Graphics 750 0x02 安装 python3 和 gitDownload and installgitandPython 3.10.6(tick Add to PATH) 0x03 安装 pytorchhttps://pytorch.org/get-started/locally/ 由于当前 PC 的显卡不是英伟达,所以下载的 tarch 版本是 cup 版本 检测 pytorch 是否安装成功 12python # 打开python环境import torch 没有报异常即说明安装成功 0x04 安装 stable-diffusion-webuihttps://github.com/AUTOMATIC1111/stable-diffusion-webui 由于当前 PC 的 CUP 和 GPU 都是 Intel,所以下载 openvino 版本 123git clone https://github.com/openvinotoolkit/stable-diffusion-webui.gitcd stable-diffusion-webui.\\webui-user.bat # 这里会开始下载,并且安装需要的软件,等待完成 0x05 下载网络上已经训练好的 AI 模型现在我们还需要一个训练好的 AI 模型来指导生成效果,在如下网站中可以下载到很多训练好的模型: https://civitai.com/ 选择一个感兴趣的模型,下载好的,将模型文件放入工程目录下的 models/Stable-diffusion 文件夹下面即可。 0x06 本地运行下面可以尝试运行下此 Web 项目,在工程目录下执行: 1.\\webui-user.bat 执行完成后,在浏览器打开如下地址: http://127.0.0.1:7860/ 0x07 自定义 OpenVINO 脚本https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Custom-Scripts#accelerate-with-openvino 选择自定义脚本 Accelerate with OpenVINO,按照文档配置相关参数。之后就可体验 stable-diffusion 生成图片了。 Good Luck. 参考链接:https://huishao.cc/2023/07/30/478Mac%E9%83%A8%E7%BD%B2AIGC%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90%E6%9C%8D%E5%8A%A1%E2%80%94%E2%80%94%E5%9F%BA%E4%BA%8Estable-diffusion/","link":"/blog/2024/03/07/Windows%E9%83%A8%E7%BD%B2stable-diffusion/"},{"title":"ZSH & iTerm2 的最佳实现","text":"ZSH & iTerm2 的最佳实现 最终效果 0x01 安装 iTerm 2iTerm 2 官网: https://www.iterm2.com/ 0x02 设置字体安装 powerline 字体, 适配 Agnoster 主题 Powerline Font Github : https://github.com/powerline/fonts 在iTerm 2 设置 powerline 字体,个人喜欢 Source Code Pro for Powerline 。 0x03 设置颜色方案Solarized Github: https://github.com/altercation/solarized 在 iTerm 2 已经预安装了 Solarized 配色方案,在 color 配色方案选择 Solarized Dark 。 完了可以选择一张背景图片,并设置合适的透明度。 0x04 安装 oh-my-zshmac os 预装了 zsh,切换 shell : 1chsh -s $(which zsh) oh my zsh 官网:https://github.com/ohmyzsh/ohmyzsh oh-my-zsh 的安装: 1234# curlsh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"# or wgetsh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" bash 的环境变量配置文件 ~/.bash_profile , 而 zsh 的环境变量配置文件是 ~/.zshrc。 0x05 agnoster 主题oh my zsh 官方为用户提供了上百种主题,在 ~/.zshrc 文件中配置主题: 1ZSH_THEME="agnoster" Agnoster 官网: https://github.com/agnoster/agnoster-zsh-theme 测试字体是否支持 1$ echo "\\ue0b0 \\u00b1 \\ue0a0 \\u27a6 \\u2718 \\u26a1 \\u2699" 路径前缀太长的问题,在 ~/.oh-my-zsh/themes 路径下找到 agnoster.zsh-theme 文件,将里面的 build_prompt 下的 prompt_context 字段,注释掉即可。 0x06 oh-my-zsh插件配置在 ~/.zshrc 文件中配置插件: 1plugins=(autojump sublime osx zsh-syntax-highlighting git-dw) 1. 推荐插件 autojumpj 是 autojump 命令的简写 j: 快速跳转 jo: 快速打开 ps: 需要安装autojump的mac平台插件配合使用 2. 推荐插件 osx ofd: 用 Finder 打开当前目录 cdf: cd 到当前 Finder 目录 pfd: 打印当前 Finder 路径 pfs: 打印当前 Finder 选择的文件路径 3. 推荐插件 git gl: git pull gs: git status ga: git add gaa: git add –all gc: git commit gcm: git commit -m gp: git push gb: git branch gba: git branch -a gbd: git branch -d gco: git checkout 4. 推荐插件 sublime st: 用 Sublime Text 打开文件 stt: 用 Sublime Text 打开当前目录 sst: sudo st,用于编辑系统文件 find_project: 从当前目录往上(parent directory)查找 sublime, git 工程 create_project: 创建 sublime 工程 0x07 自定义插件由于 zsh 推荐的插件 git 有一些别名比较难记,所以考虑自定义一个插件 git-dw 。 1. 创建 git-dw.plugin.zsh 文件在 ~/.oh-my-zsh/custom/plugins/ 创建 /git-dw/git-dw.plugin.zsh 。 2. 自定义别名编辑 git-dw.plugin.zsh 文件: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102## Aliases#alias g='git'alias ga='git add'alias gaa='git add --all'alias gb='git branch'alias gba='git branch -a'alias gbd='git branch -d'alias gbD='git branch -D'alias gc='git commit'alias gcm='git commit -m'alias gc!='git commit --amend'# alias gcam='git commit -a -m'# alias gcsm='git commit -s -m'alias gco='git checkout'alias gcob='git checkout -b'alias gcf='git config --list'# alias gcl='git clone --recurse-submodules'# alias gclean='git clean -id'# alias gpristine='git reset --hard && git clean -dfx'alias gf='git fetch'alias gfap='git fetch --all --prune'alias ggpull='git pull origin "$(git_current_branch)"'alias ggpush='git push origin "$(git_current_branch)"'alias gh='git help'alias gl='git pull'# alias glr='git pull --rebase'# alias glrv='git pull --rebase -v'# alias glra='git pull --rebase --autostash'# alias glrav='git pull --rebase --autostash -v'# alias glum='git pull upstream master'alias gm='git merge'# alias gmom='git merge origin/master'# alias gmt='git mergetool --no-prompt'# alias gmtvim='git mergetool --no-prompt --tool=vimdiff'# alias gmum='git merge upstream/master'# alias gma='git merge --abort'alias gp='git push'alias gr='git remote'alias grv='git remote -v'alias gra='git remote add'alias grmv='git remote rename'alias grrm='git remote remove'alias grset='git remote set-url'# alias grb='git rebase'# alias grba='git rebase --abort'# alias grbc='git rebase --continue'# alias grbd='git rebase develop'# alias grbi='git rebase -i'# alias grbm='git rebase master'# alias grbs='git rebase --skip'# alias grev='git revert'# alias grh='git reset'# alias grhh='git reset --hard'# alias groh='git reset origin/$(git_current_branch) --hard'# alias grm='git rm'# alias grmc='git rm --cached'# alias grs='git restore'# alias grss='git restore --source'# alias grt='cd "$(git rev-parse --show-toplevel || echo .)"'# alias gru='git reset --'# alias grup='git remote update'# alias grv='git remote -v'alias gs='git status'alias gst='git stash'# alias gstaa='git stash apply'# alias gstc='git stash clear'# alias gstd='git stash drop'# alias gstl='git stash list'# alias gstp='git stash pop'# alias gsts='git stash show --text'# alias gstall='git stash --all'# alias gsu='git submodule update'# alias gsw='git switch'# alias gswc='git switch -c'alias gt='git tag'# alias gts='git tag -s'# alias gtv='git tag | sort -V' 3. 自定义方法123456789# Pretty log messagesfunction _git_log_prettily(){ if ! [ -z $1 ]; then git log --pretty=$1 fi}compdef _git _git_log_prettily=git-log... 4. 添加 README.md5. 应用插件在 ~/.zshrc 文件中应用插件: 1plugins=(... git-dw) 现在你重启 shell 就可以看到结果了。 0x08 配置环境变量在 ~/.zshrc 文件最顶部添加: 1export PATH=/Users/**/Library/Android/sdk/platform-tools:$PATH","link":"/blog/2020/02/11/ZSH%E5%92%8CiTerm2%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E7%8E%B0/"},{"title":"mac快捷键常用键盘符号","text":"符号 键位 ⌘ command ⌃ control ⌥ option ⇧ shift ↩ return ⌫ delete","link":"/blog/2020/02/09/mac%E5%BF%AB%E6%8D%B7%E9%94%AE%E5%B8%B8%E7%94%A8%E9%94%AE%E7%9B%98%E7%AC%A6%E5%8F%B7/"},{"title":"为git仓库设置独立的账户和邮箱","text":"为 git 仓库设置独立的账户和邮箱0x01 配置 Git 全局默认的账户和邮箱123456## 全局默认的用户名和邮箱$ git config --global user.name "aaa"$ git config --global user.email "[email protected]"## 避免每次git操作都需要输入用户名密码$ git config --global credential.helper store 0x02 为 git 仓库设置独立的账户和邮箱使用编辑器打开.git/config文件,直接在文件的最后添加以下信息: 123[user] name = bbb email = [email protected]","link":"/blog/2024/05/17/%E4%B8%BAgit%E4%BB%93%E5%BA%93%E8%AE%BE%E7%BD%AE%E7%8B%AC%E7%AB%8B%E7%9A%84%E8%B4%A6%E6%88%B7%E5%92%8C%E9%82%AE%E7%AE%B1/"},{"title":"Vim 入门","text":"1. Vim 入门资料1.1 vim 的 tutor终端直接输入:vimtutor 1.2 vim manual/usr/share/vim/vim81/doc 1.3 github vim 入门到精通Vim 从入门到精通: https://github.com/wsdjeg/vim-galore-zh_cn 2. Vim 入门首个目标 把jk映射成。具体指令为inoremap jk <Esc>。这条是最重要的一条。设置完这条之后几乎马上就可以体会到vim的好处了。可以试试用^$移动到行首行末,用w移动到单词结尾,ddp交换上下两行位置,yyp复制当前行,gg跳到文件开头,G跳到文件结尾,gd跳到定义,/def跳到下个函数开始的地方。用cw修改当前单词,用cf.直接修改到下个.号。用ci(直接修改括号里的内容。写代码时,每当写完一段有小停顿,习惯性按下jk进入normal模式。 不要折腾vim插件,直接用vim。我的建议是将你原来使用的ide中的vim插件打开就行了。不要浪费太多时间在快捷键的配置上。目前对我来说jk到esc就是全部需要的配置,在可见的未来我也准备用ctrl+c或者ctrl+[替换掉jk。 适应面向搜索编程的思想。我个人感觉vim快最重要的原因就是精准选择。而实现精准选择的必要途径就是搜索。搜索是vim的核心。比如说你需要跳到下个函数,那直接/def。再比如需要跳到括号末,直接f)。终端中,刚输入完python eval.py device cuda data.batch_size 256,发现这行指令中的python要改成python3,可以 直接Fna3。又发现前面要加sudo,直接^isudo。总之一切精确修改都是通过搜索来完成的,这与我们不使用vim编程时搜索只用来查找代码有本质区别。 精确跳转+可视化模式。掌握精确跳转后,结合v V ctrl+v 这三种可视化模式进行精确选择,配合cdyrp等指令,可以完成极其灵活的代码增删改查。到这一步你会发现vim对你编程速度的提高已经是革命性的了。","link":"/blog/2020/02/14/vim%E5%85%A5%E9%97%A8/"},{"title":"多线程编程","text":"多线程编程0x01 生产者和消费者问题1.BlockingQueue 阻塞队列实现123456789101112131415161718192021222324252627// 生产者class Producer implements Runnable { private final BlockingQueue<Integer> queue; public Producer(BlockingQueue queue) { this.queue = queue; } @Override public void run() { while (true) { try { queue.put(produce()); System.out.println(Thread.currentThread().getName() + " put a message, total = " + queue.size()); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } private int produce() { int x = (int) (Math.random() * 10); System.out.println(Thread.currentThread().getName() + " put: " + x); return x; }} 1234567891011121314151617181920212223// 消费者class Consumer implements Runnable { private final BlockingQueue<Integer> queue; Consumer(BlockingQueue q) { queue = q; } public void run() { try { while (true) { System.out.println(Thread.currentThread().getName() + " take..."); consume(queue.take()); } } catch (InterruptedException ex) { ex.printStackTrace(); } } void consume(int x) { System.out.println(Thread.currentThread().getName() + " take a message :" + x + ", total = " + queue.size()); }} 1234567891011public static void main(String[] args) { BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>(3); Producer cooker = new Producer(blockingQueue); Consumer waiter = new Consumer(blockingQueue); ExecutorService s = Executors.newFixedThreadPool(5); s.submit(cooker); s.submit(cooker); s.submit(cooker); s.submit(waiter); s.submit(waiter);} 2.使用Object的wait/notify方法实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788public class 消费者模式2 { public static void main(String[] args) { WorkBench wb = new WorkBench(); Waiter waiter = new Waiter(wb); Cooker cooker = new Cooker(wb); new Thread(cooker, "厨师1号").start(); new Thread(cooker, "厨师2号").start(); new Thread(cooker, "厨师3号").start(); new Thread(waiter, "服务员1号").start(); new Thread(waiter, "服务员2号").start(); }}// 生产者:厨师 -> 做菜class Cooker implements Runnable { WorkBench wb; public Cooker(WorkBench wb) { this.wb = wb; } @Override public void run() { while (true) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } wb.put(); } }}// 消费者:服务员 -> 取菜class Waiter implements Runnable { WorkBench wb; public Waiter(WorkBench wb) { this.wb = wb; } @Override public void run() { while (true) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } wb.take(); } }}// 产品队列:(类似 BlockingQueue)class WorkBench { public static final int MAX = 3; private volatile int queue = 0; public synchronized void put() { while (queue >= MAX) { System.out.println(String.format("%s waiting...", Thread.currentThread().getName())); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } queue++; System.out.println(String.format("%s put()一盘菜,队列还有%d盘菜", Thread.currentThread().getName(), queue)); notifyAll(); } public synchronized void take() { while (queue <= 0) { System.out.println(String.format("%s waiting...", Thread.currentThread().getName())); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } queue--; System.out.println(String.format("%s take()一盘菜,队列还剩%d盘菜", Thread.currentThread().getName(), queue)); notifyAll(); }} 3.使用ReentrantLock的await/signal实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100import java.util.Random;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class 消费者模式3 { public static void main(String[] args) { WorkBench wb = new WorkBench(); Waiter waiter = new Waiter(wb); Cooker cooker = new Cooker(wb); new Thread(cooker, "厨师1号").start(); new Thread(cooker, "厨师2号").start(); new Thread(cooker, "厨师3号").start(); new Thread(waiter, "服务员1号").start(); new Thread(waiter, "服务员2号").start(); }}// 生产者:厨师 -> 做菜class Cooker implements Runnable { WorkBench wb; public Cooker(WorkBench wb) { this.wb = wb; } @Override public void run() { while (true) { try { Thread.sleep(new Random().nextInt(3000)); } catch (InterruptedException e) { e.printStackTrace(); } wb.put(); } }}// 消费者:服务员 -> 取菜class Waiter implements Runnable { WorkBench wb; public Waiter(WorkBench wb) { this.wb = wb; } @Override public void run() { while (true) { try { Thread.sleep(new Random().nextInt(2000)); } catch (InterruptedException e) { e.printStackTrace(); } wb.take(); } }}// 产品队列:(类似 BlockingQueue)class WorkBench { public static final int MAX = 4; private volatile int queue = 0; private final ReentrantLock lock = new ReentrantLock(); final Condition full = lock.newCondition(); final Condition empty = lock.newCondition(); public void put() { lock.lock(); while (queue >= MAX) { System.out.println(String.format("%s waiting...", Thread.currentThread().getName())); try { full.await(); } catch (InterruptedException e) { e.printStackTrace(); } } queue++; System.out.println(String.format("%s put()一盘菜,队列还有%d盘菜", Thread.currentThread().getName(), queue)); empty.signalAll(); lock.unlock(); } public void take() { lock.lock(); while (queue <= 0) { System.out.println(String.format("%s waiting...", Thread.currentThread().getName())); try { empty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } queue--; System.out.println(String.format("%s take()一盘菜,队列还剩%d盘菜", Thread.currentThread().getName(), queue)); full.signalAll(); lock.unlock(); }} 0x02 多线程打印ABCD题目描述: 有4个线程和1个公共的字符数组。线程1的功能就是向数组输出A,线程2的功能就是向字符输出B,线程3的功能就是向数组输出C,线程4的功能就是向数组输出D。要求按顺序向数组赋值ABCDABCDABCD,ABCD的个数由线程函数1的参数指定。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485import java.util.Scanner;public class PrintABCD { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNext()) { int n = in.nextInt(); startPrintABCD(n); } } public static void startPrintABCD(int n) { sb = new StringBuffer(); Object a = new Object(); Object b = new Object(); Object c = new Object(); Object d = new Object(); Thread t1 = new Thread(new Task(n, a, b, 'A', 0)); Thread t2 = new Thread(new Task(n, b, c, 'B', 1)); Thread t3 = new Thread(new Task(n, c, d, 'C', 2)); Thread t4 = new Thread(new Task(n, d, a, 'D', 3)); t1.start(); t2.start(); t3.start(); t4.start(); try { t1.join(); t2.join(); t3.join(); t4.join(); System.out.println(sb.toString()); // System.out.println(sb.length()); } catch (InterruptedException e) { e.printStackTrace(); } } private static StringBuffer sb; static class Task implements Runnable { int n; Object self, target; char ch; int id; public Task(int n, Object self, Object target, char ch, int id) { this.n = n; this.self = self; this.target = target; this.ch = ch; this.id = id; } @Override public void run() { // System.out.println("Task" + id + " run..."); while (n-- > 0) { synchronized (self) { while (sb.length() % 4 != id) { waitSelf(); } sb.append(ch); notifyTarget(); waitSelf(); } } notifyTarget(); // System.out.println("Task" + id + " stop..."); } private void waitSelf() { try { self.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } private void notifyTarget() { synchronized (target) { target.notify(); } } }}","link":"/blog/2020/08/21/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%BC%96%E7%A8%8B/"},{"title":"哲学启蒙","text":"哲学启蒙人们的烦恼,基本是源于读书太少,而想得太多。 读哲学,不只是去吸收大家们的思想,而是在读的同时不要忘记思考。 哲学也许给不了人成功,也不一定会带来富有,但至少获得了自由。 大问题作者: [美国] 罗伯特·C. 所罗门 哲学的故事作者:威尔·杜兰特 用讲故事的方法,介绍了主要哲学家的生平及其观点,从苏格拉底、柏拉图、亚里士多德到叔本华、尼采再到柏格森,罗素、杜威等。在阐述每位哲学家思想的同时,生动地介绍了他们生活的时代背景、生活境遇和情感经历。 苏菲的世界作者:乔斯坦·贾德 14岁的少女苏菲不断接到一些极不寻常的来信,世界像谜团一般在她眼底展开,她在一位神秘导师的指引下,运用少女天生的悟性与后天知识,企图解开这些谜团,然而。事实真相远比她所想的更怪异、更离奇…… 西方哲学简史作者:伯特兰·罗素 记述了从西方哲学萌芽的古希腊哲学一直到二十世纪早期期西方哲学的发展历程。 《西方哲学简史》是在罗素的代表作《西方哲学史》的基础上,保留原著架构,并对一些繁复的逻辑论证进行通俗化的基础上编译而成,使其更加适合普通读者阅读。 禅与摩托车维修艺术作者:罗伯特·M.波西格 1968年,他与长子克里斯一起骑着摩托车从双子城出发,在中西部旷野、落基山区和西海岸从事心灵探险。他之所以开始这场横跨美国大陆的万里长旅,是希望从狭窄而受限的自我解脱。一路经过复杂经验与反省思考,他终于找到生理上的、精神上的完整与清净。 霍金高评: 时间简史霍金 你的第一本哲学书作者:托马斯·内格尔","link":"/blog/2021/05/30/%E5%93%B2%E5%AD%A6%E5%90%AF%E8%92%99/"},{"title":"大量文本的浏览进度和浏览时长统计","text":"大量文本的浏览进度和浏览时长统计埋点需求,Android App 需要在onResume 和 onPause 方法中计算浏览的时长,同时上报浏览的进度。 浏览进度Rate具体的计算方式的具体过程: 1、在滚动屏幕过程中,通过 textContent?.viewTreeObserver?.addOnScrollChangedListener 来记录屏幕滚动的位置 2、在滚动监听里通过textContent?.getLocationOnScreen(location)获取在屏幕的具体位置,同时, 计算出visibleHeight = screenHeight - location[1] 当前文本的可见高度 3、当可见高度超过目标的高度,则认为已经全部浏览,rate = 100% ,同时移除滚动监听textContent?.viewTreeObserver?.removeOnScrollChangedListener(mScrollChangeListener) 核心代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243private var totalHeight: Int = 0private var visibleHeight: Int = 0private var screenHeight = 0fun getViewRate(): String? =if (totalHeight <= 0 || visibleHeight < 0) nullelse "${(visibleHeight * 100 / totalHeight)}%"private fun bindViewTreeObserver() { cleanCache() contentTv.post { totalHeight = contentTv.height screenHeight = ScreenUtils.getScreenHeight(context) contentVisibleRate() } contentTv.viewTreeObserver.addOnScrollChangedListener(mScrollChangeListener)}private fun cleanCache() { totalHeight = 0 screenHeight = 0 visibleHeight = 0}private val mScrollChangeListener = ViewTreeObserver.OnScrollChangedListener { contentVisibleRate()}private fun contentVisibleRate() { val location = IntArray(2) contentTv.getLocationOnScreen(location) visibleHeight = (screenHeight - location[1]).coerceAtLeast(visibleHeight) if (visibleHeight >= totalHeight) { visibleHeight = totalHeight removeOnScrollChangedListener() }}private fun removeOnScrollChangedListener() { mScrollChangeListener?.let { contentTv.viewTreeObserver.removeOnScrollChangedListener(mScrollChangeListener) }}","link":"/blog/2021/09/09/%E5%A4%A7%E9%87%8F%E6%96%87%E6%9C%AC%E7%9A%84%E6%B5%8F%E8%A7%88%E8%BF%9B%E5%BA%A6%E5%92%8C%E6%B5%8F%E8%A7%88%E6%97%B6%E9%95%BF%E7%BB%9F%E8%AE%A1/"},{"title":"如何阅读一本书","text":"如何阅读一本书 读书是一件快乐的事 阅读的三要素 控制时间 抓重点 有深度 1. 检视阅读(泛读)1.1 有系统的略读 时间控制在5-15分钟 先看书名 研究目录页 通篇略读或者重点略读 顺序:书名、序、目录、内容、附录 通篇略读:快速从头到尾读一遍,不要停,不要进入细节 重点略读:选几个和主题相关的篇章来看 或者 随意翻翻 2. 分析阅读(精读)2.1 时间控制在3天以内(不包括数学,计算机类的工具书)2.2 抓重点 找关键字 找关键句 找关键段落 找关键章节 2.3 动手用工具做笔记、列大纲 边看边列 结构图或者思维导图 大纲可以先粗略,后细化 3. 总结评论 这本书讲的是什么? 用一句话(最简洁的话)概括一本书的内容 这本书的主旨和和论述是什么? 适当的参考别人的总结和评论 这本书的内容真实吗? 真正理解才能评论,最能学习的读者,也就是最能批评的读者 不同意的观点要理性表达 分享和讨论 这本书和我有什么关系? 4.常见问题读书慢的解决办法: 检视阅读 控制时间 运用基本的快速阅读方法 阅读时没有共鸣和尿点 这本书跟你的关系 结合你自己的经历和经验 放弃还没有共鸣的书 读书时追求完美,辛苦到死 抓重点 正确的阅读流程 控制时间 看完了一本书,书还是书,我还是我 总结和实践 做笔记","link":"/blog/2020/02/04/%E5%A6%82%E4%BD%95%E9%98%85%E8%AF%BB%E4%B8%80%E6%9C%AC%E4%B9%A6/"},{"title":"SystemBarUtil 工具类","text":"SystemBarUtil 工具类工具类,提供了系统栏高度和屏幕宽高获取方法 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103import android.app.Activityimport android.content.Contextimport android.graphics.Pointimport android.os.Buildimport android.view.Viewimport android.view.ViewGroupimport android.view.WindowManager/*** 工具类,提供了系统栏高度和屏幕宽高获取方法*/object SystemBarUtil { /** * 获取状态栏高度 */ @JvmStatic fun getStatusBarHeight(context: Context): Int { var height = 0 try { val resourceId = context.applicationContext.resources.getIdentifier( "status_bar_height", "dimen", "android" ) if (resourceId > 0) { height = context.applicationContext.resources.getDimensionPixelSize(resourceId) } } catch (e: Exception) { } return height } /** * 获取系统导航栏高度 */ @JvmStatic fun getNavigationBarHeight(context: Context): Int { var height = 0 try { val resourceId = context.applicationContext.resources.getIdentifier( "navigation_bar_height", "dimen", "android" ) if (resourceId > 0) { height = context.applicationContext.resources.getDimensionPixelSize(resourceId) } } catch (e: Exception) { } return height } private const val NAVIGATION = "navigationBarBackground" // 该方法需要在View完全被绘制出来之后调用 @JvmStatic private fun isNavigationBarVisible(activity: Activity): Boolean { val vp = activity.window.decorView as ViewGroup? if (vp != null) { for (i in 0 until vp.childCount) { vp.getChildAt(i).context.packageName if (vp.getChildAt(i).id !== View.NO_ID && NAVIGATION == activity.resources.getResourceEntryName(vp.getChildAt(i).id) ) { return true } } } return false } /** * 获取屏幕的物理大小 px */ @JvmStatic fun getDeviceScreenSize(context: Context): Point { val appContext = context.applicationContext val wm = appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager val point = Point(0, 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { wm.defaultDisplay.getRealSize(point) } else { wm.defaultDisplay.getSize(point) } return point } /** * 获取显示屏幕的宽高 px */ @JvmStatic fun getDisplaySize(context: Context): Point { val point = Point(0, 0) val dm = context.applicationContext.resources.displayMetrics point.x = dm.widthPixels point.y = dm.heightPixels return point }}","link":"/blog/2023/02/15/%E5%B7%A5%E5%85%B7%E7%B1%BB-SystemBarUtil/"},{"title":"StatusBarHelper","text":"StatusBarHelperStatusBar 工具类1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950object StatusBarHelper { /** * 内容显示在状态栏下面(LAYOUT_FULLSCREEN) > 6.0 * true:白底黑字,false:黑底白字 */ fun fitSystemBar(activity: Activity, light: Boolean = true) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return val window = activity.window val decorView = window.decorView var visibility = decorView.systemUiVisibility visibility = visibility or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE visibility = if (light) { visibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } else { visibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() } decorView.systemUiVisibility = visibility window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.statusBarColor = Color.TRANSPARENT } /** * 调整状态栏文字、图标颜色 > 6.0 * true:白底黑字,false:黑底白字 */ fun lightStatusBar(activity: Activity, light: Boolean = true) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return var window: Window = activity.window var visibility = window.decorView.systemUiVisibility visibility = if (light) { visibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } else { visibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() } window.decorView.systemUiVisibility = visibility } // 获取状态栏高度 fun getStatusBarHeight(activity: Activity): Int { var result: Int = 0 var resId = activity.resources.getIdentifier("status_bar_height", "dimen", "android") if (resId > 0) result = activity.resources.getDimensionPixelOffset(resId) return result }} 默认statusbar 高度 24dp(不同版本可能不一样)。如果设置内容显示在状态栏下面,需要在相应的布局设置android:fitsSystemWindows="true",系统会为该布局自动添加一个statusbar高度的topPadding。 SystemBar隐藏/显示模式123456var options: Int = View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // make content behind status bar | View.SYSTEM_UI_FLAG_LAYOUT_STABLE // keep layout stable | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide navigation bar | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // make content behind navigation bar | View.SYSTEM_UI_FLAG_IMMERSIVE // immersive mode","link":"/blog/2020/08/23/%E5%B7%A5%E5%85%B7%E7%B1%BB-StatusBarHelper/"},{"title":"SettingsHelper","text":"SettingsHelperSettingsHelper 自启动设置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208import android.content.ComponentNameimport android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Buildimport android.provider.Settingsimport android.util.Log/** * 跳转自启动页面 */object AutoStartHelper { private val hashMap = mutableMapOf<String, List<String>>().apply { put( "Xiaomi", listOf( "com.miui.securitycenter/com.miui.permcenter.autostart.AutoStartManagementActivity", "com.miui.securitycenter" ) ) put( "samsung", listOf( "com.samsung.android.sm_cn/com.samsung.android.sm.ui.ram.AutoRunActivity", "com.samsung.android.sm_cn/com.samsung.android.sm.ui.appmanagement.AppManagementActivity", "com.samsung.android.sm_cn/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity", "com.samsung.android.sm_cn/.ui.ram.RamActivity", "com.samsung.android.sm_cn/.app.dashboard.SmartManagerDashBoardActivity", "com.samsung.android.sm/com.samsung.android.sm.ui.ram.AutoRunActivity", "com.samsung.android.sm/com.samsung.android.sm.ui.appmanagement.AppManagementActivity", "com.samsung.android.sm/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity", "com.samsung.android.sm/.ui.ram.RamActivity", "com.samsung.android.sm/.app.dashboard.SmartManagerDashBoardActivity", "com.samsung.android.lool/com.samsung.android.sm.ui.battery.BatteryActivity", "com.samsung.android.sm_cn", "com.samsung.android.sm" ) ) put( "HUAWEI", listOf( "com.huawei.systemmanager/.startupmgr.ui.StartupNormalAppListActivity", "com.huawei.systemmanager/.appcontrol.activity.StartupAppControlActivity", "com.huawei.systemmanager/.optimize.process.ProtectActivity", "com.huawei.systemmanager/.optimize.bootstart.BootStartActivity", // "com.huawei.systemmanager/com.huawei.permissionmanager.ui.MainActivity", // 这个是隐私-权限管理,但是没有自启动权限!!! "com.android.settings/com.android.settings.Settings$" + "AppAndNotificationDashboardActivity", // 鸿蒙系统,应用和服务,列表中有应用启动管理 "com.huawei.systemmanager" ) ) put( "vivo", listOf( "com.iqoo.secure/.ui.phoneoptimize.BgStartUpManager", "com.vivo.permissionmanager/.activity.BgStartUpManagerActivity", "com.vivo.permissionmanager/.activity.SoftPermissionDetailActivity", "com.iqoo.secure/.safeguard.PurviewTabActivity", "com.iqoo.secure", "com.vivo.permissionmanager" ) ) put( "Meizu", listOf( "com.meizu.safe/.permission.SmartBGActivity", "com.meizu.safe/.permission.PermissionMainActivity", "com.meizu.safe" ) ) put( "OPPO", listOf( "com.coloros.safecenter/.startupapp.StartupAppListActivity", "com.coloros.safecenter/.permission.startup.StartupAppListActivity", "com.oppo.safe/.permission.startup.StartupAppListActivity", "com.coloros.oppoguardelf/com.coloros.powermanager.fuelgaue.PowerUsageModelActivity", "com.coloros.safecenter/com.coloros.privacypermissionsentry.PermissionTopActivity", "com.coloros.safecenter", "com.oppo.safe", "com.coloros.oppoguardelf" ) ) put( "oneplus", listOf( "com.oneplus.security/.chainlaunch.view.ChainLaunchAppListActivity", "com.oneplus.security" ) ) put( "letv", listOf( "com.letv.android.letvsafe/.AutobootManageActivity", "com.letv.android.letvsafe/.BackgroundAppManageActivity", "com.letv.android.letvsafe" ) ) put( "zte", listOf( "com.zte.heartyservice/.autorun.AppAutoRunManager", "com.zte.heartyservice" ) ) //金立 put( "F", listOf( "com.gionee.softmanager/.MainActivity", "com.gionee.softmanager" ) ) //以下为未确定(厂商名也不确定) put( "smartisanos", listOf( "com.smartisanos.security/.invokeHistory.InvokeHistoryActivity", "com.smartisanos.security" ) ) //360 put( "360", listOf( "com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity", "com.yulong.android.coolsafe" ) ) //360 put( "ulong", listOf( "com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity", "com.yulong.android.coolsafe" ) ) //酷派 put( "coolpad" /*厂商名称不确定是否正确*/, listOf( "com.yulong.android.security/com.yulong.android.seccenter.tabbarmain", "com.yulong.android.security" ) ) //联想 put( "lenovo" /*厂商名称不确定是否正确*/, listOf( "com.lenovo.security/.purebackground.PureBackgroundActivity", "com.lenovo.security" ) ) put( "htc" /*厂商名称不确定是否正确*/, listOf( "com.htc.pitroad/.landingpage.activity.LandingPageActivity", "com.htc.pitroad" ) ) //华硕 put( "asus" /*厂商名称不确定是否正确*/, listOf( "com.asus.mobilemanager/.MainActivity", "com.asus.mobilemanager" ) ) //酷派 put( "YuLong", listOf( "com.yulong.android.softmanager/.SpeedupActivity", "com.yulong.android.security/com.yulong.android.seccenter.tabbarmain", "com.yulong.android.security" ) ) } fun startAutoBootSetting(context: Context?) { Log.e("AutoStartHelper", "当前手机型号为:" + Build.MANUFACTURER) var result = false run start0@{ for ((manufacturer, componentNameList) in hashMap) { if (Build.MANUFACTURER.equals(manufacturer, ignoreCase = true)) { for (actName in componentNameList) { try { var intent: Intent? = null if (actName.contains("/")) { intent = Intent() intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.component = ComponentName.unflattenFromString(actName) if (actName.contains("SoftPermissionDetailActivity")) { intent.putExtra("packagename", context?.packageName) } } // else { // // 跳转到对应的安全管家/安全中心 // intent = context?.packageManager?.getLaunchIntentForPackage(actName) // } intent?.let { context?.startActivity(intent) result = true return@start0 } } catch (e: Exception) { e.printStackTrace() } } } } } if (!result) { try { // 跳转到app详情设置 val intent = Intent() intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS intent.data = Uri.fromParts("package", context?.packageName, null) context?.startActivity(intent) } catch (e: Exception) { e.printStackTrace() } } }","link":"/blog/2022/01/07/%E5%B7%A5%E5%85%B7%E7%B1%BB-%E8%87%AA%E5%90%AF%E5%8A%A8%E8%AE%BE%E7%BD%AE/"},{"title":"打开手机应用商店的评论调研","text":"打开手机应用商店的评论调研目前国内主流的应用商店ov,华为,小米和应用宝中,只有oppo和vivo支持APP直接拉起应用商店评分 0x01 Oppo应用商店评分Oppo应用评论调起的官方文档: https://open.oppomobile.com/new/developmentDoc/info?id=11038 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455private final static String PKG_MK_HEYTAP = "com.heytap.market";//Q之后的软件商店包名private final static String PKG_MK_OPPO = "com.oppo.market";//Q之前的软件商店包名private final static String COMMENT_DEEPLINK_PREFIX = "oaps://mk/developer/comment?pkg=";private final static int SUPPORT_MK_VERSION = 84000; // 支持评论功能的软件商店版本/** * 拉起评论页面。 */public static boolean jumpToComment(Activity context) { // 此处一定要传入调用方自己的包名,不能给其他应用拉起评论页。 String url = COMMENT_DEEPLINK_PREFIX + context.getPackageName(); // 优先判断heytap包 if (getVersionCode(context, PKG_MK_HEYTAP) >= SUPPORT_MK_VERSION) { return jumpApp(context, Uri.parse(url), PKG_MK_HEYTAP); } if (getVersionCode(context, PKG_MK_OPPO) >= SUPPORT_MK_VERSION) { return jumpApp(context, Uri.parse(url), PKG_MK_OPPO); } return false;}/** * 获取目标app版本号~ * * @param context * @param packageName * @return 返回版本号 */private static long getVersionCode(Activity context, String packageName) { long versionCode = -1; try { PackageInfo info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA); if (info != null) { versionCode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? info.getLongVersionCode() : info.versionCode; } } catch (PackageManager.NameNotFoundException e) { } return versionCode;}private static boolean jumpApp(Activity context, Uri uri, String targetPkgName) { try { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setPackage(targetPkgName); intent.setData(uri); // 建议采用startActivityForResult 方法启动商店页面,requestCode由调用方自定义且必须大于0,软件商店不关注 context.startActivityForResult(intent, 100); return true; } catch (Exception e) { e.printStackTrace(); } return false;} 0x02 Vivo应用商店评分Vivo应用评论调起的官方文档: https://dev.vivo.com.cn/documentCenter/doc/257 123456String url = market://details?id=${pkg}&th_name=need_commentUri uri = Uri.parse(url);Intent intent= new Intent(Intent.ACTION_VIEW,uri);intent.setPackage("com.bbk.appstore");startActivity(intent); 0x03 其他应用商店直接跳转到应用商店的APP详情页,具体代码如下: 1234567String url = market://details?id=${pkg}Uri uri = Uri.parse(url);Intent intent= new Intent(Intent.ACTION_VIEW,uri);if (market_pkg != null) intent.setPackage(market_pkg);startActivity(intent);","link":"/blog/2023/09/15/%E6%89%93%E5%BC%80%E6%89%8B%E6%9C%BA%E5%BA%94%E7%94%A8%E5%95%86%E5%BA%97%E7%9A%84%E8%AF%84%E8%AE%BA%E8%B0%83%E7%A0%94/"},{"title":"时间管理","text":"时间管理1.远离懒惰2.提高效率 事不宜迟,速度制胜 有时候不是我们做的不够好,而是对手做的比我们更快一步。 统筹安排,平行作业 洗衣,烧水,煮早饭和刷牙可以并行执行 优化流程,简化操作 整理整顿,快速定位 选择效率更高的工具 第一次就把事情做好 3.善用零碎的时间4.充分利用业余时间 学习阅读 独处思考 补充精力 适度的社交和娱乐 注重家庭生活 培养个人兴趣","link":"/blog/2020/02/04/%E6%97%B6%E9%97%B4%E7%AE%A1%E7%90%86/"},{"title":"打开手机自带的应用商店","text":"需求背景:因为各种原因,需要打开手机自带的应用商店 核心代码 123456789101112131415161718192021222324252627282930313233343536373839404142import android.content.Contextimport android.content.Intentimport android.net.Uriobject MarketUtils { const val XIAOMI_MARKET = "com.xiaomi.market" const val HUAWEI_MARKET = "com.huawei.appmarket" const val OPPO_MARKET = "com.oppo.market" const val OPPO_MARKET2 = "com.heytap.market" const val VIVO_MARKET = "com.bbk.appstore" const val MEIZU_MARKET = "com.meizu.mstore" const val YYB_MARKET = "com.tencent.android.qqdownloader" fun startTargetMarket(context: Context, deepLink: String, packageName: String) { //"deeplink": "market://details?id=com.taobao.taobao&..." try { val uri = Uri.parse(deepLink) val intent = Intent(Intent.ACTION_VIEW, uri) intent.setPackage(packageName) context.startActivity(intent) } catch (e: Exception) { e.printStackTrace() val uri = Uri.parse(deepLink) val intent = Intent(Intent.ACTION_VIEW, uri) context.startActivity(intent) } } /** * 跳转到腾讯应用宝 */ private fun startTencentMarket(context: Context, deepLink: String) { val uri: Uri = Uri.parse(deepLink) // "market://details?id=com.taobao.taobao&..." val intent = Intent(Intent.ACTION_VIEW, uri) intent.setClassName( "com.tencent.android.qqdownloader", "com.tencent.pangu.link.LinkProxyActivity" ) context.startActivity(intent) }} 测试代码: 123456789101112131415161718192021222324252627private fun initView() { // val deepLink = "market://details?id=com.taobao.taobao&ref=&caller=com.taobao.taobao&token=OWNjY2YyNGNiYzYwYTNkMCMxNjQ5NjQ2NTA0OTA5IzEjY29tLnRhb2Jhby50YW9iYW8jS29iZS8yNEAwZjY1NGI3ZGZlZWJlZWI2NTk3MTkyNWE4ZDFhZTc1Zg==&style=1&m=adapi_2049933&tk_con=eyJ0cmFja0lkIjoiN2VkOGI0ZWM2YjAwN2FmNjkzZDc0ODJkYTBlNzgyMDMiLCJkZXZpY2VJZCI6Ijg2Mzg5NDAzMjE1ODg3NyIsImFwcElkIjoiMjMzNCIsInZlcnNpb25JZCI6IjAiLCJlbnRlcklkIjoiMTQiLCJwYWdlSWQiOiIxMDAwMDEiLCJhYiI6IjFfMF85IiwiYWRJZCI6IjQwNzA2ODY5OCIsInQiOiIxNjQ5NjQ2NTA0OTA3IiwidiI6InYxIn0%3D&tk_ref=%7B%22adId%22%3A%22407068698%22%2C%22trackId%22%3A%227ed8b4ec6b007af693d7482da0e78203%22%7D" xiaomi_market.setOnClickListener { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.XIAOMI_MARKET) } huawei_market.setOnClickListener { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.HUAWEI_MARKET) } oppo_market.setOnClickListener { if (android.os.Build.VERSION.SDK_INT >= 28) { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.OPPO_MARKET2) } else { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.OPPO_MARKET) } } vivo_market.setOnClickListener { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.VIVO_MARKET) } meizu_market.setOnClickListener { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.MEIZU_MARKET) } yyb_market.setOnClickListener { MarketUtils.startTargetMarket(this, deepLink, MarketUtils.YYB_MARKET) }}","link":"/blog/2022/04/13/%E6%89%93%E5%BC%80%E6%89%8B%E6%9C%BA%E8%87%AA%E5%B8%A6%E7%9A%84%E5%BA%94%E7%94%A8%E5%95%86%E5%BA%97/"},{"title":"Aria下载器源码分析","text":"Aria下载器源码分析Aria 中文文档: https://aria.laoyuyu.me/aria_doc/ 版本:3.8.15 0x01 注册流程在Activity的onCreate、fragment的onCreate、java的构造函数中使用Aria.download(this).register()便可以实现注册。 0x0101 Aria类,下载库的统一入口Aria类仅一个私有的构造方法,无法实例化 1private Aria() {} 0x0102 Aria类的download方法Aria类中的2个主要静态方法 download, upload, 对应下载和上传两种类型 以下载方法为例,下载,在当前类中调用Aria方法,参数需要使用this,返回对象是 DownloadReceiver 123456public static DownloadReceiver download(Object obj) { if (AriaManager.getInstance() != null) { return AriaManager.getInstance().download(obj); } return get(convertContext(obj)).download(obj);} AriaManager是个单例 首次会走get方法,最终执行AriaManager#init()方法进行初始化 convertContext()方法会判断当前参数obj是否是Context对象,并返回Context 最后都会执行AriaManager单例对象的download()方法,返回一个DownloadReceiver对象 0x0103 AriaManager初始化123456789101112131415161718192021222324252627282930@SuppressLint("StaticFieldLeak") private static volatile AriaManager INSTANCE = null;private AriaManager(Context context) { APP = context.getApplicationContext();}public static AriaManager getInstance() { return INSTANCE;}static AriaManager init(Context context) { if (INSTANCE == null) { synchronized (LOCK) { if (INSTANCE == null) { INSTANCE = new AriaManager(context); INSTANCE.initData(); } } } return INSTANCE;}private void initData() { mConfig = AriaConfig.init(APP); initDb(APP); regAppLifeCallback(APP); initAria();} init()方法,双空判断加锁实现AriaManager单例 初始化调用 initData() 方法 初始化 AriaConfig 初始化DB 注册APP生命周期回调,Activity销毁自动移除当前对象的receiver Aria初始化,异常处理,日志,命令处理器 CommandManager 初始化 0x0104 AriaManager类的download123456789101112131415161718192021222324private Map<String, AbsReceiver> mReceivers = new ConcurrentHashMap<>();/*** 处理下载操作*/DownloadReceiver download(Object obj) { IReceiver receiver = mReceivers.get(getKey(ReceiverType.DOWNLOAD, obj)); if (receiver == null) { receiver = putReceiver(ReceiverType.DOWNLOAD, obj); } return (receiver instanceof DownloadReceiver) ? (DownloadReceiver) receiver : null;}private IReceiver putReceiver(ReceiverType type, Object obj) { final String key = getKey(type, obj); IReceiver receiver = mReceivers.get(key); if (receiver == null) { AbsReceiver absReceiver = type.equals(ReceiverType.DOWNLOAD) ? new DownloadReceiver(obj) : new UploadReceiver(obj); mReceivers.put(key, absReceiver); receiver = absReceiver; } return receiver;} 调用download方法,根据obj和ReceiverType.DOWNLOAD类型生成key,查询mReceivers是否已经存在当前对象的下载功能接收器DownloadReceiver,存在直接返回 首次调用,会走到putReceiver方法,新生成一个下载功能接收器DownloadReceiver,并存储在mReceivers中 0x0105 将当前对象注册到Aria 调用DownloadReceiver#register()方法 通过DOWNLOAD注解或者实现DownloadTaskListener接口,调用TaskSchedulers.getInstance().register()将当前类注册到Aria 0x0106 TaskSchedulers注册(TODO)TaskSchedulers 事件调度器,用于处理任务状态的调度 12345678910111213141516171819202122232425262728293031private Map<String, Map<TaskEnum, Object>> mObservers = new ConcurrentHashMap<>();/** * 将当前类注册到Aria * * @param obj 观察者类 * @param taskEnum 任务类型 {@link TaskEnum} */public void register(Object obj, TaskEnum taskEnum) { String targetName = obj.getClass().getName(); Map<TaskEnum, Object> listeners = mObservers.get(getKey(obj)); if (listeners == null) { listeners = new ConcurrentHashMap<>(); mObservers.put(getKey(obj), listeners); } if (!hasProxyListener(listeners, taskEnum)) { if (obj instanceof TaskInternalListenerInterface) { listeners.put(taskEnum, obj); return; } String proxyClassName = targetName + taskEnum.proxySuffix; ISchedulerListener listener = createListener(proxyClassName); if (listener != null) { listener.setListener(obj); listeners.put(taskEnum, listener); } else { ALog.e(TAG, "注册错误,没有【" + proxyClassName + "】观察者"); } }} 0x02 下载流程(TODO)1234long taskId = Aria.download(this) .load(DOWNLOAD_URL) //读取下载地址 .setFilePath(DOWNLOAD_PATH) //设置文件保存的完整路径 .create(); //启动下载 01 DownloadReceiver.load()1 HttpBuilderTarget.create()2 BuilderController.create()3 CmdHelper.createNormalCmd()4 NormalCmdFactory.createCmd() -> StartCmd5 EventMsgUtil.getDefault().post(StartCmd) -> mEventQueue.take()6 EventMsgUtil.sendEvent()7 StartCmd.executeCmd() -> AbsNormalCmd.startTask()8 DTaskQueue.createTask(DTaskWrapper wrapper)9 TaskWrapperManager.getInstance().putTaskWrapper(wrapper)10 AbsTaskQueue.startTask()11 DLoadExecutePool.putTask()12 AbsTask.start()13 HttpDLoaderUtil.start() -> AbsNormalLoaderUtil.start()14 NormalLoader.run() -> AbsNormalLoader.run()15 AbsNormalLoader.startFlow() -> NormalLoader.handleTask()// 启动单线程任务16 NormalLoader.startThreadTask()17 NormalTTBuilder.buildThreadTask()18 ThreadTaskManager.getInstance().startThread()19 AbsNormalLoader.startTimer() // 启动进度获取定时器 21 ThreadTask.call() // 线程池执行任务回调22 AbsThreadTaskAdapter.call()23 HttpDThreadTaskAdapter.handlerThreadTask() // 正式建立Http连接,下载任务24 HttpDThreadTaskAdapter.readNormal() 25 HttpDThreadTaskAdapter.handleComplete()26 ThreadTask.updateCompleteState()27 NormalThreadStateManager.callback -> STATE_COMPLETE28 BaseListener.onComplete() // 对应的实体类是BaseDListener29 BaseListener.sendInState2Target() // 将任务状态发送给下载器 0x03 完成事件逆行分析 IDLoadListener.onComplete() NormalLoader.addComponent(IRecordHandler recordHandler) -> ILoaderVisitor.addComponent(IRecordHandler recordHandler) // 处理任务记录 RecordHandler.checkTaskCompleted() // 检查任务是否已完成 遍历TaskRecord中所有的ThreadRecord.isComplete就认为下载完成 0x04 M3U8文件下载过程M3U8ThreadTaskAdapter.readDynamicFile(InputStream is) // 动态长度文件读取方式M3U8ThreadTaskAdapter.handleComplete() // 处理完成ThreadTask.updateCompleteState() // 组装Message消息,发送给VodStateManager.callback -> Handler.CallbackVodStateManager.callback -> STATE_COMPLETEBaseListener.onComplete() // 对应的实体类是M3U8Listener","link":"/blog/2023/05/25/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-Aria%E4%B8%8B%E8%BD%BD%E5%99%A8/"},{"title":"用Android自带浏览器打开网页","text":"启动android默认浏览器12345val intent = Intent()intent.data = Uri.parse(url)intent.action = Intent.ACTION_VIEWintent.flags = Intent.FLAG_ACTIVITY_NEW_TASKcontext.startActivity(intent) 启动指定浏览器打开(不推荐)警告:爱加密加固之后的包,会把这个异常给吃掉,导致无法跳转,也无反应。 这种方式需要处理手机中不存在指定浏览器的情况 1234567891011121314151617181920try { val intent = Intent() intent.data = Uri.parse(targetUrl) intent.action = Intent.ACTION_VIEW intent.setClassName( "com.android.browser", "com.android.browser.BrowserActivity" ) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent)} catch (e: Exception) { e.printStackTrace() // android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.browser/com.android.browser.BrowserActivity}; have you declared this activity in your AndroidManifest.xml? val intent = Intent() intent.data = Uri.parse(url) intent.action = Intent.ACTION_VIEW intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent)} 由于华为鸿蒙系统已经没有Android默认的浏览器,所以此处必须要有异常处理,或者提前处理手机中不存在指定浏览器的情况。 市场上常用浏览器的包名和类名@20220630 123456华为: "com.huawei.browser/com.huawei.browser.BrowserMainActivity"Vivo: "com.vivo.browser/com.vivo.browser.MainActivity"小米: "name=com.android.browser/com.android.browser.BrowserActivity"uc浏览器: "com.uc.browser", "com.uc.browser.ActivityUpdate"opera: "com.opera.mini.android", "com.opera.mini.android.Browser"qq浏览器: "com.tencent.mtt", "com.tencent.mtt.MainActivity"","link":"/blog/2022/06/29/%E7%94%A8Android%E8%87%AA%E5%B8%A6%E6%B5%8F%E8%A7%88%E5%99%A8%E6%89%93%E5%BC%80%E7%BD%91%E9%A1%B5/"},{"title":"Android 13 监控网络连接状态","text":"Android 13 监控网络连接状态获取瞬时状态123456val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerval currentNetwork = cm.activeNetworkif (currentNetwork != null) { val caps = cm.getNetworkCapabilities(currentNetwork) val linkProperties = cm.getLinkProperties(currentNetwork)} 监听网络事件将 NetworkCallback 类与 ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback) 及 ConnectivityManager.registerNetworkCallback(NetworkCallback) 结合使用。 1234567891011121314151617181920212223val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagercm.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { Log.e(TAG, "The default network is now: " + network) } override fun onLost(network: Network) { Log.e(TAG, "The application no longer has a default network. The last default network was " + network) handle(null) } override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { Log.d(TAG, "The default network changed capabilities: " + networkCapabilities) handle(networkCapabilities) } override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { Log.i(TAG, "The default network changed link properties: " + linkProperties) }}) 解析NetworkCapabilities的网络状态信息123456789101112131415161718192021 private fun handle(caps: NetworkCapabilities?) { if (caps != null) { if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { if ( caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) ) { setResult(STATE_WIFI) return } else if ( caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ) { setResult(STATE_MOBILE) return } } } setResult(STATE_UNKNOWN) return}","link":"/blog/2023/01/31/%E7%9B%91%E6%8E%A7%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81/"},{"title":"ARouter源码解析","text":"ARouter源码解析0x01 init ()ARouter 的入口,初始化SDK ARouter.init(mApplication);: 1234567891011public static void init(Application application) { if (!hasInit) { // 避免重复初始化 logger = _ARouter.logger; // 日志初始化 _ARouter.logger.info(Consts.TAG, "ARouter init start."); hasInit = _ARouter.init(application); // 正式初始化 if (hasInit) { // 初始化之后 _ARouter.afterInit(); // 管理拦截器的服务 InterceptorService 初始化 } _ARouter.logger.info(Consts.TAG, "ARouter init over."); }} _ARouter.init(): 12345678protected static synchronized boolean init(Application application) { mContext = application; // 初始化application LogisticsCenter.init(mContext, executor); // 指定默认的线程池初始化 logger.info(Consts.TAG, "ARouter init success!"); hasInit = true;// 初始化成功 mHandler = new Handler(Looper.getMainLooper());// 初始化mHandler return true;} LogisticsCenter.init() 1 使用 arouter-auto-register 插件自动加载 routerMap 2 手动加载的情况,release 仅第一次会去解析 routerMap,其他时候都读取 SharedPreferences 3 根据routerMap三种类型,分别缓存在Warehouse 的Map中 route and metas provider interceptor 12345678910111213141516171819202122232425262728293031323334353637// LogisticsCenter init, load all metas in memory.public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { mContext = context; executor = tpe; // 线程池 //... loadRouterMap(); // 插件arouter-auto-register会生成这个方法的代码实现 if (registerByPlugin) { logger.info(TAG, "Load router map by arouter-auto-register plugin."); } else {// 不用插件手动加载routerMap Set<String> routerMap; // routerMap // Debug 模式每次更新 routerMap,并更新SharedPreferences if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) { routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); if (!routerMap.isEmpty()) { context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply(); } PackageUtils.updateVersion(context); } else { // 直接从 SharedPreferences 读取 routerMap routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>())); } //... for (String className : routerMap) { if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { // Load root ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) { // Load interceptorMeta ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) { // Load providerIndex ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex); } } } //...} class Warehouse 缓存三种类型的数据 1234567891011121314class Warehouse { // Cache route and metas static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); static Map<String, RouteMeta> routes = new HashMap<>(); // Cache provider static Map<Class, IProvider> providers = new HashMap<>(); static Map<String, RouteMeta> providersIndex = new HashMap<>(); // Cache interceptor static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]"); static List<IInterceptor> interceptors = new ArrayList<>();} 总结来说,init过程就是把所有注解的信息加载内存中,并且完成所有拦截器的初始化。 0x02 navigation()发起路由操作 ARouter.getInstance().build("/test/activity").navigation(); 123public Postcard build(String path) { return _ARouter.getInstance().build(path);} 都是单例设计,最后调用 _ARouter 单例的 build() 12345678910111213141516protected Postcard build(String path) { //... PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } return build(path, extractGroup(path));}protected Postcard build(String path, String group) { //... PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } return new Postcard(path, group);} PathReplaceService,它是继承IProvider的接口,它是预留给用户实现路径动态变化功能。 Postcard 是一个继承自 RouteMeta 的数据 Bean。 1234567891011121314151617public final class Postcard extends RouteMeta { // Base private Uri uri; private Object tag; // A tag prepare for some thing wrong. private Bundle mBundle; // Data to transform private int flags = -1; // Flags of route private int timeout = 300; // Navigation timeout, TimeUnit.Second private IProvider provider; // It will be set value, if this postcard was provider. private boolean greenChannel; private SerializationService serializationService; // Animation private Bundle optionsCompat; // The transition animation of activity private int enterAnim = -1; private int exitAnim = -1; //...} Postcard 对象的 navigation 方法,最终还是调用的 _Arouter 的 navigation方法。 1 PretreatmentService 进行跳转的拦截和检测 2 LogisticsCenter.completion(postcard) Postcard 加载和自解析 3 Postcard 解析异常 回调 callback.onLost,若 callback 为空则 DegradeService 降级回调处理 4 Postcard 处理结束,回调 callback.onFound(postcard) 5 GreenChannel 的 navigation,直接执行 _navigation 路由 6 非 GreenChannel 的 navigation,异步执行注入拦截器。拦截之后回调 callback.onInterrupt, 通过所有的拦截器,则继续执行 _navigation 路由 123456789101112131415161718192021222324252627282930313233343536373839404142protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class); // ....pretreatmentService拦截 try { LogisticsCenter.completion(postcard);// postcard 自解析 } catch (NoRouteFoundException ex) { // .... if (null != callback) { callback.onLost(postcard); } else { // No callback for this invoke, then we use the global degrade service. DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class); if (null != degradeService) { degradeService.onLost(context, postcard); } } return null; } if (null != callback) { callback.onFound(postcard); // Postcard 处理结束,回调 callback.onFound(postcard) } if (!postcard.isGreenChannel()) { interceptorService.doInterceptions(postcard, new InterceptorCallback() { @Override public void onContinue(Postcard postcard) { _navigation(context, postcard, requestCode, callback); } @Override public void onInterrupt(Throwable exception) { if (null != callback) { callback.onInterrupt(postcard); } } }); } else { return _navigation(context, postcard, requestCode, callback); } return null;} _Arouter 的 _navigation 方法 1 根据 postcard.getType() 处理 2 ACTIVITY 类型, 根据 postcard.getDestination() 生成一个 intent,然后 putExtras, 并设置对应的 flag 和 action 执行跳转 3 PROVIDER 类型,直接返回在 LogisticsCenter.completion(postcard) 生成的 IProvider 实例 4 BOARDCAST,CONTENT_PROVIDER,FRAGMENT 类型分别返回 newInstance() 生成实例,Fragment 会 setArguments 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { // ... switch (postcard.getType()) { case ACTIVITY: // Build intent final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); // Set flags. int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // Set Actions String action = postcard.getAction(); if (!TextUtils.isEmpty(action)) { intent.setAction(action); } // Navigation in main looper. runInMainThread(new Runnable() { @Override public void run() { startActivity(requestCode, currentContext, intent, postcard, callback); } }); break; case PROVIDER: return postcard.getProvider(); case BOARDCAST: case CONTENT_PROVIDER: case FRAGMENT: Class fragmentMeta = postcard.getDestination(); try { Object instance = fragmentMeta.getConstructor().newInstance(); if (instance instanceof Fragment) { ((Fragment) instance).setArguments(postcard.getExtras()); } else if (instance instanceof android.support.v4.app.Fragment) { ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras()); } return instance; } catch (Exception ex) { // ... } case METHOD: case SERVICE: default: return null; } return null;}","link":"/blog/2020/08/27/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-Arouter/"},{"title":"简单Socket服务器实现","text":"简单Socket服务器实现服务端 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273/** * 服务端: * 1)接收客户端的连接 * 2)接收客户端的数据 * 3)把数据反转,并返回给客户端 * 4)重复2,3步骤,直到客户端发送 'bye',断开连接。 * <p> * 5)新增支持多个客户端 */public class TestServer { public static void main(String[] args) throws IOException { // 1、开启服务器 ServerSocket server = new ServerSocket(8989); ExecutorService executorService = Executors.newCachedThreadPool(); while (true) { // 2、接收一个客户端的连接 Socket socket = server.accept(); Task task = new Task(socket); executorService.execute(task); } }}class Task implements Runnable { Socket socket; public Task(Socket socket) { this.socket = socket; } @Override public void run() { try { // 3、先获取输入流和输出流 InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); // 4、转为字符流 InputStreamReader isr = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(isr); // 处理字符串 String str; PrintStream ps = new PrintStream(outputStream); while ((str = bufferedReader.readLine()) != null) { System.out.println(Thread.currentThread().getName() + ": tick,tick,tick..."); if ("bye".equals(str)) { ps.println("see you"); break; } // 反转 StringBuilder sb = new StringBuilder(str); sb.reverse(); // 返回给客户端 ps.println(sb.toString()); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 断开 socket.close(); } catch (IOException e) { e.printStackTrace(); } } }} 客户端 12345678910111213141516171819202122232425262728293031323334353637383940414243/** * 客户端 * 1)用户输入数据 * 2)发送给服务器 * 3)接收服务器返回的结果 * 4)重复以上步骤,直到输入bye 结束 **/public class TestClient { public static void main(String[] args) throws IOException { // 1. 连接服务器 Socket socket = new Socket("192.168.124.17", 8989); /** * 客户端 * 1)用户输入数据 * 2)发送给服务器 * 3)接收服务器返回的结果 * 4)重复以上步骤,直到输入bye 结束 */ Scanner scanner = new Scanner(System.in); PrintStream ps = new PrintStream(socket.getOutputStream()); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); while (true) { //获取用户输入 System.out.println("Please print something:"); String word = scanner.nextLine(); // 发送给服务端 ps.println(word); // 接收服务器返回的数据 String result = br.readLine(); System.out.println("Lucy: " + result); if ("see you".equals(result)) { break; } } // 关闭 scanner.close(); socket.close(); }}","link":"/blog/2020/08/06/%E7%AE%80%E5%8D%95Socket%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0/"},{"title":"KMP算法(Java版)","text":"KMP算法(Java版)谈到字符串问题,不得不提的就是 KMP 算法,它是用来解决字符串查找的问题,可以在一个字符串(S)中查找一个子串(W)出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串,导致效率低下,而KMP算法可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。 问题: 有一个文本串S,和一个模式串P,现在要查找P在S中的位置 0x01 暴力算法12345678910111213141516171819// 暴力查找法:不一致的时候直接把i 移动到 i+1 的位置继续比较private static int search(String s, String p) { int sLen = s.length(); int pLen = p.length(); int i = 0, j = 0; while (i < sLen && j < pLen) { if (s.charAt(i) == p.charAt(j)) { // 字符相同,则继续匹配下一个字符 i++; j++; } else { // i,j 复位 i = i - j + 1; j = 0; } } if (j == pLen) return i - j; return -1;} 0x02 KMP算法123456789101112131415161718192021222324252627282930313233343536// KMP 查找法private static int kmpSearch(String s, String p) { int sLen = s.length(); int pLen = p.length(); int[] next = getNext(p); int i = 0, j = 0; while (i < sLen && j < pLen) { if (j == -1 || (s.charAt(i) == p.charAt(j))) { i++; j++; } else { j = next[j]; } } if (j == pLen) return i - j; return -1;}// 计算next数组private static int[] getNext(String p) { int len = p.length(); int[] next = new int[len]; next[0] = -1; int j = -1, i = 0; while (i < len - 1) { if (j == -1 || p.charAt(j) == p.charAt(i)) { i++; j++; next[i] = j; } else { j = next[j]; } } // System.out.println(Arrays.toString(next)); return next;}","link":"/blog/2020/08/13/%E7%AE%97%E6%B3%95-KMP%E7%AE%97%E6%B3%95Java%E7%89%88/"},{"title":"十大排序算法实现(Java版)","text":"十大排序算法实现(Java版)一. 常用排序算法12345// Java 的进制数表示int a10 = 99;int a2 = 0b101;int a8 = 0143;int a16 = 0x63; 0x01 冒泡排序原理比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 代码实现如下: 1234567891011121314151617// 0x01冒泡排序public static int[] bubbleSort(int[] nums) { if (nums != null && nums.length > 1) { int len = nums.length; int tmp; for (int i = 1; i <= len; i++) { for (int j = 0; j < len - i; j++) { if (nums[j] > nums[j + 1]) { // swap tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; } } } } return nums;} 0x02 选择排序原理第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。 代码实现如下: 1234567891011121314151617// 0x02选择排序public static int[] selectSort(int[] nums) { int len = nums.length; int min, tmp; for (int i = 0; i < len - 1; i++) { min = i; for (int j = i + 1; j < len; j++) { if (nums[j] < nums[min]) min = j; } if (min != i) { // swap tmp = nums[i]; nums[i] = nums[min]; nums[min] = tmp; } } return nums;} 0x03 插入排序原理插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。 时间复杂度在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较N- 1次,时间复杂度为 O(N) 。最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为O(N^2) 。 空间复杂度插入排序的空间复杂度为常数阶O(1)。 代码实现如下: 12345678910111213141516// 0x03插入排序public static int[] insertSort(int[] nums) { if (nums != null && nums.length > 1) { int len = nums.length; for (int i = 1; i < len; i++) { int candidate = nums[i]; int j = i - 1; while (j >= 0 && candidate < nums[j]) { nums[j + 1] = nums[j]; j--; } nums[j + 1] = candidate; } } return nums;} 0x04 快速排序原理通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。 代码实现如下: 123456789101112131415161718192021222324252627282930313233// 0x04快速排序public static int[] quickSort(int[] nums) { quickSort(nums, 0, nums.length - 1); return nums;}// 0x04快速排序private static void quickSort(int[] nums, int start, int end) { int i = start; int j = end; int key = nums[start]; while (i < j) { while (i < j && nums[j] > key) { j--;// 右边的数据大,则直接下标左移 } while (i < j && nums[i] < key) { i++;// 左边的数据更小,则直接下标右移 } if (i < j) { if (nums[i] == nums[j]) { i++;// 两边的数据一样大,直接移动一个下标 } else { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } } } // 递归排序左右两边,此时 i == j && nums[i] == key if (i - 1 > start) quickSort(nums, start, i - 1); if (j + 1 < end) quickSort(nums, j + 1, end);} 0x05 归并排序归并排序(Merge Sort)的工作原理如下:第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置重复步骤3直到某一指针超出序列尾将另一序列剩下的所有元素直接复制到合并序列尾 代码实现如下: 1234567891011121314151617181920212223242526272829303132// 0x05归并排序public static int[] mergeSort(int[] nums) { return mergeSort(nums, 0, nums.length - 1);}// 0x05归并排序private static int[] mergeSort(int[] nums, int l, int r) { if (l == r) return new int[]{nums[l]};// 单个元素的有序数组 int mid = (l + r) / 2; int[] lefts = mergeSort(nums, l, mid); // 左边的有序数组 int[] rights = mergeSort(nums, mid + 1, r);// 右边的有序数组 return merge(lefts, rights); // 合并左右两个有序数组,并返回}// 0x05合并两个有序队列private static int[] merge(int[] nums1, int[] nums2) { int len1 = nums1.length, len2 = nums2.length; int index1 = 0, index2 = 0, index = 0; int[] result = new int[len1 + len2]; for (; index1 < len1 && index2 < len2; ) { if (nums1[index1] <= nums2[index2]) { result[index++] = nums1[index1++]; } else { result[index++] = nums2[index2++]; } } if (index1 == len1) { System.arraycopy(nums2, index2, result, index, len2 - index2); } if (index2 == len2) { System.arraycopy(nums1, index1, result, index, len1 - index1); } return result;} 0x06 希尔排序希尔排序的实质就是分组插入排序,该方法又称缩小增量排序。原理先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。 代码实现如下: 12345678910111213141516171819// 0x06希尔排序public static int[] shellSort(int[] nums) { if (nums != null && nums.length > 1) { int len = nums.length; for (int gap = len / 2; gap > 0; gap /= 2) { // gap每次减半 for (int i = 0; i < gap; i++) { //遍历组 for (int m = i; m < len; m += gap) {// 插入排序i int candidate = nums[m]; int n = m - gap; for (; n >= 0 && nums[n] > candidate; n -= gap) { nums[n + gap] = nums[n]; } nums[n + gap] = candidate; } } } } return nums;}","link":"/blog/2020/08/12/%E7%AE%97%E6%B3%95-%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95Java%E7%89%88/"},{"title":"链表(Java版)","text":"链表(Java版)在链表操作中,我们通常通过生成一个 dummy 哑节点,来避免空链表的判断。 19.删除链表的倒数第 N 个结点方法一:计算链表长度 为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。 方法三:双指针 我们可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。 时间复杂度:O(L),其中 L 是链表的长度。空间复杂度:O(1)。 21.合并两个有序链表 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里。 当 l1 和 l2 有一个是空的,只需要简单地将非空链表接在合并链表的后面。 23.合并K个升序链表方法一:顺序合并 已经有合并两个有序链表的前提,直接遍历数组,依次合并两个链表得到结果。 方法二:分治合并 将 k 个链表分组并将同一组中的链表合并(递归实现) 进时间复杂度为 O(kn×logk)空间复杂度,递归会使用到 O(logk) 空间代价的栈空间","link":"/blog/2022/10/12/%E7%AE%97%E6%B3%95-%E9%93%BE%E8%A1%A8Java%E7%89%88/"},{"title":"链表常见问题","text":"链表常见问题12345678910/** * 单链表数据结构 */class Node { int val; Node next; public Node(int val) { this.val = val; }} 0x01 链表翻转题目描述: 这道算法题,说直白点就是:如何让后一个节点指向前一个节点。 1234567891011// 1.翻转链表public static Node reverse(Node head) { Node res = null; while (head != null) { Node h = head.next; head.next = res; res = head; head = h; } return res;} 0x02 两数相加题目描述: Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。 你可以假设除了数字 0 之外,这两个数字都不会以零开头。 示例: 123输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)输出:7 -> 0 -> 8原因:342 + 465 = 807 123456789101112131415161718// 2.链表相加public static Node plus(Node l1, Node l2) { Node dummyHead = new Node(0); Node l3 = dummyHead; int carry = 0; while (l1 != null || l2 != null || carry > 0) { int val = carry; val = (l1 != null ? val + l1.val : val); val = (l2 != null ? val + l2.val : val); Node t = new Node(val > 9 ? val - 10 : val); l3.next = t; carry = val > 9 ? 1 : 0; l1 = l1 != null ? l1.next : null; l2 = l2 != null ? l2.next : null; l3 = t; } return dummyHead.next;} 0x03 链表中倒数第k个节点题目描述: 输入一个链表,输出该链表中倒数第k个结点。 问题分析: 链表中倒数第k个节点也就是正数第(L-K+1)个节点。 首先两个节点/指针,一个节点 node1 先开始跑,指针 node1 跑到 k-1 个节点后,另一个节点 node2 开始跑,当 node1 跑到最后时,node2 所指的节点就是倒数第k个节点也就是正数第(L-K+1)个节点。 123456789101112131415161718// 3.链表中倒数第k个节点public static Node findKFromEnd(Node head, int k) { Node node1 = head, node2 = head; int len = 0, index = k; // 链表长度 while (node1 != null) { len++; node1 = node1.next; // 从 len = k -1 的位置开始,执行 K 次, if (index > 0) { index--; } else { node2 = node2.next; } } if (len == 0 || len < k) return null; return node2;} 0x04 删除链表的倒数第N个节点题目描述: 给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 问题分析: 两次遍历法 首先我们将添加一个 哑结点(dummyHead)作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度 L。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L - n) 个结点那里。我们把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点,完成这个算法。 一次遍历法: 链表中倒数第N个节点也就是正数第(L-N+1)个节点。 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点)。 1234567891011121314151617181920// 4.删除链表的倒数第N个节点public static Node deleteKFromEnd(Node head, int k) { Node dummyHead = new Node(0); dummyHead.next = head; int len = 0, index = k; Node p1 = head, p2 = dummyHead; while (p1 != null) { len++; p1 = p1.next; if (index > 0) { index--; } else { p2 = p2.next; } } if (len >= k && k > 0) { p2.next = p2.next.next; } return dummyHead.next;} 0x05 合并两个有序的链表题目描述: 输入两个单调递增的链表,输出两个链表合成后的链表,需要合成后的链表单调递增。 1234567891011121314// 5.合并两个排序的链表public static Node merge(Node head1, Node head2) { if (head1 == null) return head2; if (head2 == null) return head1; Node t = null; if (head1.val < head2.val) { t = head1; t.next = merge(head1.next, head2); } else { t = head2; t.next = merge(head1, head2.next); } return t;}","link":"/blog/2020/08/20/%E7%AE%97%E6%B3%95-%E9%93%BE%E8%A1%A8%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/"},{"title":"链表相交","text":"编写一个程序,找到两个单链表相交的起始节点。 例如,下面的两个链表: 12345A: a1 → a2 ↘ c1 → c2 → c3 ↗ B: b1 → b2 → b3 在节点 c1 开始相交。 注意: 如果两个链表没有交点,返回 null. 在返回结果后,两个链表仍须保持原有的结构。 可假定整个链表结构中没有循环。 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。 方法一:哈希集合思路和算法 判断两个链表是否相交,可以使用哈希集合存储链表节点。 首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中: 如果当前节点不在哈希集合中,则继续遍历下一个节点; 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。 如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。 复杂度分析 时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。需要遍历两个链表各一次。 空间复杂度:O(m),其中 m 是链表 headA 的长度。需要使用哈希集合存储链表 headA 中的全部节点。 1234567891011121314151617181920212223242526272829/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { Set<ListNode> visited = new HashSet<ListNode>(); ListNode temp = headA; while (temp != null) { visited.add(temp); temp = temp.next; } temp = headB; while (temp != null) { if (visited.contains(temp)) { return temp; } temp = temp.next; } return null; }} 方法二:双指针思路和算法 使用双指针的方法,可以将空间复杂度降至 O(1)。 只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。 当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下: 每步操作需要同时更新指针 pA 和 pB; 如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB 移到下一个节点。 如果指针 pA 为空,则将指针 pA 移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。 当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。 证明 考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。 复杂度分析 时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。 空间复杂度:O(1)。 123456789101112131415161718192021222324/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA == null || headB == null) { return null; } ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == null ? headB : pA.next; pB = pB == null ? headA : pB.next; } return pA; }} 参考连接: https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/xiang-jiao-lian-biao-by-leetcode-solutio-a8jn/ https://zhuanlan.zhihu.com/p/48313122","link":"/blog/2021/06/18/%E7%AE%97%E6%B3%95-%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4/"},{"title":"自定义Scheme支持外部调用","text":"自定义Scheme支持外部调用0x01 制定Scheme为了使用户能够从其他APP直接跳到指定页面,开发者需要使用自定义Scheme。App自定义的Uri的格式:{scheme}://{host_path} 例如: 一个优酷的视频播放页可以被描述为:youku://play/video/12321; 一个多看的电子书详情页可以被描述为:duokan://detail/ebook/21312。 0x02 添加intent-filter在 Android Manifest 文件所对应的的 activity 添加 intent-filter 对于一个可以展示 {app_name}://{page}/{type}/{id} 12345678<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <!-- 处理以"app_name://page/type"开头的 URI --> <data android:scheme="app_name" /> <data android:host="detail" /> <data android:path="/type" /> </intent-filter> 0x03 使用 am 指令进行测试通过如下指令测试调起,如果能够正确地调起页面展示数据则说明 intent-filter 设置成功。 1adb shell am start -W -a "android.intent.action.VIEW" -d "yourUri" yourPackageName","link":"/blog/2023/09/15/%E8%87%AA%E5%AE%9A%E4%B9%89Scheme%E6%94%AF%E6%8C%81%E5%A4%96%E9%83%A8%E8%B0%83%E7%94%A8/"},{"title":"自定义TextView实现多个文案切换炫酷动画","text":"当显示2个或2个以上文案时,每隔2秒切换气泡文案 核心实现代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115import android.animation.ValueAnimatorimport android.content.Contextimport android.util.AttributeSetimport android.util.Logimport androidx.appcompat.widget.AppCompatTextViewclass TextViewSwitcher @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) { private var strs: List<String>? = null private var startPos: Int = 0 private val timeStep = 2000L // 2S private val TAG = "TextViewSwitcher" private val showNext = Runnable { showNextStr() } override fun onAttachedToWindow() { Log.d(TAG, "onAttachedToWindow: ") super.onAttachedToWindow() val size = strs?.size ?: 0 if (size > 1) { handler.postDelayed(showNext, timeStep) } } override fun onDetachedFromWindow() { Log.d(TAG, "onDetachedFromWindow: ") handler.removeCallbacks(showNext) super.onDetachedFromWindow() } fun setTextList(strs: List<String>?, startPos: Int = 0) { Log.d(TAG, "setTextList: ") if (strs.isNullOrEmpty()) return this.strs = strs this.startPos = startPos if (strs.size == 1) { text = strs[0] } else { this.startPos = startPos % strs.size text = strs[this.startPos] } } private fun showNextStr() { var startPos = this.startPos + 1 val size = strs?.size ?: 0 if (size <= 1) return if (startPos >= size) startPos %= size this.startPos = startPos changeTextWithAnimator(this, strs?.get(startPos)) handler.postDelayed(showNext, timeStep) } private fun changeTextWithAnimator( textView: AppCompatTextView?, nextContent: String? ) { if (textView != null) { val animator = ValueAnimator.ofFloat(0f, 2f) animator.duration = 400 var changed = false textView.pivotX = 0f val height = textView.measuredHeight if (height > 0) { textView.pivotY = height.toFloat() } val startWidth = textView.measuredWidth var endWidth = 0 val params = textView.layoutParams animator.addUpdateListener { animation -> val value = animation.animatedValue as Float when { value < 1f -> { textView.rotation = 360 - value * 60 textView.alpha = 1 - value } value > 1f -> { textView.alpha = value - 1 textView.rotation = 360 - (2 - value) * 60 if (!changed) { changed = true textView.text = nextContent val measureSpec = MeasureSpec.makeMeasureSpec( 0, MeasureSpec.UNSPECIFIED ) textView.measure(measureSpec, measureSpec) endWidth = textView.measuredWidth Log.d(TAG, "changeTextWithAnimator: endWidth=$endWidth") } else { if (endWidth > 0) { params.width = (startWidth + (endWidth - startWidth) * (value - 1)).toInt() textView.layoutParams = params } } } } } animator.start() } }} 由于动画要在顶部浮层,这样动画才能不被父类容器的大小所限制和切割,所以,直接在PopWindow中显示。具体代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162import android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.PopupWindowobject HomePromptView { @JvmStatic fun showTipPopView(view: View, typedStr: String?): PopupWindow { val rootView = LayoutInflater.from(view.context).inflate(R.layout.home_prompt_layout, null) val promptTv = rootView.findViewById<TextViewSwitcher>(R.id.tv_prompt) promptTv.setTextList(typedStr?.split("|")) rootView.isClickable = false // PopWindow val popTipWid = PopupWindow( rootView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) popTipWid.isTouchable = false // android.view.WindowManager$BadTokenException: // Unable to add window -- token null is not valid; // is your activity running? try { popTipWid.showAtLocation(view.rootView, 0, 0, 0) layoutPromptLocation(promptTv, view)// popTipWid.showAsDropDown(view, UiUtils.dip2px(39), -UiUtils.dip2px(48)) view.addOnLayoutChangeListener { view, i, i2, i3, i4, i5, i6, i7, i8 -> layoutPromptLocation(promptTv, view) } } catch (e: Exception) { e.printStackTrace() } return popTipWid } private fun layoutPromptLocation( promptTv: TextViewSwitcher, view: View ) { try { val params = promptTv.layoutParams as ViewGroup.MarginLayoutParams val location = IntArray(2) view.getLocationInWindow(location) params.topMargin = location[1] - UiUtils.getStatusBarHeight(view.context) + UiUtils.dip2px(4) params.leftMargin = location[0] + UiUtils.dip2px(25) promptTv.layoutParams = params } catch (e: Exception) { e.printStackTrace() } } @JvmStatic fun dismissTipPopView(popTipWid: PopupWindow?) { popTipWid?.dismiss() }} 添加布局代码如下: 12345678910111213141516171819202122232425<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/transparent"> <******.TextViewSwitcher android:id="@+id/tv_prompt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/common_ui_shape_red_heavy_prompt" android:maxLines="1" android:paddingStart="5dp" android:paddingTop="1.5dp" android:paddingEnd="5dp" android:paddingBottom="1.5dp" android:textColor="@color/white" android:textSize="10sp" tools:text="硬核安利"> </******.TextViewSwitcher></FrameLayout> drawable: 12345678910111213<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:bottomRightRadius="9dp" android:topLeftRadius="9dp" android:topRightRadius="9dp" /> <solid android:color="#FF5E79" /> <stroke android:width="0.5dp" android:color="@color/white" /></shape>","link":"/blog/2022/03/09/%E8%87%AA%E5%AE%9A%E4%B9%89TextView%E5%AE%9E%E7%8E%B0%E5%A4%9A%E4%B8%AA%E6%96%87%E6%A1%88%E5%88%87%E6%8D%A2%E7%82%AB%E9%85%B7%E5%8A%A8%E7%94%BB/"},{"title":"自定义布局:西部世界 第一季","text":"自定义布局:西部世界 第一季这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778/** * 系列名称... + 第一季 * 后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示 */class FixedEndLinearLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { //获取父布局测量size和model val widthSize = MeasureSpec.getSize(widthMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) if (childCount != 2) throw RuntimeException("FixedEndLinearLayout must have 2 children.") val wrapChild = getChildAt(0) val fixedChild = getChildAt(1) //测量 measureChild(fixedChild, widthMeasureSpec, heightMeasureSpec) val fixedParams = fixedChild.layoutParams as MarginLayoutParams val fixedChildWidth = fixedChild.measuredWidth + fixedParams.leftMargin + fixedParams.rightMargin val fixedChildHeight = fixedChild.measuredHeight + fixedParams.topMargin + fixedParams.bottomMargin val wrapChildWidthSpec = ViewGroup.getChildMeasureSpec( widthMeasureSpec, paddingLeft + paddingRight + fixedChildWidth, wrapChild.layoutParams.width ) val wrapChildHeightSpec = ViewGroup.getChildMeasureSpec( heightMeasureSpec, paddingTop + paddingBottom, wrapChild.layoutParams.height ) wrapChild.measure(wrapChildWidthSpec, wrapChildHeightSpec) val wrapParams = wrapChild.layoutParams as MarginLayoutParams val wrapChildWidth = wrapChild.measuredWidth + wrapParams.leftMargin + wrapParams.rightMargin val wrapChildHeight = wrapChild.measuredHeight + wrapParams.topMargin + wrapParams.bottomMargin val width = wrapChildWidth + fixedChildWidth val height = fixedChildHeight.coerceAtLeast(wrapChildHeight) start0 = paddingLeft + wrapParams.leftMargin start1 = paddingLeft + wrapChildWidth + fixedParams.leftMargin setMeasuredDimension( if (widthMode == MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight, if (heightMode == MeasureSpec.EXACTLY) heightSize else height + paddingTop + paddingBottom ) } private var start0 = 0 private var start1 = 0 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { val wrapChild = getChildAt(0) val fixedChild = getChildAt(1) val y0 = (measuredHeight - wrapChild.measuredHeight) / 2 val y1 = (measuredHeight - fixedChild.measuredHeight) / 2 wrapChild.layout( start0, y0, start0 + wrapChild.measuredWidth, y0 + wrapChild.measuredHeight ) fixedChild.layout( start1, y1, start1 + fixedChild.measuredWidth, y1 + fixedChild.measuredHeight ) }}","link":"/blog/2021/06/08/%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B8%83%E5%B1%80-FixedEndLinearLayout/"},{"title":"SpannableString 之居中显示 ImageSpan","text":"自定义布局:SpannableString 之居中显示 ImageSpan 12345678910111213141516171819202122232425class CenteredImageSpan(context: Context, drawableRes: Int) : ImageSpan(context, drawableRes) { override fun draw( canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint ) { // image to draw val b = drawable // font metrics of text to be replaced val fm = paint.fontMetricsInt var transY = ((y + fm.descent + y + fm.ascent) / 2 - b.bounds.bottom / 2) // to check the last line.(当 image 在单独一行显示时可能会存在这个问题) if (transY > bottom - b.bounds.bottom) transY = bottom - b.bounds.bottom canvas.save() canvas.translate(x, transY.toFloat()) b.draw(canvas) canvas.restore() }} 1234567891011val spanStr = SpannableStringBuilder()spanStr.append("# ")spanStr.append(title)val imageSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_jinghao_black)spanStr.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)// cornerspanStr.append(" #")val len = spanStr.lengthval cornerSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_remen)spanStr.setSpan(cornerSpan, len - 1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)binding.ctTalkDetailInfo.talkNameTv.text = spanStr","link":"/blog/2022/01/04/%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B8%83%E5%B1%80-SpannableString%E4%B9%8B%E5%B1%85%E4%B8%AD%E6%98%BE%E7%A4%BAImageSpan/"},{"title":"RoundImageView圆角控件","text":"RoundImageView圆角控件示例代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455import 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值属性: 123<declare-styleable name="RoundImageView"> <attr name="riv_radius" format="dimension" /></declare-styleable> 使用示例 123456<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" />","link":"/blog/2022/08/15/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6-RoundImageView/"},{"title":"VolumeDialog音量控制自定义","text":"VolumeDialog音量控制自定义1 使用自定义 AlertDialog 实现 2 window?.setFlags 设置 dialog 的样式 3 window?.attributes 设置 dialog 的位置 4 返回键监听,兼容机型需要使用 setOnKeyListener 5 按一次音量键回调多次的问题,KeyEvent.action 事件分 KeyEvent.ACTION_UP 和 KeyEvent.ACTION_DOWN 6 音量加减需要获取系统音量 max 值来手动控制,不同手机 max 值域不同 示例代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154import android.app.Activityimport android.content.Contextimport android.content.res.Resourcesimport android.media.AudioManagerimport android.os.Bundleimport android.os.Handlerimport android.os.Looperimport android.util.Logimport android.util.TypedValueimport android.view.Gravityimport android.view.KeyEventimport android.view.WindowManagerimport android.widget.ProgressBarimport androidx.appcompat.app.AlertDialog/** * 声音调整控件 */class VolumeDialog(activity: Activity) : AlertDialog(activity, R.style.VolumeDialog) { companion object { private const val TAG = "VolumeDialog" @JvmStatic fun show(activity: Activity) { activity.let { if (activity.isFinishing) return VolumeDialog(activity).show() } } } private val volumeAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager private var volume = volumeAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC) private val maxVolume = volumeAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) private val step = (maxVolume / 10).coerceAtLeast(1) private val delayMillis = 1500L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.dialog_volume_progress) //实现Dialog区域外部事件可以传给Activity // FLAG_NOT_TOUCH_MODAL作用:即使该window可获得焦点情况下,仍把该window之外的任何event发送到该window之后的其他window window?.setFlags( WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL ) // FLAG_WATCH_OUTSIDE_TOUCH作用:如果点击事件发生在window之外,就会收到一个特殊的MotionEvent,为ACTION_OUTSIDE window?.setFlags( WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH ) // 顶部显示 // window?.setGravity(Gravity.TOP) val attrs = window?.attributes attrs?.apply { gravity = Gravity.TOP height = WindowManager.LayoutParams.WRAP_CONTENT width = WindowManager.LayoutParams.MATCH_PARENT y = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 35f, Resources.getSystem().displayMetrics ).toInt() } Log.d(TAG, "onCreate() called with: attrs = $attrs") window?.attributes = attrs // 按空白处不能取消 setCanceledOnTouchOutside(false) // 初始化界面控件 initView() } private var volumeProgressView: VolumeProgressView? = null private var progressBar: ProgressBar? = null private val _handler = Handler(Looper.getMainLooper()) private val r = Runnable { try { dismiss() } catch (e: Exception) { // sometimes happens windows token error. e.printStackTrace() } } override fun dismiss() { _handler.removeCallbacks(r) super.dismiss() } private fun initView() { Log.d(TAG, "initView: ") volumeProgressView = findViewById(R.id.vpv_volume) progressBar = findViewById(R.id.pb_volume) refreshProgress(volume * 1f / maxVolume) // 解决不同机型版本兼容问题,onKeyDown 可能被拦截 setOnKeyListener { _, keyCode, event -> Log.d(TAG, "setOnKeyListener: keyCode = $keyCode, event = $event") if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { handleKeyEvent(keyCode, event) true } else { false } } } private fun refreshProgress(volumePercent: Float) { _handler.removeCallbacks(r) volumeProgressView?.setProgress(volumePercent) progressBar?.progress = (volumePercent * 1000).toInt() _handler.postDelayed(r, delayMillis) } private fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "handleKeyEvent: volume = $volume, maxVolume = $maxVolume, step = $step") if ((keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN) || event.action != KeyEvent.ACTION_DOWN) return false volume = if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // up if (volume == maxVolume) return true maxVolume.coerceAtMost(volume + step) } else { // down if (volume == 0) return true 0.coerceAtLeast(volume - step) } val volumePercent = volume * 1f / maxVolume refreshProgress(volumePercent) // 变更声音 volumeAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0) Log.d(TAG, "handleKeyEvent: AudioManager set volume = $volume done.") return true } /** * 返回事件,仅拦截音量控制事件 */ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "onKeyDown() called with: keyCode = $keyCode, event = $event") return if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { handleKeyEvent(keyCode, event) return true } else { super.onKeyDown(keyCode, event) } }} 自定义Dialog的Style 123456789101112 <style name="VolumeDialog" parent="android:style/Theme.Dialog"> <!--背景颜色及和透明程度--> <item name="android:windowBackground">@android:color/transparent</item> <!--是否去除标题 --> <item name="android:windowNoTitle">true</item> <!--是否去除边框--> <item name="android:windowFrame">@null</item> <!--是否浮现在activity之上--> <item name="android:windowIsFloating">true</item> <!--是否模糊--> <item name="android:backgroundDimEnabled">false</item></style>","link":"/blog/2021/05/24/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6-VolumeDialog/"},{"title":"设计模式@Java","text":"设计模式 Java版设计模式(Design Pattern)的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。正确使用设计模式具有以下优点: 可以提高程序员的思维能力、编程能力和设计能力。 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。 根据目的用途来划分,可分为创建型模式、结构型模式和行为型模式 3 种。 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。 根据作用范围来分,可分为类模式和对象模式两种。 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了以上 4 种,其他的都是对象模式。 0x01 单例模式单例模式是运用最广泛的设计模式之一,应用单例模式的类在整个程序中只有一个实例存在。通常用于很消耗资源的类,比如线程池,缓存,网络请求,IO操作,访问数据库等。为保证单例模式的线程安全,采用 双重校验。示例代码: 12345678910public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance;} 0x02 Builder模式将一个复杂对象的构建与对象的参数或部件的创建分离,达到解耦的目的。用户不用知道内部构建细节,可以更好的控制构建流程。 1234567891011121314151617181920public class Person { private int ID; private Person(Builder builder) { this.ID = builder.ID; } public static class Builder { private int ID; public Builder setID(int ID) { this.ID = ID; return this; } public Person build() { return new Person(this); } }}Person.Builder buider = new Person.Builder();buider.setID(1001);Person p1 = buider.build(); 0x03 工厂模式工厂模式是创建型模式,多用于需要生成复杂对象的地方。用new就可以完成创建的对象就无需使用。工厂模式降低了对象之间的耦合度,由于工厂模式依赖抽象的架构,实例化的任务交由子类去完成,所以有很好的扩展性。 12345678910111213141516171819202122232425262728293031323334353637383940414243/** * 工厂设计模式 * <p> * 为了解耦合,由工厂统一创建对象实例。 */interface Vehicle { void trip();}class QQ implements Vehicle { @Override public void trip() { System.out.println("QQ飞车..."); }}class BenZ implements Vehicle { @Override public void trip() { System.out.println("奔驰跑车..."); }}class VehicleFactory { public static final int QQ = 0x0001; public static final int BEN_Z = 0x0002; public static final Vehicle buildVehicle(int type) { if (QQ == type) { return new QQ(); } else if (BEN_Z == type) { return new BenZ(); } return null; }}public class FactoryTest { public static void main(String[] args) { Vehicle vehicle = VehicleFactory.buildVehicle(VehicleFactory.QQ); vehicle.trip(); }} 0x04 观察者模式让观察者和被观察者逻辑分开。使得UI层和业务逻辑清晰。 定义一个观察者: 12345678910111213public class PersonObserver implements Observer { static final String TAG = PersonObserver.class.getSimpleName(); String name; public PersonObserver (String name) { this.name = name; } @Override public void update(Observable observable, Object o) { Log.d(TAG,name + " 接收到通知啦 "+ o); }} 定义一个被观察者 12345678910111213141516171819public class PersonObservable extends Observable { List<PersonObserver> list = new ArrayList<>(); public void addObserver(PersonObserver observer) { list.add(observer); } public void removeObserver(PersonObserver observer) { if (list.contains(observer)){ list.remove(observer); } } public void notify(String info) { for (PersonObserver observer : list) { observer.update(this, info); };// 通知所有观察者 }} 注册和通知 123456PersonObserver xiaoMing = new PersonObserver("xiaoMing");// 注册PersonObservable pObservable = new PersonObservable();pObservable.addObserver(xiaoMing);// 通知pObservable.notify("notify!"); 0x05 代理模式(Proxy)代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。代理类和委托类(被代理类)应该共同实现一个接口,或者是共同继承某个类。 1)静态代理 12345678910111213141516171819202122232425262728293031323334353637383940/** * 1)静态代理 */interface DAO { void build();}class PartnerDAO implements DAO { @Override public void build() { System.out.println("PartnerDAO..."); }}class DAOProxy implements DAO { private DAO dao; public DAOProxy(DAO dao) { this.dao = dao; } @Override public void build() { System.out.println("build start..."); long start = System.currentTimeMillis(); dao.build(); long end = System.currentTimeMillis(); System.out.println("build start..."); System.out.println("执行耗费时间:" + (end - start)); }}public class ProxyTest { @Test public void test1() { PartnerDAO dao = new PartnerDAO(); DAOProxy daoProxy = new DAOProxy(dao); daoProxy.build(); }} 2)动态代理 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566/** * 2)动态代理 */interface DAO { void build();}class PartnerDAO implements DAO { @Override public void build() { System.out.println("PartnerDAO build()."); }}interface B { int work(boolean free);}class PartnerB implements B { // 不需要继承同一个接口或类 @Override public int work(boolean free) { System.out.println("PartnerB work()."); return 2; }}class DAOHandler implements InvocationHandler { private Object target; public DAOHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + " start..."); long start = System.currentTimeMillis(); Object result = method.invoke(target, args);// 执行原来的任务 long end = System.currentTimeMillis(); System.out.println(method.getName() + " start..."); System.out.println("执行耗费时间:" + (end - start)); return result; // 返回执行结果 }}public class ProxyTest { @Test public void test1() { PartnerDAO dao = new PartnerDAO(); ClassLoader loader = dao.getClass().getClassLoader(); Class<?>[] interfaces = dao.getClass().getInterfaces(); DAOHandler h = new DAOHandler(dao); DAO a1 = (DAO) Proxy.newProxyInstance(loader, interfaces, h); a1.build(); } @Test public void test2() { PartnerB pb = new PartnerB(); ClassLoader loader = pb.getClass().getClassLoader(); Class<?>[] interfaces = pb.getClass().getInterfaces(); DAOHandler h = new DAOHandler(pb); B b = (B) Proxy.newProxyInstance(loader, interfaces, h); b.work(true); }} 静态代理和动态代理的区别,静态代理一个代理类针对一个委托类,动态代理一个代理类可利用反射机制代理多个委托类。 静态代理在代码编译时就确定了委托类的类型,动态代理在代码运行时才动态加载委托类,运行时才确定委托类的类型。 使用场景:比如RPC框架和Spring AOP机制。 0x06 迭代器模式根据传入的列表类数据提供一个额外的遍历方法。 1234// 迭代器提供遍历方法,遍历while (cursor.hasNext()) { Log.d("Cursor",cursor.next());} 0x07 适配器模式( ListView 与 Adapter )将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作","link":"/blog/2020/08/05/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F@Java/"},{"title":"阿里云 Maven 仓库","text":"0x01 阿里云Maven仓库仓库地址:https://maven.aliyun.com/mvn/view 12maven { url 'https://maven.aliyun.com/repository/public' }maven { url 'https://maven.aliyun.com/repository/google' } 0x02 Gradle配置指南在 build.gradle 文件中加入以下代码: 123456789allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public/' // public仓是包含central仓和jcenter仓的聚合仓 } mavenLocal() mavenCentral() }} 如果想使用其它代理仓,以使用spring仓为例,代码如下: 123456789101112allProjects { repositories { maven { url 'https://maven.aliyun.com/repository/public/' } maven { url 'https://maven.aliyun.com/repository/spring/' } mavenLocal() mavenCentral() }} 加入你要引用的文件信息: 123dependencies { compile '[GROUP_ID]:[ARTIFACT_ID]:[VERSION]'} 执行命令: 1gradle dependencies 或 ./gradlew dependencies 安装依赖","link":"/blog/2020/02/26/%E9%98%BF%E9%87%8C%E4%BA%91Maven%E4%BB%93%E5%BA%93%E5%9C%B0%E5%9D%80/"},{"title":"ArkTS中的typeof与instanceof使用说明","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) ArkTS 中的 typeof 与 instanceof 使用说明开发过程中遇到判断一个变量是否是 string 类型,先看下面一处代码: 1234567let url: string | PixelMap | Resource | undefined = "xxx.xxx";if ("string" == typeof url) { // this is true.}if (url instanceof String) { // this is false.} 0x01 typeoftypeof 操作符用于检测基本数据类型(如:string, number, boolean, undefined, function, object 等),返回一个表示未定义变量类型的字符串。对于函数对象,它将返回“function”。对于 object 对象的具体类型(如 Array, Date 等)则无法判断。例如,对于数组,typeof 会返回“object”,无法区分是数组还是其他对象。需要注意的是,typeof对 null 返回的是“object”。 0x02 instanceofinstanceof操作符主要用于检测对象是否由特定的构造函数创建,因此可以用来判断对象的具体类型。例如,如果你有一个 Array 对象,你可以使用 instanceof 来检测它是否是一个数组:let arr = []; console.log(arr instanceof Array); // 输出:true。 instanceof 有一个限制,它只能用于对象,不能用于基本数据类型,而且它要求对象是通过构造函数创建的。对于不是通过构造函数创建的对象,instanceof 将返回 false。 0x03 总结typeof 操作符用于检测基本数据类型,instanceof操作符用来判断 object 对象的具体类型。 相关文章https://developer.baidu.com/article/detail.html?id=3318356","link":"/blog/2024/04/30/%E9%B8%BF%E8%92%99-ArkTS%E4%B8%AD%E7%9A%84typeof%E4%B8%8Einstanceof%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E/"},{"title":"ArkTS遍历Record对象","text":"ArkTS 遍历 [key:values] 类型的 Record 和 object 对象DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 0x01 Object.keys对于 key-values 类型的 Record 或者 object 对象,可以使用 Object.keys 得到一个 keys 的数组集合,然后遍历该数组获取 key 和 value 值。 1234// let data = { "a": "1", b: 2, c: 3 }; // this line api 11 no longer works.Object.keys(data).forEach((key: string) => { console.log(key, data[key]);}); 0x02 Object.entries对于 key-values 类型的 Record 或者 object 对象,可以使用 Object.entries 把 key-values 对象变成数组,之后再组装成一个 Map 对象进行遍历。 1234567// let data = { "a": "1", b: 2, c: 3 }; // this line api 11 no longer works.let map: Map<ESObject, ESObject> = new Map<ESObject, ESObject>( Object.entries(data));map.forEach((value: ESObject, key: string) => { console.log(key, value);}); 参考https://segmentfault.com/q/1010000044602257","link":"/blog/2024/05/09/%E9%B8%BF%E8%92%99-ArkTS%E9%81%8D%E5%8E%86Record%E5%AF%B9%E8%B1%A1/"},{"title":"HDC常用功能","text":"SDK 版本:HarmonyOS NEXT Developer Beta2 SDK (5.0.0.31)DevEco-Studio 版本:DevEco Studio NEXT Developer Beta2 (5.0.3.502)工程机版本:ALN-AL00 NEXT.0.0.31 0x01 鸿蒙样机升级,查询软件版本和序列号123456## 获取 OTA 系统(鸿蒙)软件版本$ hdc shell param get const.product.software.version## 获取序列号(SN)$ hdc list targets 麻烦升级 NEXT.0.0.68 版本OTA 系统版本号ALT-AL10 5.0.0.66(SP6C00E66R6P7log)序列号22M0224313000155 0x02 鸿蒙手机抓取全量日志保存到本地文件1234hdc shell hilog -Q domainoffhdc shell hilog -Q pidoffhdc shell hilog -b Dhdc hilog >> d://txt.log","link":"/blog/2024/08/08/%E9%B8%BF%E8%92%99-HDC%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/"},{"title":"鸿蒙-HDC 常用命令","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2(4.1.3.700)HarmonyOS API 版本:4.1.0(11) HDC 常用命令0x01 全局相关命令123456789101112131415161718# 显示hdc相关的帮助信息hdc -h# 显示hdc的版本信息hdc -v# 获取OTA系统版本号hdc shell param get const.product.software.version# 获取序列号(获取设备信息)# 查询已连接的所有目标设备,添加-v选项,则会打印设备详细信息。hdc list targets# 交互命令,COMMAND表示需要执行的单次命令。不同类型或版本的系统支持的COMMAND命令有所差异,可以通过hdc shell ls /system/bin查阅支持的命令列表。hdc shellhdc shell ps -efhdc shell help -a // 查询全部可用命令 0x02 服务进程相关命令12345678# 重启目标设备,查看目标列表可用list targets命令。hdc target boot# 终止hdc服务进程,使用-r参数触发服务进程重新启动。hdc kill [-r]# 启动hdc服务进程,使用-r参数触发服务进程重新启动。hdc start [-r] 0x03 文件相关命令123456# 从本地发送文件至远端设备。hdc file send E:\\example.txt /data/local/tmp/example.txt# 从远端设备发送文件至本地。hdc file recv /data/local/tmp/a.txt ./a.txt 0x04 应用相关命令123456789# 安装指定的应用package文件。hdc install -r E:\\com.example.hello.hap# 卸载指定的应用包package包名。hdc uninstall com.example.hello# 显示可调试应用列表。hdc jpid hdc help12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364 OpenHarmony device connector(HDC) ...---------------------------------global commands:---------------------------------- -h/help [verbose] - Print hdc help, 'verbose' for more other cmds -v/version - Print hdc version -t connectkey - Use device with given connect key---------------------------------component commands:-------------------------------session commands(on server): list targets [-v] - List all devices status, -v for detail start [-r] - Start server. If with '-r', will be restart server kill [-r] - Kill server. If with '-r', will be restart serverservice commands(on daemon): target mount - Set /system /vendor partition read-write target boot [-bootloader|-recovery] - Reboot the device or boot into bootloader\\recovery. target boot [MODE] - Reboot the into MODE. smode [-r] - Restart daemon with root permissions, '-r' to cancel root permissions tmode usb - Reboot the device, listening on USB tmode port [port] - Reboot the device, listening on TCP port---------------------------------task commands:-------------------------------------file commands: file send [option] local remote - Send file to device file recv [option] remote local - Recv file from device option is -a|-s|-z -a: hold target file timestamp -sync: just update newer file -z: compress transfer -m: mode syncforward commands: fport localnode remotenode - Forward local traffic to remote device rport remotenode localnode - Reserve remote traffic to local host node config name format 'schema:content' examples are below: tcp:<port> localfilesystem:<unix domain socket name> localreserved:<unix domain socket name> localabstract:<unix domain socket name> dev:<device name> jdwp:<pid> (remote only) fport ls - Display forward/reverse tasks fport rm taskstr - Remove forward/reverse task by taskstringapp commands: install [-r|-s] src - Send package(s) to device and install them src examples: single or multiple packages and directories (.hap .hsp) -r: replace existing application -s: install shared bundle for multi-apps uninstall [-k] [-s] package - Remove application package from device -k: keep the data and cache directories -s: remove shared bundledebug commands: hilog [-h] - Show device log, -h for detail shell [COMMAND...] - Run shell command (interactive shell if no command given) bugreport [FILE] - Return all information from the device, stored in file if FILE is specified jpid - List pids of processes hosting a JDWP transportsecurity commands: keygen FILE - Generate public/private key; key stored in FILE and FILE.pub 参考HarmonyOS NEXT Developer:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/hdc-V5","link":"/blog/2024/06/28/%E9%B8%BF%E8%92%99-HDC%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/"},{"title":"鸿蒙-MD5摘要(ArkTS)","text":"SDK 版本:HarmonyOS NEXT Developer Beta2 SDK (5.0.0.31)DevEco-Studio 版本:DevEco Studio NEXT Developer Beta2 (5.0.3.502)工程机版本:ALN-AL00 NEXT.0.0.31 简介MD5 算法常常被用来验证网络文件传输的完整性,防止文件被人篡改。MD5 全称是报文摘要算法(Message-Digest Algorithm 5),此算法对任意长度的信息逐位进行计算,产生一个二进制长度为 128 位(十六进制长度就是 32 位)的“指纹”(或称“报文摘要”),不同的文件产生相同的报文摘要的可能性是非常非常之小的。 Linux 下 md5sum 用法 (查看文件或字符串的 md5 值)md5sum 命令采用 MD5 报文摘要算法(128 位)计算和检查文件的校验和。一般来说,安装了 Linux 后,就会有 md5sum 这个工具,直接在命令行终端直接运行。 windows 下如果安装了 git-bash 也可以在所在的文件夹下,右击,选择[Git Bash Here],也可以直接使用 md5sum 命令。 MD5 ArkTS 实现代码MD5Util.ets 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394/* * Copyright (c) 2024. Dench. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { cryptoFramework } from "@kit.CryptoArchitectureKit";import { buffer } from "@kit.ArkTS";import { hash } from "@kit.CoreFileKit";import { systemDateTime } from "@kit.BasicServicesKit";/** * 计算字符串Md5 */export async function md5(messageText: string): Promise<string> { const md = cryptoFramework.createMd("MD5"); await md.update({ data: new Uint8Array(buffer.from(messageText, "utf-8").buffer), }); const mdOutput = await md.digest(); console.info("md mdOutput: " + mdOutput.data); let mdLen = md.getMdLength(); console.info("md mdLen: " + mdLen); const result = buffer.from(mdOutput.data.buffer).toString("hex"); console.info("md result: " + result); return result;}// 文件Md5, 哈希计算采用的算法。可选 "md5"、"sha1" 或 "sha256"。建议采用安全强度更高的 "sha256", 此处使用"md5"export async function fileMd5(filePath: string): Promise<string> { try { const t = systemDateTime.getTime(); const result = await hash.hash(filePath, "md5"); console.info("fileMd5: result=" + result); console.info(`fileMd5: duration=${systemDateTime.getTime() - t}`); return result; } catch (err) { console.error(`fileMd5: ${JSON.stringify(err)}`); return ""; }}// 文件MD5,大文件读写效率低,有性能瓶颈,采用上面的文件Hash算法// export async function fileMd5(path: string, algName: string = "MD5"): Promise<string> {// console.info("fileMd5: path=" + path)// let md = cryptoFramework.createMd(algName);// try {// const t = systemDateTime.getTime();// let file = await fs.open(path, fs.OpenMode.READ_ONLY);// let arrayBuffer = new ArrayBuffer(4096);// let offset = 0;// let readOptions: ReadOptions = {// offset: offset,// length: 4096// };// let len = await fs.read(file.fd, arrayBuffer, readOptions);// while (len > 0) {// // console.info("md read file len: " + len);// const bf = buffer.from(arrayBuffer).subarray(0, len);// // console.info("md bf: " + bf.buffer.byteLength);// const uint8 = new Uint8Array(bf.buffer);// // console.info("md uint8: " + uint8.byteLength);// await md.update({ data: uint8 });// offset += len;// readOptions.offset = offset;// len = await fs.read(file.fd, arrayBuffer, readOptions);// }// try {// fs.close(file);// } catch (e) {// console.info(JSON.stringify(e));// }// let mdOutput = await md.digest();// // console.info("md mdOutput: " + mdOutput.data);// let mdLen = md.getMdLength();// // console.info("md mdLen: " + mdLen);// const result = buffer.from(mdOutput.data.buffer).toString('hex');// console.info("md succeed: " + result);// console.info(`md duration=${systemDateTime.getTime() - t}`);// return result;// } catch (e) {// console.error("md error: " + JSON.stringify(e));// return ''// }// } Referencehttps://www.cnblogs.com/kevingrace/p/10201723.htmlhttps://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-file-hash-V5","link":"/blog/2024/06/27/%E9%B8%BF%E8%92%99-MD5%E6%91%98%E8%A6%81/"},{"title":"鸿蒙-Promise 并发(JS)","text":"Promise 并发(JS)在 async 函数中,“过度 await”代码非常普遍。将 Promise.all() 与异步函数一起使用,可以有效的实现并发。 例如,两个异步函数 fetchA, fetchB: 123456789async function fetchA() { const response = await fetch("/path/a"); return await response.json();}async function fetchB() { const response = await fetch("/path/b"); return await response.json();} await 运算符会让异步函数串行执行,执行 fetchA()获得结果之后,才去执行 fetchB()。如下: 12345async function awaitFunc() { const a = await fetchA(); const b = await fetchB(); return handleResult(a, b);} 我们可以使用 Promise.all 让异步函数并发执行。 1234async function promiseAllFunc() { const [a, b] = await Promise.all([fetchA(), fetchB()]); return handleResult(a, b);} Promise 类提供了以下四种异步任务的并发。 Ox01 Promise.all()Promise.all() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组。 所有的 Promise 都被兑现时兑现,并返回一个包含所有兑现值的数组; 在任何一个输入的 Promise 被拒绝时立即拒绝,并带回被拒绝的原因; 12345678910const promise1 = Promise.resolve(3);const promise2 = 42;const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, "foo");});Promise.all([promise1, promise2, promise3]).then((values) => { console.log(values);});// Expected output: Array [3, 42, "foo"] Ox02 Promise.allSettled()Promise.allSettled() 静态方法将一个 Promise 可迭代对象作为输入,并返回一个单独的 Promise。当所有输入的 Promise 都已敲定时(包括传入空的可迭代对象时),返回的 Promise 将被兑现,并带有描述每个 Promise 结果的对象数组。 在所有的 Promise 都被敲定时兑现,返回一个带有描述每个 Promise 结果的对象数组; 永远不会被 reject; 12345678910111213Promise.allSettled([ Promise.resolve(33), new Promise((resolve) => setTimeout(() => resolve(66), 0)), 99, Promise.reject(new Error("一个错误")),]).then((values) => console.log(values));// [// { status: 'fulfilled', value: 33 },// { status: 'fulfilled', value: 66 },// { status: 'fulfilled', value: 99 },// { status: 'rejected', reason: Error: 一个错误 }// ] status: 一个字符串,要么是 “fulfilled”,要么是 “rejected”,表示 promise 的最终状态。 value: 仅当 status 为 “fulfilled”,才存在。promise 兑现的值。 reason: 仅当 status 为 “rejected”,才存在,promsie 拒绝的原因。 Ox03 Promise.any()在任意一个 Promise 被兑现时兑现;仅在所有的 Promise 都被拒绝时才会拒绝。 123456789const promise1 = Promise.reject(0);const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick"));const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow"));const promises = [promise1, promise2, promise3];Promise.any(promises).then((value) => console.log(value));// Expected output: "quick" Ox04 Promise.race()在任意一个 Promise 被敲定时敲定。换句话说,在任意一个 Promise 被兑现时兑现;在任意一个的 Promise 被拒绝时拒绝。 123456789101112131415161718192021222324252627282930313233343536373839404142434445function sleep(time, value, state) { return new Promise((resolve, reject) => { setTimeout(() => { if (state === "兑现") { return resolve(value); } else { return reject(new Error(value)); } }, time); });}const p1 = sleep(500, "一", "兑现");const p2 = sleep(100, "二", "兑现");Promise.race([p1, p2]).then((value) => { console.log(value); // “二” // 两个都会兑现,但 p2 更快});const p3 = sleep(100, "三", "兑现");const p4 = sleep(500, "四", "拒绝");Promise.race([p3, p4]).then( (value) => { console.log(value); // “三” // p3 更快,所以它兑现 }, (error) => { // 不会被调用 });const p5 = sleep(500, "五", "兑现");const p6 = sleep(100, "六", "拒绝");Promise.race([p5, p6]).then( (value) => { // 不会被调用 }, (error) => { console.error(error.message); // “六” // p6 更快,所以它拒绝 }); 参考MDN Web 开发技术:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise","link":"/blog/2024/06/27/%E9%B8%BF%E8%92%99-Promise%E5%B9%B6%E5%8F%91/"},{"title":"鸿蒙-TextInput清除按钮实现","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) TextInput 清除按钮实现自定义 TextInput,TextArea 组件,实现一键清空已输入内容的按钮。 具体代码如下: 12345678910111213141516171819202122@State input: string = '';@BuilderSearchLayout() { Row() { TextInput({placeholder: 'inupt your text...',}) .onChange((value) => { this.input = value }) .layoutWeight(1) ImageView({ option: { loadSrc: $r("app.media.clear"), width: 16, height: 16, } }) .visibility(this.input == '' ? Visibility.None : Visibility.Visible) .onClick(() => { this.input = '' }) }} 参考https://ost.51cto.com/answer/8395","link":"/blog/2024/05/11/%E9%B8%BF%E8%92%99-TextInput%E6%B8%85%E9%99%A4%E6%8C%89%E9%92%AE%E5%AE%9E%E7%8E%B0/"},{"title":"鸿蒙-TextInput首次进入页面不弹键盘","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) TextInput 首次进入页面不弹键盘搜索结果页面的顶部有个 TextInput 输入框,导致一进入页面会自动拉起键盘。这是因为进入页面时,TextInput 会自动获得焦点。系统组件提供了defaultFocus()方法,用来手动控制是否默认获取焦点。 注意,单纯设置 TextInput 的defaultFocus(false)可能会不生效,需要当前页面中有主动承接默认焦点的控件才行。 具体代码如下: 12345678910111213Image($r("app.media.back")) .width(24) .height(24) .onClick(() => {}) .focusable(true) .defaultFocus(true);TextInput({ placeholder: "搜索您想要的内容",}) .focusable(true) .focusOnTouch(true) .defaultFocus(false); 参考https://blog.csdn.net/Mayism123/article/details/137349464","link":"/blog/2024/05/11/%E9%B8%BF%E8%92%99-TextInput%E9%A6%96%E6%AC%A1%E8%BF%9B%E5%85%A5%E9%A1%B5%E9%9D%A2%E4%B8%8D%E5%BC%B9%E9%94%AE%E7%9B%98/"},{"title":"鸿蒙-Text组件使用自定义字体","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 1 导入字体文件将字体文件BebasNeue-Regular.ttf放在项目的resources/rawfile文件夹下,如下图: 2 注册自定义字体需要在组件的 aboutToAppear() 方法中,使用font注册自定义字体。 12345678910111213141516171819202122232425262728import font from '@ohos.font';@Componentexport struct CustomFontComponent { aboutToAppear(): void { font.registerFont({ familyName: 'BebasNeue', familySrc: $rawfile('BebasNeue-Regular.ttf') }) } build() { Column() { Text('9999') .fontSize(17) .fontColor('#333333') .fontFamily('BebasNeue') .maxLines(1) .textAlign(TextAlign.Center) .fontWeight(FontWeight.Regular) .textOverflow({ overflow: TextOverflow.Ellipsis }) .height('100%') .width('100%'); } }} 3 使用已注册的字体在Text组件中使用已注册的字体,设置fontFamily为已注册的familyName即可。 参考https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-font-V5","link":"/blog/2024/06/04/%E9%B8%BF%E8%92%99-Text%E7%BB%84%E4%BB%B6%E4%BD%BF%E7%94%A8%E8%87%AA%E5%AE%9A%E4%B9%89%E5%AD%97%E4%BD%93/"},{"title":"鸿蒙-下拉刷新控件PullToRefresh使用","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11)PullToRefresh 版本:"@ohos/pulltorefresh": "2.0.5" 下拉刷新控件 PullToRefresh 使用 支持自定义 header,footer 没有更多了布局 具体代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117import { HomeVM } from '../vm/HomeVM';import { PullToRefresh } from '@ohos/pulltorefresh';@Componentexport struct ListAreaComponent { @State data?: ListDataWrapper[] | null = null private vm: HomeVM = new HomeVM() // 需绑定列表或宫格组件 private scroller: Scroller = new Scroller(); aboutToAppear(): void { // request data. this.vm.requestData().then((data: ListDataWrapper[]) => { this.data = data; }) } build() { PullToRefresh({ // 必传项,列表组件所绑定的数据 data: this.data, // 必传项,需绑定传入主体布局内的列表或宫格组件 scroller: this.scroller, // 必传项,自定义主体布局,内部有列表或宫格组件 customList: () => { // 一个用@Builder修饰过的UI方法 this.getListView() }, // 可选项,下拉刷新回调 onRefresh: () => { return new Promise<string>((resolve) => { this.vm.requestData().then((data: ListDataWrapper[]) => { this.data = data resolve('') }).catch((error: Error) => { resolve('') }); }); }, // 可选项,上拉加载更多回调 onLoadMore: () => { return new Promise<string>((resolve) => { resolve('') }); }, customLoad: commonNoMore, customRefresh: commonLoading, }) .width('100%') .height('100%') } @Builder getListView() { List({ space: 12, scroller: this.scroller }) { ForEach(this.data, (item: ListDataWrapper, index: number) => { ListItem() { // ... }; }, (item: ListDataWrapper, index?: number) => index + JSON.stringify(item)); } .width('100%') .height('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.None) }}@Builderexport function commonLoading(): void { Stack() { // Text(refreshText) // .textAlign(TextAlign.Center) // .fontColor( 0) // .fontSize( 0) Stack() { Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true))) .width('100%') .height('100%') .onReady(() => { // this.initCanvas(); }) // .visibility(this.state == IS_PULL_DOWN_2 ? Visibility.Visible : Visibility.Hidden) // .visibility(Visibility.Hidden) LoadingProgress() .color('#FF00A3FF') .width(32) .height(32) } .width('100%') .height('100%') } .width('100%') .height('100%') .clip(true)}@Builderexport function commonNoMore(): void { Stack() { Text('已经到底了~') .textAlign(TextAlign.Center) .fontColor('#FF85888F') .fontSize(14) } .width('100%') .height('100%') .clip(true)}@Builderexport function commonEmpty(): void { Stack() .width('100%') .height('100%')} 注意,列表组件需要设置为无边缘效果:List().edgeEffect(EdgeEffect.None)。 参考https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fpulltorefresh","link":"/blog/2024/05/17/%E9%B8%BF%E8%92%99-%E4%B8%8B%E6%8B%89%E5%88%B7%E6%96%B0%E6%8E%A7%E4%BB%B6pulltorefresh%E4%BD%BF%E7%94%A8/"},{"title":"鸿蒙-使用系统文件预览不同类型的本地文件","text":"SDK 版本:HarmonyOS NEXT Developer Beta2 SDK (5.0.0.31)DevEco-Studio 版本:DevEco Studio NEXT Developer Beta2 (5.0.3.502)工程机版本:ALN-AL00 NEXT.0.0.31 使用系统文件预览不同类型的本地文件MediaUtils.ets 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475/* * Copyright (c) 2024. Dench. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { fileUri } from "@kit.CoreFileKit";import { BusinessError } from "@kit.BasicServicesKit";import { filePreview } from "@kit.PreviewKit";import { uniformTypeDescriptor } from "@kit.ArkData";/** * filePreview(文件预览) */export function openSystemPreview(context: Context, path: string) { // let uiContext = getContext(this); // let displayInfo: filePreview.DisplayInfo = { // x: 100, // y: 100, // width: 800, // height: 800 // }; const uri = fileUri.getUriFromPath(path); const mimeType = getFileMimeType(path); console.info("uri:" + uri); console.info("mimeType:" + mimeType); let fileInfo: filePreview.PreviewInfo = { uri: uri, mimeType: mimeType, }; filePreview .openPreview(context, fileInfo) .then(() => { console.info("Succeeded in opening preview"); }) .catch((err: BusinessError) => { console.error( `Failed to open preview, err.code = ${err.code}, err.message = ${err.message}` ); });}/** * 根据文件后缀查询对应文件的 mimeType */export function getFileMimeType(path: string): string { if (path.indexOf(".") != -1) { try { const ext = "." + path.split(".").pop(); console.info("ext:" + ext); // 2.可根据 “.mp3” 文件后缀查询对应UTD数据类型,并查询对应UTD数据类型的具体属性 let typeId1 = uniformTypeDescriptor.getUniformDataTypeByFilenameExtension(ext); console.info("typeId1:" + typeId1); let typeObj1 = uniformTypeDescriptor.getTypeDescriptor(typeId1); // console.info('typeId:' + typeObj1.typeId); console.info("belongingToTypes:" + typeObj1.belongingToTypes); // console.info('description:' + typeObj1.description); // console.info('referenceURL:' + typeObj1.referenceURL); // console.info('filenameExtensions:' + typeObj1.filenameExtensions); console.info("mimeTypes:" + typeObj1.mimeTypes); return typeObj1.mimeTypes[0]; } catch (e) {} } return "text/plain";} Referencehttps://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/preview-arkts-V5#section1081123302517 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/uniform-data-type-descriptors-V5","link":"/blog/2024/08/08/%E9%B8%BF%E8%92%99-%E4%BD%BF%E7%94%A8%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E9%A2%84%E8%A7%88%E4%B8%8D%E5%90%8C%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%9C%AC%E5%9C%B0%E6%96%87%E4%BB%B6/"},{"title":"基于axios和Promise的网络框架封装","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11)axios 版本:"@ohos/axios": "^2.2.0" 基于 axios 和 Promise 的网络框架封装 Get Post 方式支持 http 其他请求方式(method)支持 接口 url 参数封装 和 全局的 baseUrl 设置 超时时间设置 全局 Headers,接口自定义 Headers 和 请求 headers 拦截器封装和实现 请求 params 参数和 data 数据支持 post 支持 x-www-form-urlencoded 数据格式 请求结果 Json 数据解析(框架已自动解析) 请求结果流程控制,Promise 封装 请求结果 header 数据解析,服务器时间戳和 session 关键代码 HttpUtil.ets封装如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286/* * Copyright (c) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from '@ohos/axios';import ResponseResult from './ResponseResult';import logger from '../util/Logger';import { systemDateTime } from '@kit.BasicServicesKit';import { HashMap } from '@kit.ArkTS';import Constant from '../common/Constant';const TAG: string = "HttpUtil"const timeout = 20000 // 20s超时const baseUrl = 'https://xxx.xxx.com'export function httpDefaultSetting() { // default settings axios.defaults.baseURL = baseUrl; axios.defaults.timeout = timeout; // default headers axios.defaults.headers.common['Client-Type'] = 'xxx'; axios.defaults.headers.common['Client-Version'] = '1.0.4'; axios.defaults.headers.common['Os'] = 'hmos'; axios.defaults.headers.common['Token'] = 'xxx'; // for post axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' // 添加请求拦截器 axios.interceptors.request.use((config: InternalAxiosRequestConfig) => { return transRequest(config); }, (error: AxiosError) => { return Promise.reject(error); }); // 添加响应拦截器 axios.interceptors.response.use((response: AxiosResponse) => { return transResponse(response); }, (error: AxiosError) => { return Promise.reject(error); });}/** * 请在这里处理请求体的拦截器操作逻辑 * */function transRequest(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { try { let millis = systemDateTime.getTime(); config.headers['t'] = millis - Constant.offsetTime; // 同步时间 // 增加验签逻辑 // 验签可以仅在需要的请求中增加验签,通过增加特定的header属性来区分 } finally { return config; }}/** * 请在这里处理请求结果的拦截器操作逻辑 * */function transResponse(response: AxiosResponse): AxiosResponse { try { let millis = systemDateTime.getTime(); if (lt != 0 && millis - lt < 60000) return response; // 可选,性能优化 1分钟内避免重复处理 lt = millis let headers: HashMap<string, ESObject> = JSON.parse(JSON.stringify(response.headers)); let t: number = headers['servertimestamp']; Constant.offsetTime = millis - t; return response; } catch (e) { console.error(e) return response; }}let lt = 0/** * Initiates an HTTP request to a given URL. * * @param url URL for initiating an HTTP request. * @param params Params for initiating an HTTP request. */export function httpGet<D>(url: string, params?: ESObject, headers?: ESObject): Promise<D> { logger.debug(TAG, "httpGet: "); return new Promise<D>((resolve: Function, reject: Function) => { let startTime = systemDateTime.getTime() axios.get<ResponseResult, AxiosResponse<ResponseResult>, null>(url, { headers: headers, // 指定请求超时的毫秒数(0 表示无超时时间) timeout: timeout, // 超时 // `connectTimeout` 指定请求连接服务器超时的毫秒数(0 表示无超时时间) // 如果请求连接服务器超过 `connectTimeout` 的时间,请求将被中断 // connectTimeout: 60000, // 文档和代码不一致,代码中无法设置连接超时时间 params: params, }) .then((response: AxiosResponse<ResponseResult>) => { let duration = (systemDateTime.getTime() - startTime).toString() logger.debug(TAG, "httpGet: Success. duration=" + duration); logger.debug(TAG, "--------------------------------------"); logger.debug(TAG, "config=" + JSON.stringify(response.config)); logger.debug(TAG, "status=" + response.status); // logger.debug(TAG, "statusText=" + response.statusText); // always empty?? logger.debug(TAG, "headers=" + JSON.stringify(response.headers)); logger.debug(TAG, "data=" + JSON.stringify(response.data)); logger.debug(TAG, "--------------------------------------"); if (isSuccess(response)) { if (isResultSuccess(response.data)) { resolve(response.data.data); } else { const e: Error = { name: `${response.data.code}`, message: `${response.data.msg}` } reject(e); } } else { const e: Error = { name: `${response.status}`, message: `${response.statusText}` } reject(e); } }) .catch((reason: AxiosError) => { logger.error(TAG, JSON.stringify(reason)); reject(reason) }) });}function getRequestFormData(data?: ESObject): string | undefined { if (data == undefined) return undefined; let sb = new StringBuilder(); Object.keys(data).forEach((key: string) => { sb.append(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) }) const formData = sb.build('&'); logger.debug(TAG, "getRequestFormData: formData=" + formData); return formData;}function buildPostRequestHeader(isFormUrlencoded: boolean, headers?: Record<ESObject, ESObject>): Record<ESObject, ESObject> { if (headers != null) { headers['Content-Type'] = isFormUrlencoded ? 'application/x-www-form-urlencoded' : 'application/json' return headers } return { 'Content-Type': isFormUrlencoded ? 'application/x-www-form-urlencoded' : 'application/json', }}/** * Initiates an HTTP request to a given URL. * * @param url URL for initiating an HTTP request. * @param params Params for initiating an HTTP request. */// o: { [s: string]: ESObject }export function httpPost<D>(url: string, isFormUrlencoded: boolean = true, data?: ESObject, params?: ESObject, headers?: ESObject): Promise<D> { // logger.debug(TAG, "httpPost: "); return new Promise<D>((resolve: Function, reject: Function) => { let startTime = systemDateTime.getTime() axios.post(url, isFormUrlencoded ? getRequestFormData(data) : data, { headers: buildPostRequestHeader(isFormUrlencoded, headers), // 指定请求超时的毫秒数(0 表示无超时时间) timeout: timeout, // 超时 // `connectTimeout` 指定请求连接服务器超时的毫秒数(0 表示无超时时间) // 如果请求连接服务器超过 `connectTimeout` 的时间,请求将被中断 // connectTimeout: 60000, // 文档和代码不一致,代码中无法设置连接超时时间 params: params, }) .then((response: AxiosResponse<ResponseResult>) => { let duration = (systemDateTime.getTime() - startTime).toString() logger.debug(TAG, "httpPost: Success. duration=" + duration); logger.debug(TAG, "--------------------------------------"); logger.debug(TAG, "config=" + JSON.stringify(response.config)); logger.debug(TAG, "status=" + response.status); // logger.debug(TAG, "statusText=" + response.statusText); // always empty?? logger.debug(TAG, "headers=" + JSON.stringify(response.headers)); logger.debug(TAG, "data=" + JSON.stringify(response.data)); logger.debug(TAG, "--------------------------------------"); if (isSuccess(response)) { if (isResultSuccess(response.data)) { resolve(response.data.data); } else { const e: Error = { name: `${response.data.code}`, message: `${response.data.msg}` } reject(e); } } else { const e: Error = { name: `${response.status}`, message: `${response.statusText}` } reject(e); } }) .catch((reason: AxiosError) => { logger.error(TAG, JSON.stringify(reason)); reject(reason) }) });}/** * Initiates an HTTP request to a given URL. * * @param url URL for initiating an HTTP request. * @param params Params for initiating an HTTP request. */export function httpRequest<D>(url: string, method?: Method | string, data?: D, config?: AxiosRequestConfig<D>): Promise<ResponseResult> { // logger.debug(TAG, "httpRequest: "); return new Promise<ResponseResult>((resolve: Function, reject: Function) => { let startTime = systemDateTime.getTime() axios.request<ResponseResult, AxiosResponse<ResponseResult>, D>({ url: url, method: method, baseURL: baseUrl, headers: config?.headers, // 指定请求超时的毫秒数(0 表示无超时时间) timeout: timeout, // 超时 // `connectTimeout` 指定请求连接服务器超时的毫秒数(0 表示无超时时间) // 如果请求连接服务器超过 `connectTimeout` 的时间,请求将被中断 // connectTimeout: 60000, // 文档和代码不一致,代码中无法设置连接超时时间 params: config?.params, data: data ?? config?.data }) .then((response: AxiosResponse<ResponseResult>) => { let duration = (systemDateTime.getTime() - startTime).toString() logger.debug(TAG, "httpRequest: Success. duration=" + duration); logger.debug(TAG, "--------------------------------------"); logger.debug(TAG, "config=" + JSON.stringify(response.config)); logger.debug(TAG, "status=" + response.status); // logger.debug(TAG, "statusText=" + response.statusText); // always empty?? logger.debug(TAG, "headers=" + JSON.stringify(response.headers)); logger.debug(TAG, "data=" + JSON.stringify(response.data)); logger.debug(TAG, "--------------------------------------"); if (isSuccess(response)) { if (isResultSuccess(response.data)) { resolve(response.data.data); } else { const e: Error = { name: `${response.data.code}`, message: `${response.data.msg}` } reject(e); } } else { const e: Error = { name: `${response.status}`, message: `${response.statusText}` } reject(e); } }) .catch((reason: AxiosError) => { logger.error(TAG, JSON.stringify(reason)); reject(reason) }) });}function isSuccess(response: AxiosResponse): boolean { return response.status >= 200 && response.status < 300}function isResultSuccess(result: ResponseResult): boolean { return result.code == 0}","link":"/blog/2024/04/26/%E9%B8%BF%E8%92%99-%E5%9F%BA%E4%BA%8Eaxios%E5%92%8CPromise%E7%9A%84%E7%BD%91%E7%BB%9C%E6%A1%86%E6%9E%B6%E5%B0%81%E8%A3%85/"},{"title":"基于ImageKnife库的图片加载框架封装","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11)ImageKnife 版本:"@ohos/imageknife": "3.0.0-rc.0" 基于 ImageKnife 库的图片加载框架封装 显示本地图片 显示网络图片 支持图片圆角 圆形头像和设置边框 支持 SVG,Gif 格式(框架自动支持) 自定义大小缩放和样式填充 关键代码 ImageLoader.ets封装如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465/* * Copyright (c) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { ImageKnifeComponent } from '@ohos/imageknife';export interface ImageOption { // 必须项 // 主图资源 loadSrc: string | PixelMap | Resource; width: number; height: number; // 可选项 // 占位图 placeholderSrc?: PixelMap | Resource; // 继承Image的能力,支持option传入objectFit设置图片缩放, // 大图样式:objectFit为Contain时根据图片自适应高度 // 项目默认:objectFit为Cover时根据Image的容器大小缩放后居中裁剪 objectFit?: ImageFit // 继承Image的能力,支持option传入border,设置边框,圆角 border?: BorderOptions // priority? : taskpool.Priority = taskpool.Priority.LOW // // context?: common.UIAbilityContext; customGetImage?: (context: Context, src: string | PixelMap | Resource) => Promise<ArrayBuffer | undefined>; progressListener?: (progress: number) => void;}@Componentexport struct ImageView { option?: ImageOption build() { ImageKnifeComponent({ ImageKnifeOption: { loadSrc: this.option?.loadSrc, placeholderSrc: this.option?.placeholderSrc, objectFit: this.option?.objectFit ?? ImageFit.Cover, border: this.option?.border, customGetImage: this.option?.customGetImage, progressListener: this.option?.progressListener, }, adaptiveWidth: this.option?.width, adaptiveHeight: this.option?.height, }) .width(this.option?.width) .height(this.option?.height) }} 使用 Demo 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157import { ImageView } from '@ohos/tool';const url = 'https://www.openharmony.cn/_nuxt/img/logo.dcf95b3.png'const url2 = 'https://file.atomgit.com/uploads/user/1704857786989_8994.jpeg' // 642*642@Entry@Componentstruct ImageTest { build() { Scroll() { Column() { Text("显示本地图片") .fontSize(24) .fontWeight(FontWeight.Bold) ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 100, height: 100, } }) .backgroundColor(Color.Black) .width(100).height(100).margin(20) Text("显示网络图片") .fontSize(24) .fontWeight(FontWeight.Bold) ImageView({ option: { loadSrc: url2, width: 100, height: 100, placeholderSrc: $r("app.media.app_icon"), } }).width(100).height(100).margin(20) Text("图片圆角") .fontSize(24) .fontWeight(FontWeight.Bold) Row() { ImageView({ option: { loadSrc: url2, width: 100, height: 100, placeholderSrc: $r("app.media.app_icon"), border: { radius: 8 } } }).width(100).height(100).margin(20) ImageView({ option: { loadSrc: url2, width: 100, height: 100, placeholderSrc: $r("app.media.app_icon"), border: { radius: { topLeft: 8, topRight: 8, }, }, } }).width(100).height(100).margin(20) } Text("圆形头像和描边效果") .fontSize(24) .fontWeight(FontWeight.Bold) ImageView({ option: { loadSrc: url2, width: 100, height: 100, placeholderSrc: $r("app.media.app_icon"), border: { radius: 50, width: 2, color: Color.Green, style: BorderStyle.Solid }, } }).width(100).height(100).margin(20) Text("自定义大小和填充样式:\\n项目默认:ImageFit.Cover\\n大图样式:ImageFit.Contain") .fontSize(24) .fontWeight(FontWeight.Bold) Row() { ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 100, height: 100, objectFit: ImageFit.Auto } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 100, height: 100, objectFit: ImageFit.Contain } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) } Row() { ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 100, height: 100, objectFit: ImageFit.Cover } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 100, height: 100, objectFit: ImageFit.Fill } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) } Row() { ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 50, height: 50, objectFit: ImageFit.Cover } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) ImageView({ option: { loadSrc: $r("app.media.gif1"), width: 50, height: 50, objectFit: ImageFit.Contain } }).width(100).height(100).margin(20) .backgroundColor(Color.Black) } }.width('100%') }.width('100%').height('100%').scrollBar(BarState.Off) }}","link":"/blog/2024/04/30/%E9%B8%BF%E8%92%99-%E5%9F%BA%E4%BA%8EImageKnife%E5%BA%93%E7%9A%84%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E6%A1%86%E6%9E%B6%E5%B0%81%E8%A3%85/"},{"title":"基于mmkv的Constant封装","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11)mmkv 版本:"@tencent/mmkv": "1.3.5" 基于 mmkv 的 Constant 封装全局变量,支持 KV 直接保存到手机物理存储,使用超方便。 关键代码 Constant.ets封装如下: 1234567891011121314151617181920212223242526272829303132333435363738394041/* * Copyright (c) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { MMKV } from "@tencent/mmkv";export default class Constant { private static TAG = "Constant"; /** * time millis distance between server time and local time. * * offsetTime = ${local.getTime() - server.getTime()} */ public static offsetTime: number = 0; /** * session id * * record local from server response session id. */ private static SESSION_ID = "SESSION_ID"; public static get st(): string { return MMKV.defaultMMKV().decodeString(Constant.SESSION_ID) ?? ""; } public static set st(value: string) { if (value != null) MMKV.defaultMMKV().encodeString(Constant.SESSION_ID, value); }}","link":"/blog/2024/04/26/%E9%B8%BF%E8%92%99-%E5%9F%BA%E4%BA%8Emmkv%E7%9A%84Constant%E5%B0%81%E8%A3%85/"},{"title":"鸿蒙Next开启Wifi代理遇到的坑","text":"鸿蒙 Next 开启 Wifi 代理遇到的坑郑重申明:仅记录鸿蒙系统开发版挂代理遇到的坑 手机:华为 Mate60OS 版本:HarmonyOS NEXT Developer Preview2 0x01 鸿蒙手机设置中没有安装证书入口开发过程中需要挂 charles 代理,然后需要安装证书,但是设置中没有安装证书的入口。可以通过 hdc 命令打开隐藏的证书安装应用。 1hdc shell aa start -a MainAbility -b com.ohos.certmanager 0x02 wifi 开启代理后,应用并没有走设置的代理设置代理之后,关闭手机 wifi,然后重新打开 wifi,再启动应用就好了。","link":"/blog/2024/04/26/%E9%B8%BF%E8%92%99-%E5%BC%80%E5%90%AFWifi%E4%BB%A3%E7%90%86%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91/"},{"title":"文件大小最大两位小数显示","text":"SDK 版本:HarmonyOS NEXT Developer Beta2 SDK (5.0.0.31)DevEco-Studio 版本:DevEco Studio NEXT Developer Beta2 (5.0.3.502)工程机版本:ALN-AL00 NEXT.0.0.31 文件大小最大两位小数显示FileUtil.ets 123456789101112131415161718192021222324252627282930313233343536373839404142434445/* * Copyright (c) 2024. Dench. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { intl } from "@kit.LocalizationKit";export class FileUtil { /** * 文件大小最大两位小数显示 */ getFileSizeStr(size?: number): string { const numberFormat = new intl.NumberFormat("zh-CN", { maximumFractionDigits: 2, }); if (size === undefined) { return ""; } if (size >= this.GB) { return numberFormat.format(size / this.GB) + "G"; } if (size >= this.MB) { return numberFormat.format(size / this.MB) + "M"; } if (size >= this.KB) { return numberFormat.format(size / this.KB) + "k"; } return size + "b"; } private readonly KB = 1000; private readonly MB = 1000000; private readonly GB = 1000000000;} Referencehttps://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/i18n-numbers-weights-measures-V5#%E6%95%B0%E5%AD%97%E6%A0%BC%E5%BC%8F%E5%8C%96","link":"/blog/2024/08/06/%E9%B8%BF%E8%92%99-%E6%96%87%E4%BB%B6%E5%A4%A7%E5%B0%8F%E6%9C%80%E5%A4%A7%E4%B8%A4%E4%BD%8D%E5%B0%8F%E6%95%B0%E6%98%BE%E7%A4%BA/"},{"title":"鸿蒙-日期格式化输出(ArkTS)","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2(4.1.3.700)HarmonyOS API 版本:4.1.0(11) 日期格式化输出(ArkTS)1234567891011// 时间戳转换为显示时间输出function timestampToDate(t: number): string { let date = new Date(t); const year = date.getFullYear(); const month = ("0" + (date.getMonth() + 1)).slice(-2); const day = ("0" + date.getDate()).slice(-2); const hours = ("0" + date.getHours()).slice(-2); const minutes = ("0" + date.getMinutes()).slice(-2); const seconds = ("0" + date.getSeconds()).slice(-2); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;}","link":"/blog/2024/07/09/%E9%B8%BF%E8%92%99-%E6%97%A5%E6%9C%9F%E6%A0%BC%E5%BC%8F%E5%8C%96%E8%BE%93%E5%87%BA/"},{"title":"组件之间的数据同步,@State,@Prop,@Watch装饰器","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 组件之间的数据同步,@State,@Prop,@Watch 装饰器这里是一个使用的@State,@Prop,@Watch 装饰器做组件之间的数据同步的 demo。 父组件: 12345678910111213141516171819202122@Componentstruct SearchResultPage { @State input: string = ''; @State searchWord: string = '' aboutToAppear(): void { this.syncSearchWord(this.input) } build() { Column() { SearchDramaResultList({ searchWord: this.searchWord }).layoutWeight(1) } .width('100%') .height('100%') } syncSearchWord(keyword: string) { if (keyword == '') return; this.searchWord = keyword }} 子组件: 123456789101112131415161718@Componentexport struct SearchDramaResultList { @Prop @Watch('requestData') searchWord: string; @State data?: SearchSeasonVo[] | null = null private vm = new SearchVM() // 需绑定列表或宫格组件 private scroller: Scroller = new Scroller(); aboutToAppear(): void { // request data. this.requestData() } requestData(propName: string) { console.log("requestData: searchWord=", this.searchWord, ",propName=", propName); // do data request with searchWord }}","link":"/blog/2024/05/15/%E9%B8%BF%E8%92%99-%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E4%B9%8B%E9%97%B4%E7%9A%84%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5/"},{"title":"解析<em></em>标签高亮显示","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 解析标签高亮显示由于跟后端约定,接口中对于返回的字符串中,使用标签的内容需要使用主题色高亮显示。比如 <em>权力</em>的<em>游戏</em> 第<em>七</em>季。 注意:当前版本不支持标签嵌套。 具体代码如下: 12345678910111213141516171819202122232425262728293031323334353637Text() { buildHighLightSpan(item.title)}.fontSize(17).fontColor('#FF222222').fontWeight(600).textOverflow({ overflow: TextOverflow.Ellipsis}).width('100%')/** * 解析 <em> </em>标签高亮显示 * * 注意:不支持嵌套 * * @param highLightTitle 带标签的文本 */@Builderfunction buildHighLightSpan(highLightTitle: string | undefined) { if (highLightTitle == null || highLightTitle.indexOf('<em>') == -1 || highLightTitle.indexOf('</em>') == -1) { Span(highLightTitle?.replace('<em>', '').replace('</em>', '')).fontColor('#FF222222') } else { ForEach(highLightTitle.split('</em>'), (attr: string) => { ForEach(attr.split('<em>'), (item: string, index: number) => { if (item != null || item != '') { if (index == 0) { Span(item).fontColor('#FF222222'); } else { Span(item).fontColor('#FF00A3FF'); } } }); }) }}","link":"/blog/2024/05/15/%E9%B8%BF%E8%92%99-%E8%A7%A3%E6%9E%90%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E6%8C%87%E5%AE%9A%E6%A0%87%E7%AD%BE%E7%9A%84%E5%86%85%E5%AE%B9%E9%AB%98%E4%BA%AE%E6%98%BE%E7%A4%BA/"},{"title":"获取鸿蒙手机屏幕的宽高","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 获取鸿蒙手机屏幕的宽高DeviceUtil.ets: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960/* * Copyright (c) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */import { display } from "@kit.ArkUI";export class DeviceUtil { private static width = 0; private static widthPx = 0; private static height = 0; private static heightPx = 0; public static getDisplayWidth(): number { if (DeviceUtil.width == 0) { DeviceUtil.setupDisplaySize(); } return DeviceUtil.width; } public static getDisplayWidthPx(): number { if (DeviceUtil.widthPx == 0) { DeviceUtil.setupDisplaySize(); } return DeviceUtil.widthPx; } public static getDisplayHeight(): number { if (DeviceUtil.height == 0) { DeviceUtil.setupDisplaySize(); } return DeviceUtil.height; } public static getDisplayHeightPx(): number { if (DeviceUtil.heightPx == 0) { DeviceUtil.setupDisplaySize(); } return DeviceUtil.heightPx; } private static setupDisplaySize() { let width = display.getDefaultDisplaySync().width; let height = display.getDefaultDisplaySync().height; DeviceUtil.widthPx = width; DeviceUtil.width = px2vp(width); DeviceUtil.heightPx = height; DeviceUtil.height = px2vp(height); }} 参考https://segmentfault.com/q/1010000044715976","link":"/blog/2024/05/09/%E9%B8%BF%E8%92%99-%E8%8E%B7%E5%8F%96%E5%B1%8F%E5%B9%95%E7%9A%84%E5%AE%BD%E9%AB%98/"},{"title":"超好用的字符串拼接类StringBuilder","text":"DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 字符串拼接类 StringBuilder类似 Java 的 StringBuilder,拼接多个字符串。 支持空字符串过滤 支持多个字符串中间使用指定的字符拼接 StringBuilder.ets代码如下: 1234567891011121314151617181920212223242526272829303132333435363738/* * Copyright (c) 2024 @Dench. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */export class StringBuilder { private value: string[]; constructor() { this.value = []; } append(str: string | number | undefined | null): StringBuilder { this.value.push(String(str)); return this; } appendNotNull(str: string | number | undefined | null): StringBuilder { if (str != null) { this.value.push(String(str)); } return this; } build(separator?: string): string { return this.value.join(separator); }} 使用 Demo 12345678let res = new StringBuilder() .append("123") .append("aaa") .append("456") .append("bbb") .append("789") .build(".");console.info(res);","link":"/blog/2024/05/15/%E9%B8%BF%E8%92%99-%E8%B6%85%E5%A5%BD%E7%94%A8%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8B%BC%E6%8E%A5%E7%B1%BBStringBuilder/"},{"title":"鸿蒙Problems:路由导航","text":"鸿蒙 Problems:路由导航DevEco Studio 版本:DevEco Studio NEXT Developer Preview2HarmonyOS API 版本:4.1.0(11) 0x01 The named route is not exist.问题描述在想要跳转到的 Har 或者 Hsp 子模块的页面(hara 模块的 Index 页面),已经使用 @Entry({ routeName: "hara_index_page" }) 给 Index 页面自定义命名,使用 1234router.pushNamedRoute({ name: "hara_index_page", params: { data: "Hello World!" },}); 进行跳转,也对 hara 模块进行了依赖。然以执行跳转的时候,还是报了 The named route is not exist.异常。 解决方案查看文档发现,还需要在配置成功后,手动在跳转的页面中 import 被跳转页面: 1import("@ohos/hara/src/main/ets/pages/Index"); // 引入共享包中的命名路由页面 0x02 A page configured in ‘main_pages.json’ must have one and only one ‘@Entry’ decorator.问题描述在main_pages.json文件中申明的 Page 页面,必须有且只有一个@Entry的装饰器。 但是我检查了项目中所有main_pages.json文件配置的 Page 页面都满足要求。然后怎么清除缓存重新安装都没有用。 后来,发现项目是分层+模块化架构,其中一个 har 模块没有配置 main 入口。(一般使用 DevEco Studio 直接创建 har 模块不会有这个问题) 解决方案在模块的oh-package.json5文件中配置main入口如下: 123456789{ "name": "hara", "version": "1.0.0", "description": "Please describe the basic information.", "main": "Index.ets", "author": "", "license": "Apache-2.0", "dependencies": {}}","link":"/blog/2024/05/06/%E9%B8%BF%E8%92%99Problems-%E8%B7%AF%E7%94%B1%E5%AF%BC%E8%88%AA/"},{"title":"为什么选择Netty","text":"为什么选择NettyNetty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他如Strom还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。 通过对Netty的分析,我们将它的优点总结如下。 ◎ API使用简单,开发门槛低; ◎ 功能强大,预置了多种编解码功能,支持多种主流协议; ◎ 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展; ◎ 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优; ◎ 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼; ◎ 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入; ◎ 经历了大规模的商业应用考验,质量得到验证。Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。 正是因为这些优点,Netty逐渐成为了Java NIO编程的首选框架。 Netty 是什么?那Netty到底是什么?官方解释:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。Netty就是一个对Jdk的Nio进行封装的一个框架。 Netty is an asynchronous event-driven network application frameworkfor rapid development of maintainable high performance protocol servers & clients. https://netty.io/ 在开始了解 Netty 是什么之前,我们先来回顾一下,如果我们需要实现一个客户端与服务端通信的程序,使用传统的 IO 编程,应该如何来实现? IO编程我们简化下场景:客户端每隔两秒发送一个带有时间戳的 “hello world” 给服务端,服务端收到之后打印。 为了方便演示,下面例子中,服务端和客户端各一个类,把这两个类拷贝到你的 IDE 中,先后运行 IOServer.java 和IOClient.java可看到效果。 下面是传统的 IO 编程中服务端实现 IOServer.java 123456789101112131415161718192021222324252627282930313233343536/** * @author 闪电侠 */public class IOServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8000); // (1) 接收新连接线程 new Thread(() -> { while (true) { try { // (1) 阻塞方法获取新的连接 Socket socket = serverSocket.accept(); // (2) 每一个新的连接都创建一个线程,负责读取数据 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // (3) 按字节流方式读取数据 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } }).start(); } catch (IOException e) { } } }).start(); }} Server 端首先创建了一个serverSocket来监听 8000 端口,然后创建一个线程,线程里面不断调用阻塞方法 serversocket.accept();获取新的连接,见(1),当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,见(2),然后读取数据是以字节流的方式,见(3)。 下面是传统的IO编程中客户端实现 IOClient.java 123456789101112131415161718192021/** * @author 闪电侠 */public class IOClient { public static void main(String[] args) { new Thread(() -> { try { Socket socket = new Socket("127.0.0.1", 8000); while (true) { try { socket.getOutputStream().write((new Date() + ": hello world").getBytes()); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } }).start(); }} 客户端的代码相对简单,连接上服务端 8000 端口之后,每隔 2 秒,我们向服务端写一个带有时间戳的 “hello world”。 IO 编程模型在客户端较少的情况下运行良好,但是对于客户端比较多的业务来说,单机服务端可能需要支撑成千上万的连接,IO 模型可能就不太合适了,我们来分析一下原因。 上面的 demo,从服务端代码中我们可以看到,在传统的 IO 模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个 while 死循环,那么 1w 个连接对应 1w 个线程,继而 1w 个 while 死循环,这就带来如下几个问题: 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起 线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。 除了以上两个问题,IO 编程中,我们看到数据读写是以字节流为单位。 为了解决这三个问题,JDK 在 1.4 之后提出了 NIO。 NIO 编程关于 NIO 相关的文章网上也有很多,这里不打算详细深入分析,下面简单描述一下 NIO 是如何解决以上三个问题的。 线程资源受限NIO 编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责,那么他是怎么做到的?我们用一幅图来对比一下 IO 与 NIO 如上图所示,IO 模型中,一个连接来了,会创建一个线程,对应一个 while 死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w 个连接里面同一时刻只有少量的连接有数据可读,因此,很多个 while 死循环都白白浪费掉了,因为读不出啥数据。 而在 NIO 模型中,他把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的呢? 这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 IO 与 NIO 的区别。 在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有 100 个小朋友,有两种方案可以解决小朋友上厕所的问题: 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。 这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少 线程切换效率低下由于 NIO 模型中线程数量大大降低,线程切换效率因此也大幅度提高 IO读写面向流IO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。 而 NIO 的读写是面向 Buffer 的,你可以随意读取里面任何一个字节数据,不需要你自己缓存数据,这一切只需要移动读写指针即可。 简单讲完了 JDK NIO 的解决方案之后,我们接下来使用 NIO 的方案替换掉 IO 的方案,我们先来看看,如果用 JDK 原生的 NIO 来实现服务端,该怎么做 前方高能预警:以下代码可能会让你感觉极度不适,如有不适,请跳过 NIOServer.java 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081/** * @author 闪电侠 */public class NIOServer { public static void main(String[] args) throws IOException { Selector serverSelector = Selector.open(); Selector clientSelector = Selector.open(); new Thread(() -> { try { // 对应IO编程中服务端启动 ServerSocketChannel listenerChannel = ServerSocketChannel.open(); listenerChannel.socket().bind(new InetSocketAddress(8000)); listenerChannel.configureBlocking(false); listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); while (true) { // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms if (serverSelector.select(1) > 0) { Set<SelectionKey> set = serverSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { try { // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); clientChannel.register(clientSelector, SelectionKey.OP_READ); } finally { keyIterator.remove(); } } } } } } catch (IOException ignored) { } }).start(); new Thread(() -> { try { while (true) { // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms if (clientSelector.select(1) > 0) { Set<SelectionKey> set = clientSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isReadable()) { try { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // (3) 面向 Buffer clientChannel.read(byteBuffer); byteBuffer.flip(); System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer) .toString()); } finally { keyIterator.remove(); key.interestOps(SelectionKey.OP_READ); } } } } } } catch (IOException ignored) { } }).start(); }} 相信大部分没有接触过 NIO 的同学应该会直接跳过代码来到这一行:原来使用 JDK 原生 NIO 的 API 实现一个简单的服务端通信程序是如此复杂! 复杂得我都没耐心解释这一坨代码的执行逻辑(开个玩笑),我们还是先对照 NIO 来解释一下几个核心思路 NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在我们这个例子中serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等,参见(1) clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而批量处理,参见(2) 数据的读写面向 Buffer,参见(3) 其他的细节部分,我不愿意多讲,因为实在是太复杂,你也不用对代码的细节深究到底。总之,强烈不建议直接基于JDK原生NIO来进行网络开发,下面是我总结的原因 JDK 的 NIO 编程需要了解很多的概念,编程复杂,对 NIO 入门非常不友好,编程模型不友好,ByteBuffer 的 Api 简直反人类 对 NIO 编程来说,一个比较合适的线程模型能充分发挥它的优势,而 JDK 没有给你实现,你需要自己实现,就连简单的自定义协议拆包都要你自己实现 JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100% 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug 正因为如此,我客户端代码都懒得写给你看了==!,你可以直接使用IOClient.java与NIOServer.java通信 JDK 的 NIO 犹如带刺的玫瑰,虽然美好,让人向往,但是使用不当会让你抓耳挠腮,痛不欲生,正因为如此,Netty 横空出世! Netty编程那么 Netty 到底是何方神圣? 用一句简单的话来说就是:Netty 封装了 JDK 的 NIO,让你用得更爽,你不用再写一大堆复杂的代码了。 用官方正式的话来说就是:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。 下面是我总结的使用 Netty 不使用 JDK 原生 NIO 的原因 使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞 Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型 Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑 Netty 解决了 JDK 的很多包括空轮询在内的 Bug Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手 Netty 社区活跃,遇到问题随时邮件列表或者 issue Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大 看不懂没有关系,这些我们在后续的课程中我们都可以学到,接下来我们用 Netty 的版本来重新实现一下本文开篇的功能吧 首先,引入 Maven 依赖,本文后续 Netty 都是基于 4.1.6.Final 版本 12345<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.6.Final</version></dependency> 然后,下面是服务端实现部分 NettyServer.java 1234567891011121314151617181920212223242526/** * @author 闪电侠 */public class NettyServer { public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); serverBootstrap .group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { protected void initChannel(NioSocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(msg); } }); } }) .bind(8000); }} 这么一小段代码就实现了我们前面 NIO 编程中的所有的功能,包括服务端启动,接受新连接,打印客户端传来的数据,怎么样,是不是比 JDK 原生的 NIO 编程优雅许多? 初学 Netty 的时候,由于大部分人对 NIO 编程缺乏经验,因此,将 Netty 里面的概念与 IO 模型结合起来可能更好理解 boss 对应 IOServer.java 中的接受新连接线程,主要负责创建新连接 worker 对应 IOServer.java 中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理 然后剩下的逻辑我在后面的系列文章中会详细分析,你可以先把这段代码拷贝到你的 IDE 里面,然后运行 main 函数 然后下面是客户端 NIO 的实现部分 NettyClient.java 12345678910111213141516171819202122232425/** * @author 闪电侠 */public class NettyClient { public static void main(String[] args) throws InterruptedException { Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup group = new NioEventLoopGroup(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) { ch.pipeline().addLast(new StringEncoder()); } }); Channel channel = bootstrap.connect("127.0.0.1", 8000).channel(); while (true) { channel.writeAndFlush(new Date() + ": hello world!"); Thread.sleep(2000); } }} 在客户端程序中,group对应了我们IOClient.java中 main 函数起的线程,剩下的逻辑我在后面的文章中会详细分析,现在你要做的事情就是把这段代码拷贝到你的 IDE 里面,然后运行 main 函数,最后回到 NettyServer.java 的控制台,你会看到效果。 使用 Netty 之后是不是觉得整个世界都美好了,一方面 Netty 对 NIO 封装得如此完美,写出来的代码非常优雅,另外一方面,使用 Netty 之后,网络通信这块的性能问题几乎不用操心,尽情地让 Netty 榨干你的 CPU 吧。 资料:1、选择Netty作为基础通信框架 :https://www.cnblogs.com/mxyhws/p/5500425.html 2、Nginx/Netty/ZeroMQ网络模型: https://blog.csdn.net/kobejayandy/article/details/20294909","link":"/blog/2020/12/29/%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9Netty/"},{"title":"Problems专题:ViewPager2","text":"ViewPager20x01 FragmentManager is already executing transactions1234567891011121314151617181920212223242526272829303132333435363738394041424344454647java.lang.IllegalStateException: FragmentManager is already executing transactions at androidx.fragment.app.FragmentManager.ensureExecReady(FragmentManager.java:1778) at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1814) at androidx.fragment.app.BackStackRecord.commitNow(BackStackRecord.java:297) at androidx.viewpager2.adapter.FragmentStateAdapter.removeFragment(FragmentStateAdapter.java:464) at androidx.viewpager2.adapter.FragmentStateAdapter.gcFragments(FragmentStateAdapter.java:228) at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:569) at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350) at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829) at android.view.View.restoreHierarchyState(View.java:18613) at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:573) at androidx.fragment.app.FragmentStateManager.restoreViewState(FragmentStateManager.java:356) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1189) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356) at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1434) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1497) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2625) at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2577) at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:247) at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:541) at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210) at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1392) at android.app.Activity.performStart(Activity.java:7260) at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3009) at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180) at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1840) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:207) at android.app.ActivityThread.main(ActivityThread.java:6878) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876) 解决方案 如果在 Fragment 中使用 ViewPager2,那么 FragmentStateAdapter 应该使用 childFragmentManager。将 1FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getActivity().getSupportFragmentManager(), titles); 改为 1FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getChildFragmentManager(), titles); 0x02 ViewPager2+FragmentStateAdapter 的 notifyDataSetChanged 方法失效原因分析: 因为 FragmentStateAdapter 会保存所有 Fragment 实例,当调用 Adapter.notifyDataSetChanged() 方法时,Fragment 并没有走 onCreate 方法。 解决方案: 方案一(这个方法会导致内存泄漏,不推荐) 在调用 notifyDataSetChanged 之前,清空 FragmentStateAdapter 的 Fragment 列表。 方案二 重写 getItemId() containsItem() 这两个方法,并确保 getItemId() 的值是唯一的。 1234567891011121314151617181920212223242526272829303132override fun createViewPagerAdapter(): RecyclerView.Adapter<*> { val items = items // avoids resolving the ViewModel multiple times return object : FragmentStateAdapter(this) { override fun createFragment(position: Int): PageFragment { val itemId = items.itemId(position) val itemText = items.getItemById(itemId) return PageFragment.create(itemText) } override fun getItemCount(): Int = items.size override fun getItemId(position: Int): Long = items.itemId(position) override fun containsItem(itemId: Long): Boolean = items.contains(itemId) }}/** A very simple collection of items. Optimized for simplicity (i.e. not performance). */class ItemsViewModel : ViewModel() { private var nextValue = 1L private val items = (1..9).map { longToItem(nextValue++) }.toMutableList() fun getItemById(id: Long): String = items.first { itemToLong(it) == id } fun itemId(position: Int): Long = itemToLong(items[position]) fun contains(itemId: Long): Boolean = items.any { itemToLong(it) == itemId } fun addNewAt(position: Int) = items.add(position, longToItem(nextValue++)) fun removeAt(position: Int) = items.removeAt(position) fun createIdSnapshot(): List<Long> = (0 until size).map { position -> itemId(position) } val size: Int get() = items.size private fun longToItem(value: Long): String = "item#$value" private fun itemToLong(value: String): Long = value.split("#")[1].toLong()} 0x03 Design assumption violated123456java.lang.IllegalStateException: Design assumption violated. at androidx.viewpager2.widget.ViewPager2.updateCurrentItem(ViewPager2.java:538) at androidx.viewpager2.widget.ViewPager2$4.onAnimationsFinished(ViewPager2.java:518) at androidx.recyclerview.widget.RecyclerView$ItemAnimator.isRunning(RecyclerView.java:13244) at androidx.viewpager2.widget.ViewPager2.onLayout(ViewPager2.java:515) at android.view.View.layout(View.java:15596) 解决方案: 如果重写了 getItemId() containsItem() 这两个方法,确保 getItemId() 的值是唯一的。代码同 0x02 0x04 ViewPager2 嵌套 RecyclerView 手势冲突问题 原因分析: 同方向滚动事件被 ViewPager2 拦截了。 解决方案: 方案一 自定义 NestedScrollableHost采用官方提供的自定义 NestedScrollableHost 来包一层 RecyclerView 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394import android.content.Contextimport android.util.AttributeSetimport android.view.MotionEventimport android.view.Viewimport android.view.ViewConfigurationimport android.widget.FrameLayoutimport androidx.viewpager2.widget.ViewPager2import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTALimport kotlin.math.absoluteValueimport kotlin.math.sign/** * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. * * This solution has limitations when using multiple levels of nested scrollable elements * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). */class NestedScrollableHost : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) private var touchSlop = 0 private var initialX = 0f private var initialY = 0f private val parentViewPager: ViewPager2? get() { var v: View? = parent as? View while (v != null && v !is ViewPager2) { v = v.parent as? View } return v as? ViewPager2 } private val child: View? get() = if (childCount > 0) getChildAt(0) else null init { touchSlop = ViewConfiguration.get(context).scaledTouchSlop } private fun canChildScroll(orientation: Int, delta: Float): Boolean { val direction = -delta.sign.toInt() return when (orientation) { 0 -> child?.canScrollHorizontally(direction) ?: false 1 -> child?.canScrollVertically(direction) ?: false else -> throw IllegalArgumentException() } } override fun onInterceptTouchEvent(e: MotionEvent): Boolean { handleInterceptTouchEvent(e) return super.onInterceptTouchEvent(e) } private fun handleInterceptTouchEvent(e: MotionEvent) { val orientation = parentViewPager?.orientation ?: return // Early return if child can't scroll in same direction as parent if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { return } if (e.action == MotionEvent.ACTION_DOWN) { initialX = e.x initialY = e.y parent.requestDisallowInterceptTouchEvent(true) } else if (e.action == MotionEvent.ACTION_MOVE) { val dx = e.x - initialX val dy = e.y - initialY val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL // assuming ViewPager2 touch-slop is 2x touch-slop of child val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f if (scaledDx > touchSlop || scaledDy > touchSlop) { if (isVpHorizontal == (scaledDy > scaledDx)) { // Gesture is perpendicular, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } else { // Gesture is parallel, query child if movement in that direction is possible if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { // Child can scroll, disallow all parents to intercept parent.requestDisallowInterceptTouchEvent(true) } else { // Child cannot scroll, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } } } } }} 对应的 layout 代码: 123456789101112131415161718192021222324252627282930313233<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical">... 水平 <androidx.viewpager2.integration.testapp.NestedScrollableHost android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/first_rv" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#FFFFFF" /> </androidx.viewpager2.integration.testapp.NestedScrollableHost> ... 竖直 <androidx.viewpager2.integration.testapp.NestedScrollableHost android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="8dp" android:layout_weight="1"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/second_rv" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" /> </androidx.viewpager2.integration.testapp.NestedScrollableHost></LinearLayout> 方案二 自定义 RecyclerView自定义 NestedRecyclerView 的分发事件通过 requestDisallowInterceptTouchEvent() 方法来限制父布类的拦截事件 1234567891011121314151617181920212223242526272829303132333435363738394041public class NestedRecyclerView extends RecyclerView { public NestedRecyclerView(@NonNull Context context) { super(context); } public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private int startX, startY; @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startX = (int) ev.getX(); startY = (int) ev.getY(); getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int endX = (int) ev.getX(); int endY = (int) ev.getY(); int disX = Math.abs(endX - startX); int disY = Math.abs(endY - startY); if (disX > disY) { getParent().requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX)); } else { getParent().requestDisallowInterceptTouchEvent(canScrollVertically(startX - endX)); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: getParent().requestDisallowInterceptTouchEvent(false); break; } return super.dispatchTouchEvent(ev); }} 方法三 使用 ViewPager降级,使用 ViewPager 来嵌套 RecyclerView ,可以避免事件冲突,亲测有效。 0x05 ViewPager2 两边保留上一页预览1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import android.os.Bundleimport android.view.LayoutInflaterimport android.view.ViewGroupimport android.widget.Toastimport androidx.fragment.app.FragmentActivityimport androidx.recyclerview.widget.RecyclerViewimport androidx.viewpager2.widget.ViewPager2class PreviewPagesActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_viewpager2) findViewById<ViewPager2>(R.id.view_pager).apply { // Set offscreen page limit to at least 1, so adjacent pages are always laid out offscreenPageLimit = 1 val recyclerView = getChildAt(0) as RecyclerView recyclerView.apply { clipToPadding = false val leftPadding = resources.getDimensionPixelOffset(R.dimen.halfPageMargin) + resources.getDimensionPixelOffset(R.dimen.peekOffset) // setting padding on inner RecyclerView puts overscroll effect in the right place // TODO: expose in later versions not to rely on // getChildAt(0) which might break setPadding(leftPadding, 0, leftPadding, 0) } adapter = Adapter() } } class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_preview_pages, parent, false) ) class Adapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun getItemCount(): Int { return 10 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ViewHolder(parent) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { holder.itemView.tag = position holder.itemView.setOnClickListener { Toast.makeText(it.context, "position=$position", Toast.LENGTH_LONG).show() } } }} 0x06 Fragment no longer exists for key f1在 Fragment 中使用 ViewPager 的时候,切换 Fragment 导致 ViewPager 无法正确恢复异常 123456789101112131415161718192021222324java.lang.IllegalStateException: Fragment no longer exists for key f1: unique id 55efaee5-a65c-4e57-9281-7c8f8f6e4156 at androidx.fragment.app.FragmentManager.getFragment(FragmentManager.java:960) at androidx.fragment.app.FragmentStatePagerAdapter.restoreState(FragmentStatePagerAdapter.java:328) at androidx.viewpager.widget.ViewPager.onRestoreInstanceState(ViewPager.java:1461) at android.view.View.dispatchRestoreInstanceState(View.java:20032) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3922) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3928) at android.view.View.restoreHierarchyState(View.java:20010) at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:639) at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:3010) at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3001) at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:580) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:285) at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189) at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100) at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002) at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:230) at android.app.ActivityThread.main(ActivityThread.java:8018) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:526) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034) 在这个页面中,内容列表使用 ViewPager 嵌套 Fragment 实现,并和时间选择 Tab 绑定。切换【即将上线】和【播出时间表】Tab,实际是使用 FragmentManager 的 replace 方法,动态切换两个 Fragment,然后就报了上面的异常。 网上流行的解决方案是使用 FragmentPagerAdapter 或者添加 1234@Overridepublic Parcelable saveState() { return null;} 但是这样处理会导致 ViewPager 中的 fragments 全部无法恢复,导致 ViewPager 白屏。 本例中的解决方案是: 在切换【即将上线】和【播出时间表】Tab 时,不使用 FragmentManager 的 replace 方法,而采用动态的 Hide 和 show 方法暂时规避 Fragment 被回收的问题。","link":"/blog/2021/06/20/Problems%E4%B8%93%E9%A2%98%E4%B9%8BViewPager2/"},{"title":"SpannableString 之显示查看全文","text":"SpannableString 之显示查看全文显示内容超出规定的行数之后,显示 展开 和 收起 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629import android.content.Context;import android.graphics.Color;import android.os.Build;import android.text.Layout;import android.text.SpannableString;import android.text.SpannableStringBuilder;import android.text.Spanned;import android.text.StaticLayout;import android.text.TextPaint;import android.text.TextUtils;import android.text.style.AlignmentSpan;import android.text.style.ClickableSpan;import android.text.style.StyleSpan;import android.util.AttributeSet;import android.view.View;import android.view.animation.Animation;import android.view.animation.Transformation;import androidx.annotation.ColorInt;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import androidx.appcompat.widget.AppCompatTextView;import java.lang.reflect.Field;/** * Description : 显示展开和收起 * PackageName : com.mrtrying.widget * Created by mrtrying on 2019/4/17 17:21. * e_mail : [email protected] */public class ExpandableTextView extends AppCompatTextView { private static final String TAG = ExpandableTextView.class.getSimpleName(); public static final String ELLIPSIS_STRING = new String(new char[]{'\\u2026'}); private static final String DEFAULT_OPEN_SUFFIX = " 展开"; private static final String DEFAULT_CLOSE_SUFFIX = " 收起"; volatile boolean animating = false; boolean isClosed = false; private int mMaxLines = getMaxLines(); private int initWidth = 0; private CharSequence originalText; private SpannableStringBuilder mOpenSpannableStr, mCloseSpannableStr; private boolean hasAnimation = false; private Animation mOpenAnim, mCloseAnim; private int mOpenHeight, mCLoseHeight; private boolean mExpandable; private boolean mCloseInNewLine; @Nullable private SpannableString mOpenSuffixSpan, mCloseSuffixSpan; private String mOpenSuffixStr = DEFAULT_OPEN_SUFFIX; private String mCloseSuffixStr = ""; private int mOpenSuffixColor, mCloseSuffixColor; private int mNormalColor = Color.parseColor("#FF222222"); private View.OnClickListener mOnClickListener; private CharSequenceToSpannableHandler mCharSequenceToSpannableHandler; private IOnOpenSuffixSpanListener onOpenSuffixSpanListener; public ExpandableTextView(Context context) { super(context); initialize(); } public ExpandableTextView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } public ExpandableTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(); } /** * 初始化 */ private void initialize() { mOpenSuffixColor = mCloseSuffixColor = Color.parseColor("#FF0091FF"); setMovementMethod(CustomLinkMovementMethod.getInstance());// setIncludeFontPadding(false); updateOpenSuffixSpan(); updateCloseSuffixSpan(); } @Override public boolean hasOverlappingRendering() { return false; } public void setOriginalText(CharSequence originalText) { this.originalText = originalText; mExpandable = false; mCloseSpannableStr = new SpannableStringBuilder(); final int maxLines = mMaxLines; SpannableStringBuilder tempText = charSequenceToSpannable(originalText); mOpenSpannableStr = charSequenceToSpannable(originalText); if (maxLines != -1) { Layout layout = createStaticLayout(tempText); mExpandable = layout.getLineCount() > maxLines; if (mExpandable) { //拼接展开内容 if (mCloseInNewLine) { mOpenSpannableStr.append("\\n"); } if (mCloseSuffixSpan != null) { mOpenSpannableStr.append(mCloseSuffixSpan); } //计算原文截取位置 int endPos = layout.getLineEnd(maxLines - 1); if (originalText.length() <= endPos) { mCloseSpannableStr = charSequenceToSpannable(originalText); } else { mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos)); } SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { tempText2.append(mOpenSuffixSpan); } //循环判断,收起内容添加展开后缀后的内容 Layout tempLayout = createStaticLayout(tempText2); while (tempLayout.getLineCount() > maxLines) { int lastSpace = mCloseSpannableStr.length() - 1; if (lastSpace == -1) { break; } if (originalText.length() <= lastSpace) { mCloseSpannableStr = charSequenceToSpannable(originalText); } else { mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace)); } tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { tempText2.append(mOpenSuffixSpan); } tempLayout = createStaticLayout(tempText2); } int lastSpace = mCloseSpannableStr.length();// - mOpenSuffixSpan.length();// if(lastSpace >= 0 && originalText.length() > lastSpace){// CharSequence redundantChar = originalText.subSequence(lastSpace, lastSpace + mOpenSuffixSpan.length());// int offset = hasEnCharCount(redundantChar) - hasEnCharCount(mOpenSuffixSpan) + 1;// lastSpace = offset <= 0 ? lastSpace : lastSpace - offset;// mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));// } //计算收起的文本高度 mCLoseHeight = tempLayout.getHeight() + getPaddingTop() + getPaddingBottom();// mCloseSpannableStr.setSpan(new ClickableSpan() {// @Override// public void onClick(@NonNull View widget) {// if (mOnClickListener != null) {// mOnClickListener.onClick(widget);// }// }//// @Override// public void updateDrawState(@NonNull TextPaint ds) {// super.updateDrawState(ds);// ds.setColor(mNormalColor);// ds.setUnderlineText(false);// }// }, 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE); } } isClosed = mExpandable; if (mExpandable) {// mCloseSpannableStr.setSpan(new ClickableSpan() {// @Override// public void onClick(@NonNull View widget) {// if(listener!=null){// listener.onClick(widget);// }// }//// @Override// public void updateDrawState(@NonNull TextPaint ds) {// super.updateDrawState(ds);// ds.setColor(mNormalColor);// ds.setUnderlineText(false);// }// }, 0, mCloseSpannableStr.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mCloseSpannableStr.append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { mCloseSpannableStr.append(mOpenSuffixSpan); } setMovementMethod(CustomLinkMovementMethod.getInstance()); setClickable(false); setLongClickable(false); setText(mCloseSpannableStr); } else {// mOpenSpannableStr.setSpan(new ClickableSpan() {// @Override// public void onClick(@NonNull View widget) {// if (mOnClickListener != null) {// mOnClickListener.onClick(widget);// }// }//// @Override// public void updateDrawState(@NonNull TextPaint ds) {// super.updateDrawState(ds);// ds.setColor(mNormalColor);// ds.setUnderlineText(false);// }// }, 0, mOpenSpannableStr.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); setText(mOpenSpannableStr); } } private int hasEnCharCount(CharSequence str) { int count = 0; if (!TextUtils.isEmpty(str)) { for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c >= ' ' && c <= '~') { count++; } } } return count; } private void switchOpenClose() { if (mExpandable) { isClosed = !isClosed; if (isClosed) { close(); } else { open(); } } } /** * 设置是否有动画 * * @param hasAnimation */ public void setHasAnimation(boolean hasAnimation) { this.hasAnimation = hasAnimation; } /** * 展开 */ private void open() { if (hasAnimation) { Layout layout = createStaticLayout(mOpenSpannableStr); mOpenHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom(); executeOpenAnim(); } else { ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE); setText(mOpenSpannableStr); if (mOpenCloseCallback != null) { mOpenCloseCallback.onOpen(); } } } /** * 收起 */ private void close() { if (hasAnimation) { executeCloseAnim(); } else { ExpandableTextView.super.setMaxLines(mMaxLines); setText(mCloseSpannableStr); if (mOpenCloseCallback != null) { mOpenCloseCallback.onClose(); } } } /** * 执行展开动画 */ private void executeOpenAnim() { //创建展开动画 if (mOpenAnim == null) { mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight); mOpenAnim.setFillAfter(true); mOpenAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE); setText(mOpenSpannableStr); } @Override public void onAnimationEnd(Animation animation) { // 动画结束后textview设置展开的状态 getLayoutParams().height = mOpenHeight; requestLayout(); animating = false; } @Override public void onAnimationRepeat(Animation animation) { } }); } if (animating) { return; } animating = true; clearAnimation(); // 执行动画 startAnimation(mOpenAnim); } /** * 执行收起动画 */ private void executeCloseAnim() { //创建收起动画 if (mCloseAnim == null) { mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight); mCloseAnim.setFillAfter(true); mCloseAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { animating = false; ExpandableTextView.super.setMaxLines(mMaxLines); setText(mCloseSpannableStr); getLayoutParams().height = mCLoseHeight; requestLayout(); } @Override public void onAnimationRepeat(Animation animation) { } }); } if (animating) { return; } animating = true; clearAnimation(); // 执行动画 startAnimation(mCloseAnim); } /** * @param spannable * @return */ private Layout createStaticLayout(SpannableStringBuilder spannable) { int contentWidth = initWidth - getPaddingLeft() - getPaddingRight(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { StaticLayout.Builder builder = StaticLayout.Builder.obtain(spannable, 0, spannable.length(), getPaint(), contentWidth); builder.setAlignment(Layout.Alignment.ALIGN_NORMAL); builder.setIncludePad(getIncludeFontPadding()); builder.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()); return builder.build(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), getIncludeFontPadding()); } else { return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL, getFloatField("mSpacingMult", 1f), getFloatField("mSpacingAdd", 0f), getIncludeFontPadding()); } } private float getFloatField(String fieldName, float defaultValue) { float value = defaultValue; if (TextUtils.isEmpty(fieldName)) { return value; } try { // 获取该类的所有属性值域 Field[] fields = this.getClass().getDeclaredFields(); for (Field field : fields) { if (TextUtils.equals(fieldName, field.getName())) { value = field.getFloat(this); break; } } } catch (IllegalAccessException e) { e.printStackTrace(); } return value; } /** * @param charSequence * @return */ private SpannableStringBuilder charSequenceToSpannable(@NonNull CharSequence charSequence) { SpannableStringBuilder spannableStringBuilder = null; if (mCharSequenceToSpannableHandler != null) { spannableStringBuilder = mCharSequenceToSpannableHandler.charSequenceToSpannable(charSequence); } if (spannableStringBuilder == null) { spannableStringBuilder = new SpannableStringBuilder(charSequence); } return spannableStringBuilder; } /** * 初始化TextView的可展示宽度 * * @param width */ public void initWidth(int width) { initWidth = width; } @Override public void setMaxLines(int maxLines) { this.mMaxLines = maxLines; super.setMaxLines(maxLines); } /** * 设置展开后缀text * * @param openSuffix */ public void setOpenSuffix(String openSuffix, IOnOpenSuffixSpanListener onOpenSuffixSpanListener) { mOpenSuffixStr = openSuffix; this.onOpenSuffixSpanListener = onOpenSuffixSpanListener; updateOpenSuffixSpan(); } public void setNewOpenSuffix(String openSuffix, IOnOpenSuffixSpanListener onOpenSuffixSpanListener) { mOpenSuffixStr = openSuffix; this.onOpenSuffixSpanListener = onOpenSuffixSpanListener; updateNewOpenSuffixSpan(); } /** * 设置展开后缀文本颜色 * * @param openSuffixColor */ public void setOpenSuffixColor(@ColorInt int openSuffixColor) { mOpenSuffixColor = openSuffixColor; updateOpenSuffixSpan(); } /** * 设置收起后缀text * * @param closeSuffix */ public void setCloseSuffix(String closeSuffix) { mCloseSuffixStr = closeSuffix; updateCloseSuffixSpan(); } /** * 设置收起后缀文本颜色 * * @param closeSuffixColor */ public void setCloseSuffixColor(@ColorInt int closeSuffixColor) { mCloseSuffixColor = closeSuffixColor; updateCloseSuffixSpan(); } /** * 收起后缀是否另起一行 * * @param closeInNewLine */ public void setCloseInNewLine(boolean closeInNewLine) { mCloseInNewLine = closeInNewLine; updateCloseSuffixSpan(); } public interface IOnOpenSuffixSpanListener { void onClick(); } /** * 更新展开后缀Spannable */ private void updateOpenSuffixSpan() { if (TextUtils.isEmpty(mOpenSuffixStr)) { mOpenSuffixSpan = null; return; } mOpenSuffixSpan = new SpannableString(mOpenSuffixStr); mOpenSuffixSpan.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View widget) { if (onOpenSuffixSpanListener != null) { onOpenSuffixSpanListener.onClick(); } else { switchOpenClose(); } } @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); ds.setColor(mOpenSuffixColor); ds.setUnderlineText(false); } }, 0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } private void updateNewOpenSuffixSpan() { if (TextUtils.isEmpty(mOpenSuffixStr)) { mOpenSuffixSpan = null; return; } mOpenSuffixSpan = new SpannableString(mOpenSuffixStr); mOpenSuffixSpan.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View widget) { if (onOpenSuffixSpanListener != null) { onOpenSuffixSpanListener.onClick(); } else { switchOpenClose(); } } @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); ds.setColor(mOpenSuffixColor); ds.setUnderlineText(false); } }, 0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } /** * 更新收起后缀Spannable */ private void updateCloseSuffixSpan() { if (TextUtils.isEmpty(mCloseSuffixStr)) { mCloseSuffixSpan = null; return; } mCloseSuffixSpan = new SpannableString(mCloseSuffixStr); mCloseSuffixSpan.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); if (mCloseInNewLine) { AlignmentSpan alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE); mCloseSuffixSpan.setSpan(alignmentSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } mCloseSuffixSpan.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View widget) { switchOpenClose(); } @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); ds.setColor(mCloseSuffixColor); ds.setUnderlineText(false); } }, 1, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } public void setContentClickListener(View.OnClickListener onClickListener) { mOnClickListener = onClickListener; } public OpenAndCloseCallback mOpenCloseCallback; public void setOpenAndCloseCallback(OpenAndCloseCallback callback) { this.mOpenCloseCallback = callback; } public interface OpenAndCloseCallback { void onOpen(); void onClose(); } /** * 设置文本内容处理 * * @param handler */ public void setCharSequenceToSpannableHandler(CharSequenceToSpannableHandler handler) { mCharSequenceToSpannableHandler = handler; } public interface CharSequenceToSpannableHandler { @NonNull SpannableStringBuilder charSequenceToSpannable(CharSequence charSequence); } class ExpandCollapseAnimation extends Animation { private final View mTargetView;//动画执行view private final int mStartHeight;//动画执行的开始高度 private final int mEndHeight;//动画结束后的高度 ExpandCollapseAnimation(View target, int startHeight, int endHeight) { mTargetView = target; mStartHeight = startHeight; mEndHeight = endHeight; setDuration(400); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { mTargetView.setScrollY(0); //计算出每次应该显示的高度,改变执行view的高度,实现动画 mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight); mTargetView.requestLayout(); } }} 控件使用方式 123456789101112131415161718192021222324252627282930313233343536val tvContent = holder.getView<ExpandableTextView>(R.id.tv_content)tvContent.movementMethod = LinkMovementMethod.getInstance()tvContent.isClickable = falsetvContent.isLongClickable = falseval builder = SpannableStringBuilder()if (item.spoiler && spoilerEnable) builder.append(" ")if (item.talkList != null && item.talkList.size > 0) { val talk = item.talkList[0] val startIndex = builder.length builder.append("#") builder.append(talk.name) builder.append(" ") val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { talkClickListener?.invoke(item, holder.adapterPosition) } override fun updateDrawState(ds: TextPaint) { ds.color = if (talk.enable) Color.parseColor("#1890FF") else Color.parseColor("#85888F") } } builder.setSpan( clickableSpan, startIndex, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE )}builder.append(it)tvContent.initWidth(UiUtils.getScreenWidth() - UiUtils.dip2px(16 * 2 + 36 + 9))tvContent.setOpenSuffix("查看全文") { listener.invoke(item, null)}tvContent.setOriginalText(builder)","link":"/blog/2022/02/16/%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B8%83%E5%B1%80-SpannableString%E4%B9%8B%E6%98%BE%E7%A4%BA%E6%9F%A5%E7%9C%8B%E5%85%A8%E6%96%87/"}],"tags":[{"name":"Android","slug":"Android","link":"/blog/tags/Android/"},{"name":"Kotlin","slug":"Kotlin","link":"/blog/tags/Kotlin/"},{"name":"H5","slug":"H5","link":"/blog/tags/H5/"},{"name":"JsBridge","slug":"JsBridge","link":"/blog/tags/JsBridge/"},{"name":"adb","slug":"adb","link":"/blog/tags/adb/"},{"name":"FileProvider","slug":"FileProvider","link":"/blog/tags/FileProvider/"},{"name":"深色模式","slug":"深色模式","link":"/blog/tags/%E6%B7%B1%E8%89%B2%E6%A8%A1%E5%BC%8F/"},{"name":"Shortcuts","slug":"Shortcuts","link":"/blog/tags/Shortcuts/"},{"name":"ExoPlayer","slug":"ExoPlayer","link":"/blog/tags/ExoPlayer/"},{"name":"Git","slug":"Git","link":"/blog/tags/Git/"},{"name":"Gradle","slug":"Gradle","link":"/blog/tags/Gradle/"},{"name":"Gson","slug":"Gson","link":"/blog/tags/Gson/"},{"name":"Hexo","slug":"Hexo","link":"/blog/tags/Hexo/"},{"name":"NexT","slug":"NexT","link":"/blog/tags/NexT/"},{"name":"Java","slug":"Java","link":"/blog/tags/Java/"},{"name":"Problems专题","slug":"Problems专题","link":"/blog/tags/Problems%E4%B8%93%E9%A2%98/"},{"name":"RecyclerView","slug":"RecyclerView","link":"/blog/tags/RecyclerView/"},{"name":"Homebrew","slug":"Homebrew","link":"/blog/tags/Homebrew/"},{"name":"Python","slug":"Python","link":"/blog/tags/Python/"},{"name":"Shell","slug":"Shell","link":"/blog/tags/Shell/"},{"name":"Proxy","slug":"Proxy","link":"/blog/tags/Proxy/"},{"name":"Vim","slug":"Vim","link":"/blog/tags/Vim/"},{"name":"SSH","slug":"SSH","link":"/blog/tags/SSH/"},{"name":"AIGC","slug":"AIGC","link":"/blog/tags/AIGC/"},{"name":"stable diffusion","slug":"stable-diffusion","link":"/blog/tags/stable-diffusion/"},{"name":"iTerm2","slug":"iTerm2","link":"/blog/tags/iTerm2/"},{"name":"Reading","slug":"Reading","link":"/blog/tags/Reading/"},{"name":"Aria","slug":"Aria","link":"/blog/tags/Aria/"},{"name":"下载","slug":"下载","link":"/blog/tags/%E4%B8%8B%E8%BD%BD/"},{"name":"源码","slug":"源码","link":"/blog/tags/%E6%BA%90%E7%A0%81/"},{"name":"算法","slug":"算法","link":"/blog/tags/%E7%AE%97%E6%B3%95/"},{"name":"HarmonyOS","slug":"HarmonyOS","link":"/blog/tags/HarmonyOS/"},{"name":"arkts","slug":"arkts","link":"/blog/tags/arkts/"},{"name":"Hdc","slug":"Hdc","link":"/blog/tags/Hdc/"},{"name":"hdc","slug":"hdc","link":"/blog/tags/hdc/"},{"name":"ArkTS","slug":"ArkTS","link":"/blog/tags/ArkTS/"},{"name":"MD5","slug":"MD5","link":"/blog/tags/MD5/"},{"name":"Promise","slug":"Promise","link":"/blog/tags/Promise/"},{"name":"TextInput","slug":"TextInput","link":"/blog/tags/TextInput/"},{"name":"TextArea","slug":"TextArea","link":"/blog/tags/TextArea/"},{"name":"PullToRefresh","slug":"PullToRefresh","link":"/blog/tags/PullToRefresh/"},{"name":"axios","slug":"axios","link":"/blog/tags/axios/"},{"name":"ImageKnife","slug":"ImageKnife","link":"/blog/tags/ImageKnife/"},{"name":"mmkv","slug":"mmkv","link":"/blog/tags/mmkv/"},{"name":"Problems","slug":"Problems","link":"/blog/tags/Problems/"},{"name":"Netty","slug":"Netty","link":"/blog/tags/Netty/"}],"categories":[],"pages":[{"title":"Categories","text":"","link":"/blog/categories/index.html"},{"title":"404","text":"","link":"/blog/404/index.html"},{"title":"Tags","text":"","link":"/blog/tags/index.html"},{"title":"关于小站","text":"Dench Home-boy Developer Gamer Android Flutter","link":"/blog/about/index.html"}]}