Skip to content

Commit 911e168

Browse files
committed
feat: add lib implementation from test project
https://github.com/dimaportenko/react-native-android-shadow-test fix example project
1 parent 440665b commit 911e168

File tree

15 files changed

+563
-76
lines changed

15 files changed

+563
-76
lines changed

android/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ android {
2929
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
3030
buildToolsVersion getExtOrDefault('buildToolsVersion')
3131
defaultConfig {
32-
minSdkVersion 16
32+
minSdkVersion 21
3333
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
3434
versionCode 1
3535
versionName "1.0"
36-
36+
3737
}
38-
38+
3939
buildTypes {
4040
release {
4141
minifyEnabled false
@@ -127,4 +127,5 @@ dependencies {
127127
// noinspection GradleDynamicVersion
128128
api 'com.facebook.react:react-native:+'
129129
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
130+
implementation "androidx.core:core-ktx:$kotlin_version"
130131
}

android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ShadowView_kotlinVersion=1.3.50
1+
ShadowView_kotlinVersion=1.5.0
22
ShadowView_compileSdkVersion=29
33
ShadowView_buildToolsVersion=29.0.2
44
ShadowView_targetSdkVersion=29
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package com.reactnativeshadowview
2+
3+
import android.content.Context
4+
import android.content.res.Resources
5+
import android.graphics.*
6+
import android.renderscript.Allocation
7+
import android.renderscript.Element
8+
import android.renderscript.RenderScript
9+
import android.renderscript.ScriptIntrinsicBlur
10+
import android.util.AttributeSet
11+
import android.util.DisplayMetrics
12+
import android.view.View
13+
import android.view.ViewOutlineProvider
14+
import android.widget.FrameLayout
15+
import androidx.annotation.ColorRes
16+
import androidx.annotation.DimenRes
17+
import androidx.annotation.Nullable
18+
import androidx.core.content.res.ResourcesCompat
19+
import androidx.core.graphics.scaleMatrix
20+
import androidx.core.graphics.times
21+
import androidx.core.graphics.translationMatrix
22+
import androidx.core.graphics.withMatrix
23+
import kotlin.math.ceil
24+
25+
/** A CSS like shadow */
26+
@Suppress("PropertyName")
27+
open class ShadowLayout @JvmOverloads constructor(
28+
context: Context,
29+
@Nullable attrs: AttributeSet? = null,
30+
defStyleAttr: Int = R.attr.shadowLayoutStyle,
31+
defStyleRes: Int = 0
32+
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
33+
34+
companion object {
35+
@JvmField
36+
val ratioDpToPixels =
37+
Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
38+
39+
@JvmField
40+
val ratioPixelsToDp: Float = (1.0 / ratioDpToPixels.toDouble()).toFloat()
41+
42+
// Thanks to this hero https://stackoverflow.com/a/41322648/4681367
43+
const val cssRatio: Float = 5f / 3f
44+
}
45+
46+
// BASIC FIELDS
47+
48+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)
49+
private val eraser = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }
50+
51+
var shadow_color: Int
52+
get() = paint.color
53+
set(value) {
54+
if (paint.color == value) return
55+
paint.color = value
56+
postInvalidate()
57+
}
58+
59+
fun setColorRes(@ColorRes color: Int) {
60+
shadow_color = ResourcesCompat.getColor(resources, color, context.theme)
61+
}
62+
63+
var shadow_x_shift: Float by OnUpdate(0f)
64+
fun setXShift(@DimenRes shift: Int) {
65+
shadow_x_shift = context.resources.getDimension(shift)
66+
}
67+
68+
var shadow_y_shift: Float by OnUpdate(0f)
69+
fun setYShift(@DimenRes shift: Int) {
70+
shadow_y_shift = context.resources.getDimension(shift)
71+
}
72+
73+
var shadow_downscale: Float by OnUpdate(1f, { it.coerceAtLeast(0.1f) }) {
74+
realRadius = shadow_radius / it
75+
updateBitmap()
76+
}
77+
var shadow_radius: Float by OnUpdate(0f, { it.coerceAtLeast(0f) }) {
78+
realRadius = it / shadow_downscale
79+
}
80+
private var realRadius: Float by OnUpdate(
81+
0f, { it.coerceIn(0f, 25f) } // allowed blur size on ScriptIntrinsicBlur by android
82+
)
83+
84+
var shadow_cast_only_background: Boolean by OnUpdate(false)
85+
var shadow_with_content: Boolean by OnUpdate(true)
86+
var shadow_with_color: Boolean by OnUpdate(false) {
87+
destroyBitmap()
88+
updateBitmap()
89+
}
90+
var shadow_with_dpi_scale: Boolean by OnUpdate(true) {
91+
destroyBitmap()
92+
updateBitmap()
93+
}
94+
var shadow_with_css_scale: Boolean by OnUpdate(true) {
95+
destroyBitmap()
96+
updateBitmap()
97+
}
98+
99+
100+
// IN VARIABLES
101+
102+
private val ratioDpToPixels get() = if (shadow_with_dpi_scale) Companion.ratioDpToPixels else 1f
103+
private val ratioPixelsToDp get() = if (shadow_with_dpi_scale) Companion.ratioPixelsToDp else 1f
104+
private val cssRatio get() = if (shadow_with_css_scale) Companion.cssRatio else 1f
105+
106+
// size in pixel of the blur spread
107+
private val pixelsOverBoundaries: Int get() = if (shadow_downscale < 1f) 25 else ceil(25f * shadow_downscale).toInt()
108+
private val viewBounds: Rect = Rect()
109+
private fun setViewBounds(width: Int, height: Int) {
110+
viewBounds.set(0, 0, width, height)
111+
updateBitmap()
112+
}
113+
114+
private var blurBitmap: Bitmap? = null
115+
private var blurCanvas: Canvas? = null
116+
117+
private var renderScript: RenderScript? = null;
118+
private var script: ScriptIntrinsicBlur? = null;
119+
private var inAlloc: Allocation? = null
120+
private var outAlloc: Allocation? = null
121+
122+
private var lastWithColorScript: Boolean? = null
123+
private fun getScript(): Pair<ScriptIntrinsicBlur, RenderScript> {
124+
val renderScript = this.renderScript ?: RenderScript.create(context)
125+
if (lastWithColorScript != shadow_with_color) { // recreate script only if colors change
126+
lastWithColorScript = shadow_with_color
127+
script = null
128+
}
129+
if (script != null) return Pair(script!!, renderScript!!)
130+
val element = if (shadow_with_color) Element.U8_4(renderScript) else Element.U8(renderScript)
131+
script = ScriptIntrinsicBlur.create(renderScript, element)
132+
return Pair(script!!, renderScript!!)
133+
}
134+
135+
private val lastBounds = Rect()
136+
private var lastScale = 0f
137+
private var lastWithColorBitmap: Boolean? = null
138+
private var lastWithDpi: Boolean? = null
139+
private var lastWithCss: Boolean? = null
140+
private fun updateBitmap() {
141+
// do not recreate if same specs.
142+
if (viewBounds.isEmpty || isAttachedToWindow
143+
&& lastBounds == viewBounds
144+
&& shadow_downscale == lastScale
145+
&& shadow_with_color == lastWithColorBitmap
146+
&& shadow_with_dpi_scale == lastWithDpi
147+
&& shadow_with_css_scale == lastWithCss
148+
) return
149+
lastBounds.set(viewBounds)
150+
lastScale = shadow_downscale
151+
lastWithColorBitmap = shadow_with_color
152+
lastWithColorBitmap = shadow_with_color
153+
lastWithDpi = shadow_with_dpi_scale
154+
lastWithCss = shadow_with_css_scale
155+
156+
// create a receptacle for blur script. (MDPI / downscale) + (pixels * 2) cause blur spread in all directions
157+
blurBitmap?.recycle()
158+
blurBitmap = Bitmap.createBitmap(
159+
(ceil(
160+
(viewBounds.width().toFloat() * ratioPixelsToDp) / shadow_downscale / cssRatio
161+
) + pixelsOverBoundaries * 2).toInt(),
162+
(ceil(
163+
(viewBounds.height().toFloat() * ratioPixelsToDp) / shadow_downscale / cssRatio
164+
) + pixelsOverBoundaries * 2).toInt(),
165+
if (shadow_with_color) Bitmap.Config.ARGB_8888 else Bitmap.Config.ALPHA_8
166+
)
167+
168+
blurCanvas = Canvas(blurBitmap!!)
169+
170+
val (script, renderScript) = getScript()
171+
inAlloc?.destroy()
172+
inAlloc = Allocation.createFromBitmap(renderScript, blurBitmap)
173+
if (outAlloc?.type != inAlloc?.type) {
174+
outAlloc?.destroy()
175+
outAlloc = Allocation.createTyped(renderScript, inAlloc!!.type)
176+
}
177+
script.setInput(inAlloc)
178+
}
179+
180+
private fun destroyBitmap() {
181+
blurBitmap?.recycle()
182+
blurBitmap = null
183+
blurCanvas = null
184+
script?.destroy()
185+
script = null
186+
inAlloc?.destroy()
187+
inAlloc = null
188+
outAlloc?.destroy()
189+
outAlloc = null
190+
lastBounds.setEmpty()
191+
lastScale = 0f
192+
lastWithColorScript = null
193+
lastWithColorBitmap = null
194+
lastWithDpi = null
195+
lastWithCss = null
196+
}
197+
198+
/** Cause the default elevation rendering to not work */
199+
override fun getOutlineProvider(): ViewOutlineProvider = object : ViewOutlineProvider() {
200+
override fun getOutline(view: View?, outline: Outline?) = Unit
201+
}
202+
203+
// Overriding view
204+
205+
init {
206+
if (!isInEditMode) {
207+
val attributes = context.obtainStyledAttributes(
208+
attrs, R.styleable.ShadowLayout, defStyleAttr, defStyleRes
209+
)
210+
211+
shadow_color = attributes.getColor(R.styleable.ShadowLayout_shadow_color, 51 shl 24)
212+
shadow_with_color = attributes.getBoolean(R.styleable.ShadowLayout_shadow_with_color, false)
213+
shadow_with_content = attributes.getBoolean(R.styleable.ShadowLayout_shadow_with_content, true)
214+
shadow_with_dpi_scale = attributes.getBoolean(R.styleable.ShadowLayout_shadow_with_dpi_scale, true)
215+
shadow_with_css_scale = attributes.getBoolean(R.styleable.ShadowLayout_shadow_with_css_scale, true)
216+
shadow_x_shift = attributes.getDimension(R.styleable.ShadowLayout_shadow_x_shift, 0f)
217+
shadow_y_shift = attributes.getDimension(R.styleable.ShadowLayout_shadow_y_shift, 0f)
218+
shadow_downscale = attributes.getFloat(R.styleable.ShadowLayout_shadow_downscale, 1f)
219+
shadow_radius = attributes.getFloat(R.styleable.ShadowLayout_shadow_radius, 6f)
220+
shadow_cast_only_background = attributes.getBoolean(R.styleable.ShadowLayout_shadow_cast_only_background, false)
221+
222+
attributes.recycle()
223+
}
224+
setWillNotDraw(false)
225+
}
226+
227+
override fun onAttachedToWindow() {
228+
super.onAttachedToWindow()
229+
if (!isInEditMode) updateBitmap()
230+
}
231+
232+
override fun onDetachedFromWindow() {
233+
super.onDetachedFromWindow()
234+
if (!isInEditMode) destroyBitmap()
235+
}
236+
237+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
238+
super.onSizeChanged(w, h, oldw, oldh)
239+
if (!isInEditMode) setViewBounds(w, h)
240+
}
241+
242+
private inline val blurTMatrix: Matrix // cause blur spreads
243+
get() = translationMatrix(pixelsOverBoundaries.toFloat(), pixelsOverBoundaries.toFloat())
244+
private inline val blurSMatrix: Matrix // to draw inside the small blurBitmap
245+
get() = scaleMatrix(ratioPixelsToDp / shadow_downscale / cssRatio, ratioPixelsToDp / shadow_downscale / cssRatio)
246+
247+
private inline val drawTMatrix: Matrix // counterbalance for blur spread in canvas
248+
get() = translationMatrix(
249+
-(pixelsOverBoundaries * ratioDpToPixels * shadow_downscale * cssRatio),
250+
-(pixelsOverBoundaries * ratioDpToPixels * shadow_downscale * cssRatio)
251+
)
252+
private inline val drawSMatrix: Matrix // enlarge blur image to canvas size
253+
get() = scaleMatrix(
254+
ratioDpToPixels * shadow_downscale * cssRatio,
255+
ratioDpToPixels * shadow_downscale * cssRatio
256+
)
257+
private inline val shiftTMatrix: Matrix // User want a nice shifted shadow
258+
get() = translationMatrix(
259+
shadow_x_shift / shadow_downscale / cssRatio,
260+
shadow_y_shift / shadow_downscale / cssRatio
261+
)
262+
263+
override fun draw(canvas: Canvas?) {
264+
canvas ?: return
265+
if (isInEditMode) {
266+
super.draw(canvas)
267+
return
268+
}
269+
if (blurCanvas != null) {
270+
blurCanvas!!.drawRect(blurCanvas!!.clipBounds, eraser)
271+
blurCanvas!!.withMatrix(blurTMatrix * blurSMatrix) {
272+
if (shadow_cast_only_background) {
273+
background.bounds = viewBounds
274+
background?.draw(blurCanvas!!)
275+
} else super.draw(blurCanvas)
276+
}
277+
if (realRadius > 0f) { // Do not blur if no radius
278+
val (script) = getScript()
279+
script.setRadius(realRadius)
280+
inAlloc?.copyFrom(blurBitmap)
281+
script.forEach(outAlloc)
282+
outAlloc?.copyTo(blurBitmap)
283+
}
284+
canvas.withMatrix(drawTMatrix * drawSMatrix * shiftTMatrix) {
285+
canvas.drawBitmap(blurBitmap!!, 0f, 0f, paint)
286+
}
287+
}
288+
if (shadow_with_content) super.draw(canvas)
289+
}
290+
291+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.reactnativeshadowview
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import com.facebook.react.bridge.ReadableMap
6+
7+
/**
8+
* Created by Dmytro Portenko on 10.08.2021.
9+
*/
10+
class ShadowView(
11+
context: Context,
12+
attrs: AttributeSet? = null,
13+
defStyleAttr: Int = R.attr.shadowLayoutStyle,
14+
defStyleRes: Int = 0
15+
) :
16+
ShadowLayout(context, attrs, defStyleAttr, defStyleRes) {
17+
init {
18+
shadow_color = 0
19+
shadow_with_color = false
20+
shadow_with_content = true
21+
shadow_with_css_scale = true
22+
shadow_with_dpi_scale = true
23+
shadow_cast_only_background = true
24+
shadow_x_shift = 0f
25+
shadow_y_shift = 0f
26+
shadow_radius = 0f
27+
shadow_downscale = 1f
28+
}
29+
30+
fun getColorWithOpacity(color: Int, opacity: Float): Int {
31+
return (opacity * 255.0f).toInt() shl 24 or (color and 0x00ffffff)
32+
}
33+
34+
fun setShadowProps(shadowProps: ReadableMap?) {
35+
if (shadowProps != null) {
36+
val shadowOffset = shadowProps.getMap("shadowOffset")
37+
if (shadowOffset != null) {
38+
shadow_x_shift = if (shadowOffset.hasKey("width")) {
39+
val shadowX = shadowOffset.getDouble("width").toFloat()
40+
shadowX
41+
} else {
42+
0f
43+
}
44+
45+
shadow_y_shift = if (shadowOffset.hasKey("height")) {
46+
val shadowY = shadowOffset.getDouble("height").toFloat()
47+
shadowY
48+
} else {
49+
0f
50+
}
51+
}
52+
if (shadowProps.hasKey("shadowRadius")) {
53+
val shadowRadius = shadowProps.getDouble("shadowRadius").toFloat()
54+
shadow_radius = shadowRadius
55+
}
56+
57+
shadow_color = when {
58+
shadowProps.hasKey("shadowColor") -> {
59+
var shadowColor = shadowProps.getInt("shadowColor")
60+
shadowColor = if (shadowProps.hasKey("shadowOpacity")) {
61+
val shadowOpacity = shadowProps.getDouble("shadowOpacity").toFloat()
62+
getColorWithOpacity(shadowColor, shadowOpacity)
63+
} else {
64+
getColorWithOpacity(shadowColor, 0f)
65+
}
66+
shadowColor
67+
}
68+
shadowProps.hasKey("shadowOpacity") -> {
69+
val shadowOpacity = shadowProps.getDouble("shadowOpacity").toFloat()
70+
getColorWithOpacity(0, shadowOpacity)
71+
}
72+
else -> getColorWithOpacity(0, 0f)
73+
}
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)