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

前端 未结 7 1462
南旧
南旧 2020-11-27 11:44

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

android:elevation=\"2dp\"

for buttons,

an         


        
7条回答
  •  借酒劲吻你
    2020-11-27 12:28

    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();
    }
    

提交回复
热议问题