How to implement the Material-design Elevation for Pre-lollipop

50.4k views Asked by At

Google has shown some nice ways that elevation effect are shown on Lollipop here.

android:elevation="2dp"

for buttons,

android:stateListAnimator="@anim/button_state_list_animator"

How can I mimic the elevation effect on pre-Lollipop versions without 3rd party library?

7

There are 7 answers

2
Ranjithkumar On BEST ANSWER

You can mimic the elevation on pre-Lollipop with a official method.

I achieve same effect using,

  android:background="@android:drawable/dialog_holo_light_frame"

My tested output:

enter image description here

reference - https://stackoverflow.com/a/25683148/3879847

Thanks to user @Repo..

Update : If you want change color of this drawable try @Irfan answer below ↓

https://stackoverflow.com/a/40815944/3879847

0
Gabriele Mariotti On

You can't mimic the elevation on pre-Lollipop with a official method.

You can use some drawables to make the shadow in your component. Google uses this way in CardView for example.

The ViewCompat.setElevation(View, int) currently creates the shadow only on API21+. If you check the code behind, this method calls:

API 21+:

  @Override
    public void setElevation(View view, float elevation) {
        ViewCompatLollipop.setElevation(view, elevation);
    }

API < 21

@Override
public void setElevation(View view, float elevation) {

}
1
Zielony On

To bring dynamic, animated shadows to pre-Lollipop devices you have to:

  1. Draw a black shape of a view to a bitmap
  2. Blur that shape using elevation as a radius. You can do that using RenderScript. It's not exactly the method Lollipop's using, but gives good results and is easy to add to existing views.
  3. Draw that blurred shape beneath the view. Probably the best place is the drawChild method. You also have to override setElevation and setTranslationZ, override child view drawing in layouts, turn off clip-to-padding and implement state animators.

enter image description here

That's a lot of work, but it gives the best looking, dynamic shadows with response animations. I'm not sure why you'd like to achieve that without third party libraries. If you wish, you can analyze sources of Carbon and port the parts you'd like to have in your app:

Shadow generation

private static void blurRenderScript(Bitmap bitmap, float radius) {
    Allocation inAllocation = Allocation.createFromBitmap(renderScript, bitmap,
            Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
    Allocation outAllocation = Allocation.createTyped(renderScript, inAllocation.getType());

    blurShader.setRadius(radius);
    blurShader.setInput(inAllocation);
    blurShader.forEach(outAllocation);

    outAllocation.copyTo(bitmap);
}

public static Shadow generateShadow(View view, float elevation) {
    if (!software && renderScript == null) {
        try {
            renderScript = RenderScript.create(view.getContext());
            blurShader = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
        } catch (RSRuntimeException ignore) {
            software = true;
        }
    }

    ShadowView shadowView = (ShadowView) view;
    CornerView cornerView = (CornerView) view;
    boolean isRect = shadowView.getShadowShape() == ShadowShape.RECT ||
            shadowView.getShadowShape() == ShadowShape.ROUND_RECT && cornerView.getCornerRadius() < view.getContext().getResources().getDimension(R.dimen.carbon_1dip) * 2.5;

    int e = (int) Math.ceil(elevation);
    Bitmap bitmap;
    if (isRect) {
        bitmap = Bitmap.createBitmap(e * 4 + 1, e * 4 + 1, Bitmap.Config.ARGB_8888);

        Canvas shadowCanvas = new Canvas(bitmap);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(0xff000000);

        shadowCanvas.drawRect(e, e, e * 3 + 1, e * 3 + 1, paint);

        blur(bitmap, elevation);

        return new NinePatchShadow(bitmap, elevation);
    } else {
        bitmap = Bitmap.createBitmap((int) (view.getWidth() / SHADOW_SCALE + e * 2), (int) (view.getHeight() / SHADOW_SCALE + e * 2), Bitmap.Config.ARGB_8888);

        Canvas shadowCanvas = new Canvas(bitmap);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(0xff000000);

        if (shadowView.getShadowShape() == ShadowShape.ROUND_RECT) {
            roundRect.set(e, e, (int) (view.getWidth() / SHADOW_SCALE - e), (int) (view.getHeight() / SHADOW_SCALE - e));
            shadowCanvas.drawRoundRect(roundRect, e, e, paint);
        } else {
            int r = (int) (view.getWidth() / 2 / SHADOW_SCALE);
            shadowCanvas.drawCircle(r + e, r + e, r, paint);
        }

        blur(bitmap, elevation);

        return new Shadow(bitmap, elevation);
    }
}

Drawing a view with a shadow

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    if (!child.isShown())
        return super.drawChild(canvas, child, drawingTime);

    if (!isInEditMode() && child instanceof ShadowView && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) {
        ShadowView shadowView = (ShadowView) child;
        Shadow shadow = shadowView.getShadow();
        if (shadow != null) {
            paint.setAlpha((int) (ShadowGenerator.ALPHA * ViewHelper.getAlpha(child)));

            float childElevation = shadowView.getElevation() + shadowView.getTranslationZ();

            float[] childLocation = new float[]{(child.getLeft() + child.getRight()) / 2, (child.getTop() + child.getBottom()) / 2};
            Matrix matrix = carbon.internal.ViewHelper.getMatrix(child);
            matrix.mapPoints(childLocation);

            int[] location = new int[2];
            getLocationOnScreen(location);
            float x = childLocation[0] + location[0];
            float y = childLocation[1] + location[1];
            x -= getRootView().getWidth() / 2;
            y += getRootView().getHeight() / 2;   // looks nice
            float length = (float) Math.sqrt(x * x + y * y);

            int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
            canvas.translate(
                    x / length * childElevation / 2,
                    y / length * childElevation / 2);
            canvas.translate(
                    child.getLeft(),
                    child.getTop());

            canvas.concat(matrix);
            canvas.scale(ShadowGenerator.SHADOW_SCALE, ShadowGenerator.SHADOW_SCALE);
            shadow.draw(canvas, child, paint);
            canvas.restoreToCount(saveCount);
        }
    }

    if (child instanceof RippleView) {
        RippleView rippleView = (RippleView) child;
        RippleDrawable rippleDrawable = rippleView.getRippleDrawable();
        if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless) {
            int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
            canvas.translate(
                    child.getLeft(),
                    child.getTop());
            rippleDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }

    return super.drawChild(canvas, child, drawingTime);
}

