1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.spa.framework.compose 18 19 import android.graphics.drawable.Animatable 20 import android.graphics.drawable.BitmapDrawable 21 import android.graphics.drawable.ColorDrawable 22 import android.graphics.drawable.Drawable 23 import android.os.Build 24 import android.os.Handler 25 import android.os.Looper 26 import android.view.View 27 import androidx.compose.runtime.Composable 28 import androidx.compose.runtime.RememberObserver 29 import androidx.compose.runtime.getValue 30 import androidx.compose.runtime.mutableStateOf 31 import androidx.compose.runtime.remember 32 import androidx.compose.runtime.setValue 33 import androidx.compose.ui.geometry.Size 34 import androidx.compose.ui.graphics.Color 35 import androidx.compose.ui.graphics.ColorFilter 36 import androidx.compose.ui.graphics.asAndroidColorFilter 37 import androidx.compose.ui.graphics.asImageBitmap 38 import androidx.compose.ui.graphics.drawscope.DrawScope 39 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 40 import androidx.compose.ui.graphics.nativeCanvas 41 import androidx.compose.ui.graphics.painter.BitmapPainter 42 import androidx.compose.ui.graphics.painter.ColorPainter 43 import androidx.compose.ui.graphics.painter.Painter 44 import androidx.compose.ui.graphics.withSave 45 import androidx.compose.ui.unit.LayoutDirection 46 import kotlin.math.roundToInt 47 48 /** 49 * ************************************************************************************************* 50 * This file was forked from 51 * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt 52 * and will be removed once it lands in AndroidX. 53 */ 54 55 private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) { 56 Handler(Looper.getMainLooper()) 57 } 58 59 /** 60 * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances 61 * should be remembered to be able to start and stop [Animatable] animations. 62 * 63 * Instances are usually retrieved from [rememberDrawablePainter]. 64 */ 65 class DrawablePainter( 66 val drawable: Drawable 67 ) : Painter(), RememberObserver { 68 private var drawInvalidateTick by mutableStateOf(0) 69 private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize) 70 71 private val callback: Drawable.Callback by lazy { 72 object : Drawable.Callback { 73 override fun invalidateDrawable(d: Drawable) { 74 // Update the tick so that we get re-drawn 75 drawInvalidateTick++ 76 // Update our intrinsic size too 77 drawableIntrinsicSize = drawable.intrinsicSize 78 } 79 80 override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) { 81 MAIN_HANDLER.postAtTime(what, time) 82 } 83 84 override fun unscheduleDrawable(d: Drawable, what: Runnable) { 85 MAIN_HANDLER.removeCallbacks(what) 86 } 87 } 88 } 89 90 init { 91 if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) { 92 // Update the drawable's bounds to match the intrinsic size 93 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) 94 } 95 } 96 97 override fun onRemembered() { 98 drawable.callback = callback 99 drawable.setVisible(true, true) 100 if (drawable is Animatable) drawable.start() 101 } 102 103 override fun onAbandoned() = onForgotten() 104 105 override fun onForgotten() { 106 if (drawable is Animatable) drawable.stop() 107 drawable.setVisible(false, false) 108 drawable.callback = null 109 } 110 111 override fun applyAlpha(alpha: Float): Boolean { 112 drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255) 113 return true 114 } 115 116 override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { 117 drawable.colorFilter = colorFilter?.asAndroidColorFilter() 118 return true 119 } 120 121 override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean { 122 if (Build.VERSION.SDK_INT >= 23) { 123 return drawable.setLayoutDirection( 124 when (layoutDirection) { 125 LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR 126 LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL 127 } 128 ) 129 } 130 return false 131 } 132 133 override val intrinsicSize: Size get() = drawableIntrinsicSize 134 135 override fun DrawScope.onDraw() { 136 drawIntoCanvas { canvas -> 137 // Reading this ensures that we invalidate when invalidateDrawable() is called 138 drawInvalidateTick 139 140 // Update the Drawable's bounds 141 drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt()) 142 143 canvas.withSave { 144 drawable.draw(canvas.nativeCanvas) 145 } 146 } 147 } 148 } 149 150 /** 151 * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the 152 * drawable contents and use Compose primitives where possible. 153 * 154 * If the provided [drawable] is `null`, an empty no-op painter is returned. 155 * 156 * This function tries to dispatch lifecycle events to [drawable] as much as possible from 157 * within Compose. 158 * 159 * @sample com.google.accompanist.sample.drawablepainter.BasicSample 160 */ 161 @Composable 162 fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) { 163 when (drawable) { 164 null -> EmptyPainter 165 is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap()) 166 is ColorDrawable -> ColorPainter(Color(drawable.color)) 167 // Since the DrawablePainter will be remembered and it implements RememberObserver, it 168 // will receive the necessary events 169 else -> DrawablePainter(drawable.mutate()) 170 } 171 } 172 173 private val Drawable.intrinsicSize: Size 174 get() = when { 175 // Only return a finite size if the drawable has an intrinsic size 176 intrinsicWidth >= 0 && intrinsicHeight >= 0 -> { 177 Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat()) 178 } 179 else -> Size.Unspecified 180 } 181 182 internal object EmptyPainter : Painter() { 183 override val intrinsicSize: Size get() = Size.Unspecified 184 override fun DrawScope.onDraw() {} 185 } 186