Giter Site home page Giter Site logo

shadowlayout's Introduction

ShadowLayout a css like shadow for android

Have you ever wanted a CSS type of shadow in your Android project ? No?! Me neither but designers keep using it and the death flag is triggered when the app isn't a copy of the design. So, there it goes.

It do much more than casting a shadow so the name can be inaccurate. Internally, it uses the native ScriptIntrinsicBlur render script so it can easily blur the whole layout like the Blurry library.

Requirements

Android 5.+ (API 21)

Gradle

The project is hosted on maven Central :

allprojects {
    repositories {
        mavenCentral()
    }
}

Then add dependency :

dependencies {
    implementation("net.orandja.shadowlayout:shadowlayout:1.0.1")
}

Usage

Add android:clipChildren="false", android:clipToPadding="false" to the parent layout to let ShadowLayout draw outside of view bounds.

<net.orandja.shadowlayout.ShadowLayout
    android:padding="8dp"
    android:layout_gravity="center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/round_corners"
    app:shadow_radius="8"
    app:shadow_x_shift="-1dp"
    app:shadow_y_shift="2dp"
    app:shadow_downscale="1"
    app:shadow_color="#808"
    app:shadow_with_foreground="true"
    app:shadow_with_color="false"
    app:shadow_with_dpi_scale="true"
    app:shadow_with_css_scale="true"
    app:shadow_cast_only_background="true">

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:text="The quick brown fox jump over the lazy dog"
        android:textSize="16sp" />

</net.orandja.shadowlayout.ShadowLayout>

quick brown fox text view

Parameters

