我们知道,Google 在 2014 年 I/O
大会上发布的一种新的设计规范——Material Design,这种设计规范给 Android UI 设计带来了很多的变化。比如,更加强调真实性、有立体感,由此引发的一系列针对阴影的UI设计。我相信,很多专注业务逻辑的Android程序员,拿着这样的一个UI效果,往往一头雾水。所以,我仅在此抛砖引玉,希望能够打开思路,更好的满足UI的要求。
好的,不多说废话,下面先来看看几种实现阴影的方式,先有个直观印象,后面挨个说明(文末有几种实现方式的效果对比图):
这种方式稍微复杂点,不过对shape熟悉的话,那也是分分钟的事。
-
-
-
-
-
-
这个方式算是比较简单,和自定义的drawable差不多,UI切好图放做背景即可,就不多说了。
相比自定义的drawable,我们只是把作图的行为放到了UI切图部分而已,整体工作量并未减少。
这是最简单的实现方式,只需要xml布局中的一个elevation属性就可以做到,如果要做作动画阴影效果,可以使用TranslationZ动态海拔高度偏移高度,是一个偏移的距离。Z轴有如下公式:
Z = elevation + translationZ
shadow_background_1.xml文件存放路径:res--drawable--shadow_background_1.xml,代码如下:
不过使用Z轴实现阴影有几点需要注意:
PS:使用完全一样的View,在界面不同位置,阴影不一样。相当于在Y为0,正上方Z轴的光源,向左、向右、向下照射而形成的阴影。
总的来说: Android原生实现的阴影是比较好的,推荐使用。但是有些特殊UI效果需要改变阴影位置和颜色、透明度,Z轴的方式目前是做不到(当然API28可以一试),所以需要更多的方式来实现阴影。
我们知道CardView可以实现圆角和阴影,当然CardView不支持改变颜色、位置,因为实现方式的原因,如下见解如有误还望读者提出:
API21之后实现应该不用设置layertype,默认硬件加速就可以,因为使用的Z轴实现。
具体实现就不贴出了,有如下属性可以使用:
这种方式是顺着CardView的思路去实现的,因为CardView没有暴露出颜色、位置的API,那我们就可以自己去参照实现,依赖View提供的API——setShadowLayer()。
这种方式稍微复杂些,会涉及到自定义属性,自定义ViewGroup,但写好了就可以一劳永逸,值得试试。效果如图:
(1)自定义ShadowContainer文件,继承自ViewGroup:
package com.tcl.uicompat.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.tcl.uicompat.R;
/**
* Created by Agg on 2020/03/05.
* Tips: Implemented based on packaging.
* Reference: https://github.com/cjlemon/Shadow
*/
public class ShadowContainer extends ViewGroup {
private final float deltaLength;
private final float cornerRadius;
private final Paint mShadowPaint;
private boolean drawShadow;
public ShadowContainer(Context context) {
this(context, null);
}
public ShadowContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShadowContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShadowContainer);
int shadowColor = a.getColor(R.styleable.ShadowContainer_containerShadowColor, Color.RED);
float shadowRadius = a.getDimension(R.styleable.ShadowContainer_containerShadowRadius, 0);
deltaLength = a.getDimension(R.styleable.ShadowContainer_containerDeltaLength, 0);
cornerRadius = a.getDimension(R.styleable.ShadowContainer_containerCornerRadius, 0);
float dx = a.getDimension(R.styleable.ShadowContainer_deltaX, 0);
float dy = a.getDimension(R.styleable.ShadowContainer_deltaY, 0);
drawShadow = a.getBoolean(R.styleable.ShadowContainer_enable, true);
a.recycle();
mShadowPaint = new Paint();
mShadowPaint.setStyle(Paint.Style.FILL);
mShadowPaint.setAntiAlias(true);
mShadowPaint.setColor(shadowColor);
mShadowPaint.setShadowLayer(shadowRadius, dx, dy, shadowColor);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (drawShadow) {
/*
setShadowLayer()/setMaskFilter is not support hardware acceleration, so using LAYER_TYPE_SOFTWARE, but software layers isn't always good.
LAYER_TYPE_SOFTWARE: software layers should be avoided when the affected view tree updates often.
*/
if (getLayerType() != LAYER_TYPE_SOFTWARE) {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
View child = getChildAt(0);
int left = child.getLeft();
int top = child.getTop();
int right = child.getRight();
int bottom = child.getBottom();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius, mShadowPaint);
} else {
Path drawablePath = new Path();
drawablePath.moveTo(left + cornerRadius, top);
drawablePath.arcTo(new RectF(left, top, left + 2 * cornerRadius, top + 2 * cornerRadius), -90, -90, false);
drawablePath.lineTo(left, bottom - cornerRadius);
drawablePath.arcTo(new RectF(left, bottom - 2 * cornerRadius, left + 2 * cornerRadius, bottom), 180, -90, false);
drawablePath.lineTo(right - cornerRadius, bottom);
drawablePath.arcTo(new RectF(right - 2 * cornerRadius, bottom - 2 * cornerRadius, right, bottom), 90, -90, false);
drawablePath.lineTo(right, top + cornerRadius);
drawablePath.arcTo(new RectF(right - 2 * cornerRadius, top, right, top + 2 * cornerRadius), 0, -90, false);
drawablePath.close();
canvas.drawPath(drawablePath, mShadowPaint);
}
}
super.dispatchDraw(canvas);
}
/**
* setMeasuredDimension(): store the modified width and modified height.
*
* @param widthMeasureSpec the original width
* @param heightMeasureSpec the original height
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() != 1) {
throw new IllegalStateException("Child View can have only one!!!");
}
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
View child = getChildAt(0);
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
int childBottomMargin = (int) (Math.max(deltaLength, layoutParams.bottomMargin) + 1);
int childLeftMargin = (int) (Math.max(deltaLength, layoutParams.leftMargin) + 1);
int childRightMargin = (int) (Math.max(deltaLength, layoutParams.rightMargin) + 1);
int childTopMargin = (int) (Math.max(deltaLength, layoutParams.topMargin) + 1);
int widthMeasureSpecMode;
int widthMeasureSpecSize;
int heightMeasureSpecMode;
int heightMeasureSpecSize;
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthMeasureSpecMode = MeasureSpec.UNSPECIFIED;
widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec);
} else {
if (layoutParams.width == MarginLayoutParams.MATCH_PARENT) {
widthMeasureSpecMode = MeasureSpec.EXACTLY;
widthMeasureSpecSize = measuredWidth - childLeftMargin - childRightMargin;
} else if (MarginLayoutParams.WRAP_CONTENT == layoutParams.width) {
widthMeasureSpecMode = MeasureSpec.AT_MOST;
widthMeasureSpecSize = measuredWidth - childLeftMargin - childRightMargin;
} else {
widthMeasureSpecMode = MeasureSpec.EXACTLY;
widthMeasureSpecSize = layoutParams.width;
}
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightMeasureSpecMode = MeasureSpec.UNSPECIFIED;
heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec);
} else {
if (layoutParams.height == MarginLayoutParams.MATCH_PARENT) {
heightMeasureSpecMode = MeasureSpec.EXACTLY;
heightMeasureSpecSize = measuredHeight - childBottomMargin - childTopMargin;
} else if (MarginLayoutParams.WRAP_CONTENT == layoutParams.height) {
heightMeasureSpecMode = MeasureSpec.AT_MOST;
heightMeasureSpecSize = measuredHeight - childBottomMargin - childTopMargin;
} else {
heightMeasureSpecMode = MeasureSpec.EXACTLY;
heightMeasureSpecSize = layoutParams.height;
}
}
measureChild(child, MeasureSpec.makeMeasureSpec(widthMeasureSpecSize, widthMeasureSpecMode), MeasureSpec.makeMeasureSpec(heightMeasureSpecSize, heightMeasureSpecMode));
int parentWidthMeasureSpec = MeasureSpec.getMode(widthMeasureSpec);
int parentHeightMeasureSpec = MeasureSpec.getMode(heightMeasureSpec);
int height = measuredHeight;
int width = measuredWidth;
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
if (parentHeightMeasureSpec == MeasureSpec.AT_MOST) {
height = childHeight + childTopMargin + childBottomMargin;
}
if (parentWidthMeasureSpec == MeasureSpec.AT_MOST) {
width = childWidth + childRightMargin + childLeftMargin;
}
if (width < childWidth + 2 * deltaLength) {
width = (int) (childWidth + 2 * deltaLength);
}
if (height < childHeight + 2 * deltaLength) {
height = (int) (childHeight + 2 * deltaLength);
}
if (height != measuredHeight || width != measuredWidth) {
setMeasuredDimension(width, height);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View child = getChildAt(0);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
int childMeasureWidth = child.getMeasuredWidth();
int childMeasureHeight = child.getMeasuredHeight();
child.layout((measuredWidth - childMeasureWidth) / 2, (measuredHeight - childMeasureHeight) / 2, (measuredWidth + childMeasureWidth) / 2, (measuredHeight + childMeasureHeight) / 2);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
public void setDrawShadow(boolean drawShadow) {
if (this.drawShadow == drawShadow) {
return;
}
this.drawShadow = drawShadow;
postInvalidate();
}
}
(2)自定义属性,在res--values--attrs.xml中添加如下内容:
(3)layout中使用:
(4)特别注意:
总的来说,5种方式各有利弊,最推荐的还是Android原生的Z轴实现。
随着Android的发展,各种需求越来越多,我们在解决问题的同时需要知道别人解决此问题的原理,这样才能触类旁通。——硬性总结一把……^ - ^