Elevation API backported to pre-Lollipop

private float elevation = 0;
private float translationZ = 0;
private Shadow shadow;

@Override
public float getElevation() {
    return elevation;
}

public synchronized void setElevation(float elevation) {
    if (elevation == this.elevation)
        return;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        super.setElevation(elevation);
    this.elevation = elevation;
    if (getParent() != null)
        ((View) getParent()).postInvalidate();
}

@Override
public float getTranslationZ() {
    return translationZ;
}

public synchronized void setTranslationZ(float translationZ) {
    if (translationZ == this.translationZ)
        return;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        super.setTranslationZ(translationZ);
    this.translationZ = translationZ;
    if (getParent() != null)
        ((View) getParent()).postInvalidate();
}

@Override
public ShadowShape getShadowShape() {
    if (cornerRadius == getWidth() / 2 && getWidth() == getHeight())
        return ShadowShape.CIRCLE;
    if (cornerRadius > 0)
        return ShadowShape.ROUND_RECT;
    return ShadowShape.RECT;
}

@Override
public void setEnabled(boolean enabled) {
    super.setEnabled(enabled);
    setTranslationZ(enabled ? 0 : -elevation);
}

@Override
public Shadow getShadow() {
    float elevation = getElevation() + getTranslationZ();
    if (elevation >= 0.01f && getWidth() > 0 && getHeight() > 0) {
        if (shadow == null || shadow.elevation != elevation)
            shadow = ShadowGenerator.generateShadow(this, elevation);
        return shadow;
    }
    return null;
}

@Override
public void invalidateShadow() {
    shadow = null;
    if (getParent() != null && getParent() instanceof View)
        ((View) getParent()).postInvalidate();
}
0
random On

Create a 9-patch image with stretchable patches defined on an image with shadow around it.

enter image description here

Add this 9-patch image as a background of your button with a padding so that the shadow is visible.

You can find some pre-defined 9-patch (.9.png) images here or here from where you can select, customize and copy to your project's drawable.

0
Steven Mark Ford On

You can either hack it using a card-view:

<android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/btnGetStuff"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    card_view:cardCornerRadius="4dp"
    card_view:cardBackgroundColor="@color/accent"
    >
    <!-- you could also add image view here for icon etc. -->
    <TextView
        android:id="@+id/txtGetStuff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="@dimen/textSize_small"
        android:textColor="@color/primary_light"
        android:freezesText="true"
        android:text="Get Stuff"
        android:maxWidth="120dp"
        android:singleLine="true"
        android:ellipsize="end"
        android:maxLines="1"
        /></android.support.v7.widget.CardView>

Or look at using this third party library: https://github.com/rey5137/Material (see wiki article on button https://github.com/rey5137/Material/wiki/Button)

0
Aman Verma On

u can easily simulate it by declaring a drawable like this -

shadow.xml

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"

    >

    <gradient android:type="linear" android:angle="270" android:startColor="#b6b6b6" android:endColor="#ffffff"/>


</shape>

and use it int ur main xml like -

 android:background="@drawable/shadow"
0
Irfan On

to add @Ranjith Kumar answer

To add background color to the drawable (example button background color), we need to get drawable programatically.

first get the drawable

Drawable drawable = getResources().getDrawable(android.R.drawable.dialog_holo_light_frame);

set the color

drawable.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.color_primary), PorterDuff.Mode.MULTIPLY));

then set it to the view.

view.setBackgroundDrawable(drawable);

in case anyone searching.