Giter Site home page Giter Site logo

fiftyshades's Introduction

Fifty Shades: CSS-style shadows for Android

What?

In CSS, shadows are specified by (dx, dy, blurRadius, colour) (I call it ShadowSpec). This library implements such shadows for Android.

Why?

I know only two shadow implementations in Android SDK out of the box: Paint#setShadowLayer and View#elevation.

ShadowLayer produces nice-looking shadows but works only with software rendering. A sheet with shadow could be big, containing many pixels, but this becomes even worse if you set SOFTWARE_LAYER on a whole full-screen RecyclerView in order to draw background properly.

Elevation is implemented somewhere deep inside RenderNode, there's no direct control over this shadow, there's no such a thing like Canvas#drawElevation. Also, speaking in terms of CSS, dx=0, and both dy and blurRadius are driven by elevation. Elevation colour is one more pain in the ass, but, well, I think you already know it.

There's also MaterialShapeDrawable which is actually a poor man's elevation. Same problems as above apply plus drawing artifacts. It also drops shadowColor's alpha; Drawable#setAlpha invocations don't alter shadow transparency either.

How?

repositories {
    // Groovy:
    maven { url 'https://jitpack.io' }
    
    // Kotlin:
    maven(url = "https://jitpack.io")
}

// module-level build.gradle:
dependencies {
    implementation('com.github.Miha-x64:FiftyShades:-SNAPSHOT')
}

Static shadow

RectWithShadow.createDrawable(
    RectSpec(Color.WHITE, dp(20)),
    ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK)
)

This will return a Drawable (a NinePatchDrawable wrapped in InsetDrawable, actually) with a white rectangle, round corners (20dp radius), and a black shadow blurred by 20dp and offset by (2dp; 3dp).

Keep in mind that it will draw out of bounds, so clipChildren=false on parent layout is required.

Dynamic shadow

LayerDrawable(arrayOf(
    RectShadow(dp(20), ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK)),
    RoundRectDrawable(Color.WHITE, dp(20)) // explained later
))

RectShadow draws a shadow (out of bounds, remember!) while RoundRectDrawable, well, it draws a round rect.

Now you can modify properties of RectShadow at runtime: .cornerRadius(100500).shadow(nicerShadow)

Inner shadow

LayerDrawable(arrayOf(
    RoundRectDrawable(Color.WHITE, dp(20)), // explained later
    RectInnerShadow(dp(20), ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK))
))

Add Inner, make it draw after round rect, and that's it: inner shadow, known as inset in CSS. Interface is the same.

Dafuq is RoundRectDrawable?

It's just standard GradientDrawable (a.k.a. <shape>):

fun RoundRectDrawable(@ColorInt color: Int, @Px radius: Int): Drawable =
    GradientDrawable().apply {
        setColor(color)
        setCornerRadius(radius)
    }

ItemDecoration for RecyclerView

You may want to create a RecyclerView, set clipChildren=false, and set drawables with shadow as item backgrounds. This will work 99% of time but fail miserably during item animations: when alpha < 1, clipChildren becomes effectively true because of intermediate buffer which has its bounds, so you will have your shadows clipped.

The saviour is RectItemsWithShadows(rect, shadow) item decorator with an already familiar constructor. Individual item properties are controllable and animatable:

itemView.stateListAnimator = StateListAnimator().apply {
    addState(intArrayOf(android.R.attr.state_selected),
        ObjectAnimator.ofPropertyValuesHolder(null as Any?,
            PropertyValuesHolder.ofFloat(DECOR_SHADOW_RADIUS, dp(32f)),
            PropertyValuesHolder.ofInt(DECOR_SHADOW_COLOR, 0xFF_AAFFCC.toInt()).argb(),
            PropertyValuesHolder.ofInt(DECOR_RECT_FILL_COLOR, 0xFF_AAFFCC.toInt()).argb(),
        )
    )
    addState(intArrayOf(),
        ObjectAnimator.ofPropertyValuesHolder(null as Any?,
            PropertyValuesHolder.ofFloat(DECOR_SHADOW_RADIUS, dp(8f)),
            PropertyValuesHolder.ofInt(DECOR_SHADOW_COLOR, 0x66_000000).argb(),
            PropertyValuesHolder.ofInt(DECOR_RECT_FILL_COLOR, Color.WHITE).argb(),
        )
    )
}

private val argbEvaluator = ArgbEvaluator()
fun PropertyValuesHolder.argb(): PropertyValuesHolder = apply { setEvaluator(argbEvaluator) }

Some shadows

fiftyshades's People

Contributors

miha-x64 avatar

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.