diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..dc26666 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/README.md b/README.md index 128aff6..ed21396 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ dependencyResolutionManagement { ```kotlin dependencies { - implementation("com.github.YenalyLiew:CircularRevealSwitch:0.4.5") + implementation("com.github.YenalyLiew:CircularRevealSwitch:0.4.6") } ``` @@ -181,6 +181,16 @@ builder.setSwitcher(); ## 更新 +### v0.4.6 + +1. 使用 PixelCopyCompat,可使在 API 小于 26 时使用先进的 PixelCopy。虽然使用了反射调用,但整体速度还是比 `View#getDrawingCache()` 快不少 + + 图上部分为 PixelCopy 在 API 24 时的表现(平均耗时 40ms 左右),图下部分为 `View#getDrawingCache()` 的表现(平均耗时 60-70ms 左右) + + ![](./docs/img/pixelcopy_android_7.png) + +2. `takeScreenshot()` 方法实现由 `View#getDrawingCache()` 修改为 `View#drawToBitmap()`,速度快一倍以上(平均耗时 30ms 左右) + ### v0.4.5 1. 修复多 Activity 下,日间夜间切换动画无法正常播放的问题 diff --git a/README_EN.md b/README_EN.md index 1d1e7fd..7f9cdfe 100644 --- a/README_EN.md +++ b/README_EN.md @@ -44,7 +44,7 @@ Add dependency in your module's build.gradle.kts ```kotlin dependencies { - implementation("com.github.YenalyLiew:CircularRevealSwitch:0.4.5") + implementation("com.github.YenalyLiew:CircularRevealSwitch:0.4.6") } ``` @@ -164,6 +164,16 @@ builder.setSwitcher(); ## Updates +### v0.4.6 + +1. Using PixelCopyCompat allows for the use of advanced PixelCopy when the API is less than 26. Although reflection calls are used, the overall speed is still much faster than `View#getDrawingCache()`. + + The top part of the image shows the performance of PixelCopy at API 24 (average time consumption around 40ms), while the bottom part shows the performance of `View#getDrawingCache()` (average time consumption around 60-70ms). + + ![](./docs/img/pixelcopy_android_7.png) + +2. The implementation of the `takeScreenshot()` method has been changed from `View#getDrawingCache()` to `View#drawToBitmap()`, which is more than twice as fast (average time consumption around 30ms). + ### v0.4.5 1. Fixed the issue where the day-night switch animation cannot play normally under multiple Activities diff --git a/circularrevealswitch/build.gradle.kts b/circularrevealswitch/build.gradle.kts index 05f99bd..082e6fe 100644 --- a/circularrevealswitch/build.gradle.kts +++ b/circularrevealswitch/build.gradle.kts @@ -42,7 +42,7 @@ android { afterEvaluate { publishing { - val versionName = "0.4.5" + val versionName = "0.4.6" publications { create("release") { from(components["release"]) diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt index 150901a..6761ccd 100644 --- a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt @@ -22,6 +22,7 @@ import android.widget.ImageView import androidx.core.animation.addListener import androidx.core.view.children import androidx.core.view.doOnAttach +import androidx.core.view.drawToBitmap import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -178,35 +179,32 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) */ protected open fun Window.takeScreenshotCompat(): Bitmap { val root = decorView.rootView - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val thread = HandlerThread("Screenshot") + try { val bitmap = Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888) - val thread = HandlerThread("Screenshot") thread.start() var isSuccess = false - try { - val latch = CountDownLatch(1) - var time = System.currentTimeMillis() - PixelCopy.request(this, bitmap, { copyResult -> - isSuccess = copyResult == PixelCopy.SUCCESS - latch.countDown() - }, Handler(thread.looper)) - isSuccess = latch.await(1000, TimeUnit.MILLISECONDS) && isSuccess - time = System.currentTimeMillis() - time - if (isSuccess) { - if (DEBUG) { - Log.d(TAG, "take screenshot by PixelCopy, time: $time ms") - } - return bitmap + val latch = CountDownLatch(1) + var time = System.currentTimeMillis() + PixelCopyCompat.request(this, bitmap, { copyResult -> + isSuccess = copyResult == PixelCopy.SUCCESS + latch.countDown() + }, Handler(thread.looper)) + isSuccess = latch.await(1000, TimeUnit.MILLISECONDS) && isSuccess + time = System.currentTimeMillis() - time + if (isSuccess) { + if (DEBUG) { + Log.d(TAG, "take screenshot by PixelCopy, time: $time ms") } - return takeScreenshot() - } catch (e: Exception) { - e.printStackTrace() - return takeScreenshot() - } finally { - thread.quit() + return bitmap } + return takeScreenshot() + } catch (e: Exception) { + e.printStackTrace() + return takeScreenshot() + } finally { + thread.quit() } - return takeScreenshot() } /** @@ -216,13 +214,10 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) * * @return Bitmap Returns a screenshot of the window. */ - @Suppress("DEPRECATION") protected open fun Window.takeScreenshot(): Bitmap { val root = decorView.rootView var time = System.currentTimeMillis() - root.isDrawingCacheEnabled = true - val bitmap = Bitmap.createBitmap(root.drawingCache) - root.isDrawingCacheEnabled = false + val bitmap = root.drawToBitmap() time = System.currentTimeMillis() - time if (DEBUG) { Log.d(TAG, "take screenshot by default, time: $time ms") diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/PixelCopyCompat.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/PixelCopyCompat.kt new file mode 100644 index 0000000..2e991ce --- /dev/null +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/PixelCopyCompat.kt @@ -0,0 +1,102 @@ +package com.yenaly.circularrevealswitch + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Rect +import android.os.Build +import android.os.Handler +import android.view.PixelCopy +import android.view.Surface +import android.view.View +import android.view.Window +import androidx.annotation.RequiresApi + +object PixelCopyCompat { + + fun request( + window: Window, srcRect: Rect?, dest: Bitmap, + listener: PixelCopy.OnPixelCopyFinishedListener, + listenerHandler: Handler, + ) { + when { + Build.VERSION.SDK_INT >= 26 -> Api26Impl.request( + window, srcRect, dest, + listener, listenerHandler + ) + + else -> Api24Impl.request( + window, dest, listener, + listenerHandler + ) + } + } + + fun request( + window: Window, dest: Bitmap, + listener: PixelCopy.OnPixelCopyFinishedListener, + listenerHandler: Handler, + ) = request(window, null, dest, listener, listenerHandler) + + @RequiresApi(26) + internal object Api26Impl { + fun request( + window: Window, srcRect: Rect?, dest: Bitmap, + listener: PixelCopy.OnPixelCopyFinishedListener, + listenerHandler: Handler, + ) = PixelCopy.request(window, srcRect, dest, listener, listenerHandler) + } + + internal object Api24Impl { + fun request( + window: Window, dest: Bitmap, + listener: PixelCopy.OnPixelCopyFinishedListener, + listenerHandler: Handler, + ) { + val insets = Rect() + val surface = sourceForWindow(window, insets) + PixelCopy.request(surface, dest, listener, listenerHandler) + } + + @SuppressLint("PrivateApi") + private fun sourceForWindow(source: Window?, outInsets: Rect): Surface { + requireNotNull(source) { "source is null" } + requireNotNull(source.peekDecorView()) { "Only able to copy windows with decor views" } + var surface: Surface? = null + val dv = source.peekDecorView() + val root = dv?.let { + View::class.java.getDeclaredField("mAttachInfo").apply { + isAccessible = true + }[it]?.let { attachInfo -> + attachInfo.javaClass.getDeclaredField("mViewRootImpl").apply { + isAccessible = true + }[attachInfo] + } + } // as ViewRootImpl + if (root != null) { + surface = root.javaClass.getDeclaredField("mSurface").apply { + isAccessible = true + }[root] as Surface + val windowAttrs = root.javaClass.getDeclaredField("mWindowAttributes").apply { + isAccessible = true + }[root] // as WindowManager.LayoutParams + val surfaceInsets = windowAttrs.javaClass.getDeclaredField("surfaceInsets").apply { + isAccessible = true + }[windowAttrs] as Rect + // val width = root.javaClass.getDeclaredField("mWidth").apply { + // isAccessible = true + // }[root] as Int + // val height = root.javaClass.getDeclaredField("mHeight").apply { + // isAccessible = true + // }[root] as Int + val width = dv.rootView.width + val height = dv.rootView.height + outInsets.set( + surfaceInsets.left, surfaceInsets.top, + width + surfaceInsets.left, height + surfaceInsets.top + ) + } + require(surface != null && surface.isValid) { "Window doesn't have a backing surface!" } + return surface + } + } +} \ No newline at end of file diff --git a/docs/img/pixelcopy_android_7.png b/docs/img/pixelcopy_android_7.png new file mode 100644 index 0000000..84f8a48 Binary files /dev/null and b/docs/img/pixelcopy_android_7.png differ