Default value in parenthesis

  • shadow_radius (6f) The radius of the gaussian blur in float. See #Rules of downscaling section to see how it works.
  • shadow_x_shift (0dp) Shift on the horizontal axis in dp
  • shadow_y_shift (0dp) Shift on the vertical axis in dp
  • shadow_color (#33000000) Color of the casted shadow
  • shadow_downscale (1f) Downscale of the internal bitmap that render the shadow. The higher the downscale, the lower in size is the bitmap. At 2, the size of bitmap is halved.
  • shadow_with_foreground (true) Draw the actual view on top of the shadow.
  • shadow_with_color (false) Keep all the subview colors in the blur. Coupled with shadow_with_foreground at false, a blur image can be render on screen.
  • shadow_with_dpi_scale (true) Downscale the internal bitmap by the current smartphone dpi. A 100dp view will result in a 100px bitmap.
  • shadow_with_css_scale (true) Downscale by 5/3 more. Because CSS shadow standard blur by a half more than the real blur size.
  • shadow_cast_only_background (false) Only render the background and not the view as shadow

Limitations

The default implementation of android gaussian blur (ScriptIntrinsicBlur render script) is limited to 25 pixels at max. To counter that, the layout downscale the real render of the view inside a bitmap (ALPHA_8 or RGBA with shadow_with_color at true). Then blur it. Then renders the blur inside the canvas. Then redraw the view in the canvas. (shadow_with_foreground at true)

The radius can mean multiple things depending on the configurations.

Rules of downscaling

When no downscale are applied:

<net.orandja.shadowlayout.ShadowLayout
    app:shadow_radius="3f"
    app:shadow_downscale="1"
    app:shadow_with_dpi_scale="false"
    app:shadow_with_css_scale="false" />

The internal bitmap is the same size of the view. The radius is the number of pixels blured around. With high dpi, it will be hard to see the effect. However, in some cases, it can be usefull to have a nice looking blur at the expense of more memory. The max radius is 25 pixels. The parameter shadow_downscale multiply the max radius by its value. So at shadow_downscale="2.5" it increases the max radius to 62.5f.

When app:shadow_with_dpi_scale="true": Downscale the internal bitmap by the current smartphone dpi. A 100dp view will result in a 100px bitmap. The radius means dp. It blurs up to 25dp.

When app:shadow_with_css_scale="true": Same as dpi, it will add a 5/3 downscale to mimic the CSS shadow blur.

Shadow anything

As said before since it draw the whole layout it can cast a shadow of anything. Like text:

    <net.orandja.shadowlayout.ShadowLayout
        android:padding="8dp"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:shadow_radius="1"
        app:shadow_x_shift="1dp"
        app:shadow_y_shift="2dp"
        app:shadow_downscale="1"
        app:shadow_color="#AA000000"
        app:shadow_with_foreground="true"
        app:shadow_with_color="true"
        app:shadow_with_css_scale="false"
        app:shadow_with_dpi_scale="true"
        app:shadow_cast_only_background="true">

        <androidx.appcompat.widget.AppCompatTextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#808"
            android:text="The quick brown fox jump over the lazy dog"
            android:textSize="16sp" />

    </net.orandja.shadowlayout.ShadowLayout>

quick brown fox text view

Since shadow_with_color is at true, the drawn shadow is of the same color of the text rendered. The color parameter only affect alpha.

shadowlayout's People

Contributors

l-briand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

shadowlayout's Issues

Error in preview and show shadow

Hi,

I just copied your code and did not change it. But Android Studio does not show me preview and shadow. Android Studio preview shows the following error:

Render problem in android studio:

android.renderscript.RSDriverException: Failed to create RS context.   
   at android.renderscript.RenderScript.internalCreate(RenderScript.java:1441)   
   at android.renderscript.RenderScript.create(RenderScript.java:1543)   
   at android.renderscript.RenderScript.create(RenderScript.java:1500)   
   at android.renderscript.RenderScript.create(RenderScript.java:1474)   
   at android.renderscript.RenderScript.create(RenderScript.java:1461)   
   at com.package.ShadowLayout.getScript(ShadowLayout.kt:166)   
   at com.package.ShadowLayout.updateBitmap(ShadowLayout.kt:213)   
   at com.package.ShadowLayout.setViewBounds(ShadowLayout.kt:153)   
   at com.package.ShadowLayout.onSizeChanged(ShadowLayout.kt:278)   
   at android.view.View.sizeChange(View.java:22120)   
   at android.view.View.setFrame(View.java:22072)   
   at android.view.View.layout_Original(View.java:21931)   
   at android.view.View_Delegate.layout(View_Delegate.java:91)  
   at android.view.View.layout(View.java:21920)   
   at android.view.ViewGroup.layout(ViewGroup.java:6260)   
   at androidx.constraintlayout.widget.ConstraintLayout.onLayout_Original(ConstraintLayout.java:1762)  
   at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:-1)   
   at android.view.View.layout_Original(View.java:21934)   
   at android.view.View_Delegate.layout(View_Delegate.java:91)   
   at android.view.View.layout(View.java:21920)   
   at android.view.ViewGroup.layout(ViewGroup.java:6260)   
   at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)   
   at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)   
   at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout_Original(CoordinatorLayout.java:919)   
   at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:-1)  
   at android.view.View.layout_Original(View.java:21934)   
   at android.view.View_Delegate.layout(View_Delegate.java:91)   
   at android.view.View.layout(View.java:21920)  
   at android.view.ViewGroup.layout(ViewGroup.java:6260)   
   at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)    
   at android.widget.FrameLayout.onLayout(FrameLayout.java:270)   
   at android.view.View.layout_Original(View.java:21934)  
   at android.view.View_Delegate.layout(View_Delegate.java:91)   
   at android.view.View.layout(View.java:21920)   
   at android.view.ViewGroup.layout(ViewGroup.java:6260)  
   at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1103)    
   at android.view.View.layout_Original(View.java:21934)    
   at android.view.View_Delegate.layout(View_Delegate.java:91)  
   at android.view.View.layout(View.java:21920)   
   at android.view.ViewGroup.layout(ViewGroup.java:6260)   
   at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)    
   at android.widget.FrameLayout.onLayout(FrameLayout.java:270)   
   at android.view.View.layout_Original(View.java:21934)   
   at android.view.View_Delegate.layout(View_Delegate.java:91)   
   at android.view.View.layout(View.java:21920)   
   at android.view.ViewGroup.layout(ViewGroup.java:6260)

Kotlin:

package com.package

import android.content.Context
import android.content.res.Resources
import android.graphics.*
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.Nullable
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.scaleMatrix
import androidx.core.graphics.times
import androidx.core.graphics.translationMatrix
import androidx.core.graphics.withMatrix
import com.package.R
import kotlin.math.ceil

/** A CSS like shadow */
class ShadowLayout @JvmOverloads constructor(
    context: Context,
    @Nullable attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        @JvmField
        val ratioDpToPixels =
            Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT

        @JvmField
        val ratioPixelsToDp: Float = (1.0 / ratioDpToPixels.toDouble()).toFloat()

        // Thanks to this hero https://stackoverflow.com/a/41322648/4681367
        const val cssRatio: Float = 5f / 3f
    }

    // BASIC FIELDS

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)
    private val eraser = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }

    fun getColor(): Int = paint.color

    fun setColor(@ColorInt color: Int) {
        if (paint.color == color) return
        paint.color = color
        postInvalidate()
    }

    fun setColorRes(@ColorRes color: Int) {
        setColor(ResourcesCompat.getColor(resources, color, null))
    }

    var xShift: Float = 0f
        set(value) {
            if (field == value) return
            field = value
            postInvalidate()
        }

    fun setXShift(@DimenRes shift: Int) {
        xShift = context.resources.getDimension(shift)
    }

    var yShift: Float = 0f
        set(value) {
            if (field == value) return
            field = value
            postInvalidate()
        }

    fun setYShift(@DimenRes shift: Int) {
        yShift = context.resources.getDimension(shift)
    }

    var downscale: Float = 1f
        set(value) {
            if (field == value) return
            field = value.coerceAtLeast(0.1f)
            realRadius = radius / field
            updateBitmap()
            postInvalidate()
        }

    var radius: Float = 0f
        set(value) {
            if (field == value) return
            field = value.coerceAtLeast(0f)
            realRadius = field / downscale
            postInvalidate()
        }

    private var realRadius: Float = 0f
        set(value) {
            if (field == value) return
            field = value.coerceIn(0f, 25f) // allowed blur size on ScriptIntrinsicBlur by android
        }

    var withForeground: Boolean = true
        set(value) {
            if (field == value) return
            field = value
            postInvalidate()
        }

    var withColor: Boolean = false
        set(value) {
            if (field == value) return
            field = value
            destroyBitmap()
            updateBitmap()
            postInvalidate()
        }

    var withDpi: Boolean = true
        set(value) {
            field = value
            destroyBitmap()
            updateBitmap()
            postInvalidate()
        }

    var withCss: Boolean = true
        set(value) {
            field = value
            destroyBitmap()
            updateBitmap()
            postInvalidate()
        }

    // IN VARIABLES

    private val ratioDpToPixels get() = if (withDpi) SDKShadowLayout.ratioDpToPixels else 1f
    private val ratioPixelsToDp get() = if (withDpi) SDKShadowLayout.ratioPixelsToDp else 1f
    private val cssRatio get() = if (withCss) SDKShadowLayout.cssRatio else 1f

    // size in pixel of the blur spread
    private val pixelsOverBoundaries: Int get() = if (downscale < 1f) 25 else ceil(25f * downscale).toInt()
    private val viewBounds: Rect = Rect()
    private fun setViewBounds(width: Int, height: Int) {
        viewBounds.set(0, 0, width, height)
        println(viewBounds)
        updateBitmap()
    }

    private var blurBitmap: Bitmap? = null
    private var blurCanvas: Canvas? = null

    private var renderScript: RenderScript? = null
    private var script: ScriptIntrinsicBlur? = null
    private var inAlloc: Allocation? = null
    private var outAlloc: Allocation? = null

    private var lastWithColorScript: Boolean? = null
    private fun getScript(): Pair<ScriptIntrinsicBlur, RenderScript> {
        val renderScript = this.renderScript ?: RenderScript.create(context)
        if (lastWithColorScript != withColor) { // recreate script only if colors change
            lastWithColorScript = withColor
            script = null
        }
        if (script != null) return Pair(script!!, renderScript!!)
        val element = if (withColor) Element.U8_4(renderScript) else Element.U8(renderScript)
        script = ScriptIntrinsicBlur.create(renderScript, element)
        return Pair(script!!, renderScript!!)
    }

    private val lastBounds = Rect()
    private var lastScale = 0f
    private var lastWithColorBitmap: Boolean? = null
    private var lastWithDpi: Boolean? = null
    private var lastWithCss: Boolean? = null
    private fun updateBitmap() {
        // do not recreate if same specs.
        if (viewBounds.isEmpty || isAttachedToWindow
            && lastBounds == viewBounds
            && downscale == lastScale
            && withColor == lastWithColorBitmap
            && withDpi == lastWithDpi
            && withCss == lastWithCss
        ) return
        lastBounds.set(viewBounds)
        lastScale = downscale
        lastWithColorBitmap = withColor
        lastWithColorBitmap = withColor
        lastWithDpi = withDpi
        lastWithCss = withCss

        // create a receptacle for blur script. (MDPI / downscale) + (pixels * 2) cause blur spread in all directions
        blurBitmap?.recycle()
        blurBitmap = Bitmap.createBitmap(
            (ceil(
                (viewBounds.width().toFloat() * ratioPixelsToDp) / downscale / cssRatio
            ) + pixelsOverBoundaries * 2).toInt(),
            (ceil(
                (viewBounds.height().toFloat() * ratioPixelsToDp) / downscale / cssRatio
            ) + pixelsOverBoundaries * 2).toInt(),
            if (withColor) Bitmap.Config.ARGB_8888 else Bitmap.Config.ALPHA_8
        )
        println("lol : " + blurBitmap!!.width + ", " + blurBitmap!!.height + " -- " + pixelsOverBoundaries)

        blurCanvas = Canvas(blurBitmap!!)

        val (script, renderScript) = getScript()
        inAlloc?.destroy()
        inAlloc = Allocation.createFromBitmap(renderScript, blurBitmap)
        if (outAlloc?.type != inAlloc?.type) {
            outAlloc?.destroy()
            outAlloc = Allocation.createTyped(renderScript, inAlloc!!.type)
        }
        script.setInput(inAlloc)
    }

    private fun destroyBitmap() {
        blurBitmap?.recycle()
        blurBitmap = null
        blurCanvas = null
        script?.destroy()
        script = null
        inAlloc?.destroy()
        inAlloc = null
        outAlloc?.destroy()
        outAlloc = null
        lastBounds.setEmpty()
        lastScale = 0f
        lastWithColorScript = null
        lastWithColorBitmap = null
        lastWithDpi = null
        lastWithCss = null
    }

    override fun getOutlineProvider(): ViewOutlineProvider = object : ViewOutlineProvider() {
        override fun getOutline(view: View?, outline: Outline?) = Unit
    }

    // Overriding view

    init {
        val attributes = context.obtainStyledAttributes(
            attrs, R.styleable.SDKShadowLayout, defStyleAttr, defStyleRes
        )
        setColor(attributes.getColor(R.styleable.SDKShadowLayout_shadow_color, 51 shl 24))
        withColor = attributes.getBoolean(R.styleable.SDKShadowLayout_shadow_with_color, false)
        withForeground =
            attributes.getBoolean(R.styleable.SDKShadowLayout_shadow_with_foreground, true)
        withDpi = attributes.getBoolean(R.styleable.SDKShadowLayout_shadow_with_dpi_scale, true)
        withCss = attributes.getBoolean(R.styleable.SDKShadowLayout_shadow_with_css_scale, true)
        xShift = attributes.getDimension(R.styleable.SDKShadowLayout_shadow_x_shift, 0f)
        yShift = attributes.getDimension(R.styleable.SDKShadowLayout_shadow_y_shift, 0f)
        downscale = attributes.getFloat(R.styleable.SDKShadowLayout_shadow_downscale, 1f)
        radius = attributes.getFloat(R.styleable.SDKShadowLayout_shadow_radius, 6f)

        attributes.recycle()
        setWillNotDraw(false)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        updateBitmap()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        destroyBitmap()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        setViewBounds(w, h)
    }

    private val blurTMatrix: Matrix // cause blur spreads
        get() = translationMatrix(pixelsOverBoundaries.toFloat(), pixelsOverBoundaries.toFloat())
    private val blurSMatrix: Matrix // to draw inside the small blurBitmap
        get() = scaleMatrix(
            ratioPixelsToDp / downscale / cssRatio,
            ratioPixelsToDp / downscale / cssRatio
        )
    private val drawTMatrix: Matrix // counterbalance for blur spread in canvas
        get() = translationMatrix(
            -(pixelsOverBoundaries * ratioDpToPixels * downscale * cssRatio),
            -(pixelsOverBoundaries * ratioDpToPixels * downscale * cssRatio)
        )
    private val drawSMatrix: Matrix // enlarge blur image to canvas size
        get() = scaleMatrix(
            ratioDpToPixels * downscale * cssRatio,
            ratioDpToPixels * downscale * cssRatio
        )
    private val shiftTMatrix: Matrix // User want a nice shifted shadow
        get() = translationMatrix(
            xShift / downscale / cssRatio,
            yShift / downscale / cssRatio
        )

    override fun draw(canvas: Canvas?) {
        canvas ?: return
        if (blurCanvas != null) {
            blurCanvas!!.drawRect(blurCanvas!!.clipBounds, eraser)
            blurCanvas!!.withMatrix(blurTMatrix * blurSMatrix) { super.draw(blurCanvas) }
            if (realRadius > 0f) { // Do not blur if no radius
                val (script) = getScript()
                script.setRadius(realRadius)
                inAlloc?.copyFrom(blurBitmap)
                script.forEach(outAlloc)
                outAlloc?.copyTo(blurBitmap)
            }
            canvas.withMatrix(drawTMatrix * drawSMatrix * shiftTMatrix) {
                canvas.drawBitmap(blurBitmap!!, 0f, 0f, paint)
            }
        }
        if (withForeground) super.draw(canvas)
    }
}

XML:

<com.package.ShadowLayout
    android:layout_width="250dp"
    android:layout_height="250dp"
    android:layout_margin="12dp"
    android:background="@drawable/irx_shape_big"
    android:padding="16dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/appBar"
    app:layout_constraintVertical_bias="0.7"
    app:shadow_color="#FF000000"
    app:shadow_downscale="1"
    app:shadow_radius="25"
    app:shadow_with_color="true"
    app:shadow_with_css_scale="true"
    app:shadow_with_dpi_scale="true"
    app:shadow_with_foreground="true"
    app:shadow_x_shift="4dp"
    app:shadow_y_shift="10dp">

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="The quick brown fox jump over the lazy dog"
        android:textColor="#808"
        android:textSize="16sp" />

</com.package.ShadowLayout>

Shadow not working...

alt text

How to install

There are no installation instructions anywhere
How do I properly install this library?

Not working

Hi there,
I am very appreciate your hard work. But when I make an example it is totally not working at all.
Can you please add some executable code for testing please?
Thanks,

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.