1.阴影
相关文章
- Android materialDesign 风格阴影改变阴影颜色
- setOutlineAmbientShadowColor 及 setOutlineSpotShadowColor
- 承香墨影 聊聊 Material Design里,阴影的那些事儿!
知识点
- Api Level>=21情况下:
- 直接使用 elevation , translationZ 即可实现阴影.
- 使用其中1个就行,同时使用,则View在Z轴的高度是 elevation + translationZ . 两者效果是累加的.
- elevation,translationZ要生效,对应的View一定要有背景:
- android:background 或 View.setBackground()
- Api Level<21: 无法实现.
- 可是使用.9图模拟阴影效果,且颜色,阴影范围可控制,但是会占用View的尺寸,见 承香墨影
- 在 sdk>=21 情况下,android:elevation 和 android:translationZ才有用,不占用View的大小,但是无法控制阴影颜色.
本地验证
-
Android未提供现成的工具,实现自定义颜色的阴影.
-
开源库使用的是Paint.setShadowLayer方法:
- 在原始View外部套了1个ViewGroup,增加了View的层级;横向和纵向占用尺寸都会增大;
- Paint.setShadowLayer试验下来,颜色的透明度变化太快,只有很窄的范围能看到颜色渐变;
-
使用Shader在特定场景下,实现阴影/颜色渐变效果,更容易控制.
- 更进一步,使用Paint.setXfermode,仅仅截取出'阴影部分',可以解决上层内容区域是半透明情况下,底部的'阴影'冗余显示问题.还未试验.
public class ShaderView extends View { private Paint paint; private Shader shader; int[] colors; float[] stops; public ShaderView(Context context) { this(context, null, 0); } public ShaderView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public ShaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setLayerType(LAYER_TYPE_SOFTWARE, null); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.FILL); colors = new int[]{ Color.parseColor("#000C51CA"), Color.parseColor("#A00C51CA"), Color.parseColor("#500C51CA"), Color.parseColor("#000C51CA") }; stops = new float[]{ 0.75F, 0.80F, 0.90F, 1.00F }; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); shader = new RadialGradient(getWidth() / 2.0F, getHeight() / 2.0F, 300.0f, colors, stops, Shader.TileMode.CLAMP); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setShader(shader); //经试验,同时使用Shader和setShadowLayer,setShadowLayer无效果 //paint.setShadowLayer(40,0,0,Color.parseColor("#040C51CA")); canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300.0F, paint); paint.setShader(null); paint.setColor(Color.parseColor("#FF0C51CA")); canvas.drawCircle(getWidth() / 2, getHeight() / 2 - 300.0F * 0.20F, 300.0F, paint); } }- 直接继承ImageView,添加部分属性,实现'上,下,左,右'四个位置绘制阴影,可以调整阴影初始透明度及阴影宽度与实际内容区域的比例.也可以添加'角度'属性,可以围绕实际内容区域任意角度绘制阴影.
public class RoundShadowImageView extends AppCompatImageView { private Paint paint; private Shader shader; int[] colors; float[] stops; private float shadowRatio = 0.20F; private float shadowRadius = 0.0F; private boolean shadowRatioViaHeight = true; private boolean shadowOnEnd = true; private boolean shadowOnBottom = true; private @IntRange(from = 1, to = 255) int shadowStartAlpha = 255; *** }
RoundShadowImageView
-
自定义阴影宽度相对于内容区域半径的比例
-
自定义阴影中心相对于内容区域中心的角度,以内容区域垂直向下为0度/起始角度
-
自定义阴影颜色
-
自定义阴影颜色初始透明度
-
自定义阴影位置
-
attr属性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RoundShadowImageView"> <!--阴影宽度相对于内容区域半径的比例--> <attr name="shadowRatio" format="float" /> <!--阴影中心相对于内容区域中心的角度,以内容区域垂直向下为0度/起始角度--> <attr name="shadowCircleAngle" format="float" /> <!--阴影颜色--> <attr name="shadowColor" format="color|reference" /> <!--阴影颜色初始透明度--> <attr name="shadowStartAlpha" format="float" /> <!--阴影位置--> <attr name="shadowPosition" format="enum"> <enum name="start" value="1" /> <enum name="top" value="2" /> <enum name="end" value="3" /> <enum name="bottom" value="4" /> </attr> </declare-styleable> </resources> -
Java源码
import static com.huanhailiuxin.jet2020.othertest.shadow.ShadowPosition.BOTTOM; import static com.huanhailiuxin.jet2020.othertest.shadow.ShadowPosition.END; import static com.huanhailiuxin.jet2020.othertest.shadow.ShadowPosition.START; import static com.huanhailiuxin.jet2020.othertest.shadow.ShadowPosition.TOP; @IntDef({ START, TOP, END, BOTTOM }) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.FIELD, ElementType.PARAMETER}) @interface ShadowPosition { int START = 1; int TOP = 2; int END = 3; int BOTTOM = 4; } /** * @author HuanHaiLiuXin * @github https://github.com/HuanHaiLiuXin * @date 2020/11/23 */ public class RoundShadowImageView extends AppCompatImageView { private Paint paint; private Shader shader; int[] colors; float[] stops; private float contentSize; @FloatRange(from = 0.0F, to = 1.0F) private float shadowRatio = 0.30F; private float shadowRadius = 0.0F; private float shadowCenterX, shadowCenterY; @ShadowPosition private int shadowPosition = ShadowPosition.BOTTOM; private float shadowCircleAngle = 0F; private boolean useShadowCircleAngle = false; private int red, green, blue; private int shadowColor = Color.RED; private @FloatRange(from = 0F, to = 1F) float shadowStartAlpha = 0.5F; private boolean isLtr = true; public RoundShadowImageView(Context context) { this(context, null, 0); } public RoundShadowImageView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public RoundShadowImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); } private void initAttrs(Context context, @Nullable AttributeSet attrs) { setLayerType(LAYER_TYPE_SOFTWARE, null); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.FILL); isLtr = getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundShadowImageView); shadowRatio = typedArray.getFloat(R.styleable.RoundShadowImageView_shadowRatio, shadowRatio); shadowCircleAngle = typedArray.getFloat(R.styleable.RoundShadowImageView_shadowCircleAngle, shadowCircleAngle); if (shadowCircleAngle > 0F) { useShadowCircleAngle = true; } if (!useShadowCircleAngle) { shadowPosition = typedArray.getInt(R.styleable.RoundShadowImageView_shadowPosition, shadowPosition); } shadowColor = typedArray.getColor(R.styleable.RoundShadowImageView_shadowColor, shadowColor); gainRGB(); shadowStartAlpha = typedArray.getFloat(R.styleable.RoundShadowImageView_shadowStartAlpha, shadowStartAlpha); typedArray.recycle(); } } private void gainRGB() { red = Color.red(shadowColor); green = Color.green(shadowColor); blue = Color.blue(shadowColor); } private void gainShadowCenterAndShader() { gainShadowCenter(); gainShader(); } private void gainShadowCenter() { shadowRadius = contentSize / 2F; if (useShadowCircleAngle) { double radians = Math.toRadians(shadowCircleAngle + 90); shadowCenterX = (float) (getWidth() / 2 + Math.cos(radians) * shadowRadius * shadowRatio); shadowCenterY = (float) (getHeight() / 2 + Math.sin(radians) * shadowRadius * shadowRatio); } else { switch (shadowPosition) { case ShadowPosition.START: if (isLtr) { shadowCenterX = getWidth() / 2 - shadowRadius * shadowRatio; } else { shadowCenterX = getWidth() / 2 + shadowRadius * shadowRatio; } shadowCenterY = getHeight() / 2; break; case ShadowPosition.TOP: shadowCenterY = getHeight() / 2 - shadowRadius * shadowRatio; shadowCenterX = getWidth() / 2; break; case ShadowPosition.END: if (isLtr) { shadowCenterX = getWidth() / 2 + shadowRadius * shadowRatio; } else { shadowCenterX = getWidth() / 2 - shadowRadius * shadowRatio; } shadowCenterY = getHeight() / 2; break; case ShadowPosition.BOTTOM: shadowCenterY = getHeight() / 2 + shadowRadius * shadowRatio; shadowCenterX = getWidth() / 2; break; default: shadowCenterY = getHeight() / 2 + shadowRadius * shadowRatio; shadowCenterX = getWidth() / 2; break; } } } private void gainShader() { colors = new int[]{ Color.TRANSPARENT, Color.argb((int) (shadowStartAlpha * 255), red, green, blue), Color.argb((int) (shadowStartAlpha * 255 / 2), red, green, blue), Color.argb(0, red, green, blue) }; stops = new float[]{ (1F - shadowRatio) * 0.95F, 1F - shadowRatio, 1F - shadowRatio * 0.50F, 1F }; shader = new RadialGradient(shadowCenterX, shadowCenterY, shadowRadius, colors, stops, Shader.TileMode.CLAMP); } private void contentSizeChanged() { contentSize = Math.min(getWidth(), getHeight()) / (1 + this.shadowRatio); setPadding((int) (getWidth() - contentSize) / 2, (int) (getHeight() - contentSize) / 2, (int) (getWidth() - contentSize) / 2, (int) (getHeight() - contentSize) / 2); gainShadowCenterAndShader(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); contentSizeChanged(); } public void setShadowRatio(@FloatRange(from = 0.0F, to = 1.0F) float shadowRatio) { shadowRatio = shadowRatio % 1F; if (shadowRatio != this.shadowRatio) { this.shadowRatio = shadowRatio; contentSizeChanged(); invalidate(); } } public void setShadowColor(@ColorInt int shadowColor) { if (shadowColor != this.shadowColor) { this.shadowColor = shadowColor; gainRGB(); gainShader(); invalidate(); } } public void setShadowStartAlpha(@FloatRange(from = 0F, to = 1F) float shadowStartAlpha) { shadowStartAlpha = shadowStartAlpha % 1F; if (shadowStartAlpha != this.shadowStartAlpha) { this.shadowStartAlpha = shadowStartAlpha; gainShader(); invalidate(); } } public void setShadowCircleAngle(float shadowCircleAngle) { shadowCircleAngle = Math.abs(shadowCircleAngle) % 360.0F; if (shadowCircleAngle != this.shadowCircleAngle) { this.shadowCircleAngle = shadowCircleAngle; if (this.shadowCircleAngle > 0F) { useShadowCircleAngle = true; } gainShadowCenterAndShader(); invalidate(); } } public void setShadowPosition(@ShadowPosition int shadowPosition){ if(useShadowCircleAngle || shadowPosition != this.shadowPosition){ useShadowCircleAngle = false; this.shadowPosition = shadowPosition; gainShadowCenterAndShader(); invalidate(); } } public float getShadowRatio() { return shadowRatio; } public float getShadowCircleAngle() { return shadowCircleAngle; } public int getShadowColor() { return shadowColor; } public float getShadowStartAlpha() { return shadowStartAlpha; } public int getShadowPosition() { return shadowPosition; } @Override protected void onDraw(Canvas canvas) { paint.setShader(shader); canvas.drawCircle(shadowCenterX, shadowCenterY, shadowRadius, paint); paint.setShader(null); super.onDraw(canvas); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); boolean newLtr = getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; if (newLtr != isLtr) { this.isLtr = newLtr; gainShadowCenterAndShader(); invalidate(); } } } -
使用
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".othertest.shadow.ShadowActivity" android:fillViewport="true" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="30dp" android:background="@android:color/white" > <com.huanhailiuxin.jet2020.othertest.shadow.RoundShadowImageView android:id="@+id/shadowImageView" android:layout_width="700px" android:layout_height="800px" android:src="@drawable/ic_smile" app:shadowColor="#FFCD00" app:shadowStartAlpha="0.7" /> <androidx.appcompat.widget.AppCompatButton android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="调整阴影位置" android:onClick="modifyShadowPosition" /> <androidx.appcompat.widget.AppCompatButton android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="旋转阴影" android:onClick="rotateTheShadow" /> <androidx.appcompat.widget.AppCompatButton android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="调整阴影尺寸比例" android:onClick="modifyShadowRatio" /> <androidx.appcompat.widget.AppCompatButton android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="调整阴影起始透明度" android:onClick="modifyShadowStartAlpha" /> </LinearLayout> </ScrollView>public class ShadowActivity extends BaseActivity { private RoundShadowImageView shadowImageView; ObjectAnimator animator; private float shadowCircleAngle,shadowRatio,shadowStartAlpha; private @ShadowPosition int shadowPosition; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shadow); setTitle("试验阴影"); shadowImageView = findViewById(R.id.shadowImageView); shadowCircleAngle = shadowImageView.getShadowCircleAngle(); shadowRatio = shadowImageView.getShadowRatio(); shadowStartAlpha = shadowImageView.getShadowStartAlpha(); shadowPosition = shadowImageView.getShadowPosition(); } private void reset(){ shadowImageView.setShadowPosition(shadowPosition); shadowImageView.setShadowCircleAngle(shadowCircleAngle); shadowImageView.setShadowRatio(shadowRatio); shadowImageView.setShadowStartAlpha(shadowStartAlpha); } private void initAnimator(){ animator.setRepeatCount(ValueAnimator.INFINITE); animator.setRepeatMode(ObjectAnimator.RESTART); animator.setDuration(4000); animator.setInterpolator(new LinearInterpolator()); } private void initAngleAnimator(){ if(animator != null){ animator.cancel(); } animator = ObjectAnimator.ofFloat(shadowImageView,"shadowCircleAngle",0F,360F); initAnimator(); } public void rotateTheShadow(View view) { reset(); initAngleAnimator(); animator.start(); } private void initShadowRatioAnimator(){ if(animator != null){ animator.cancel(); } animator = ObjectAnimator.ofFloat(shadowImageView,"shadowRatio",0.1F,0.5F); initAnimator(); } public void modifyShadowRatio(View view) { reset(); initShadowRatioAnimator(); animator.start(); } private void initShadowStartAlphaAnimator(){ if(animator != null){ animator.cancel(); } animator = ObjectAnimator.ofFloat(shadowImageView,"shadowStartAlpha",0.1F,0.9F); initAnimator(); } public void modifyShadowStartAlpha(View view) { reset(); initShadowStartAlphaAnimator(); animator.start(); } private int shadowPositionIndex = 0; private @ShadowPosition int[] shadowPositions = new int[]{ ShadowPosition.START, ShadowPosition.TOP, ShadowPosition.END, ShadowPosition.BOTTOM }; public void modifyShadowPosition(View view) { if(animator != null){ animator.cancel(); } reset(); int index = shadowPositionIndex ++ % shadowPositions.length; shadowImageView.setShadowPosition(shadowPositions[index]); } }
2.轮廓
相关文章
知识点
- 源码解析:
- 要想通过ViewOutlineProvider生成的Outline绘制阴影,需要调用Outline.setRoundRect, Outline.setOval, Outline.setConvexPath , set(@NonNull Outline src) ,改变 mMode 的默认值(MODE_EMPTY)
- 要想通过ViewOutlineProvider生成的Outline实现View的轮廓裁剪, Outline需要最终调用 setRoundRect.
- View的默认ViewOutlineProvider生成的的Outline可以实现轮廓裁剪.
View.java //View默认的ViewOutlineProvider是ViewOutlineProvider.BACKGROUND ViewOutlineProvider mOutlineProvider = ViewOutlineProvider.BACKGROUND; /** * Sets the {@link ViewOutlineProvider} of the view, which generates the Outline that defines * the shape of the shadow it casts, and enables outline clipping. * <p> * The default ViewOutlineProvider, {@link ViewOutlineProvider#BACKGROUND}, queries the Outline * from the View's background drawable, via {@link Drawable#getOutline(Outline)}. Changing the * outline provider with this method allows this behavior to be overridden. * <p> * If the ViewOutlineProvider is null, if querying it for an outline returns false, * or if the produced Outline is {@link Outline#isEmpty()}, shadows will not be cast. * <p> * Only outlines that return true from {@link Outline#canClip()} may be used for clipping. * * @see #setClipToOutline(boolean) * @see #getClipToOutline() * @see #getOutlineProvider() */ //ViewOutlineProvider用于View的轮廓裁切,并生成定义View阴影形状的Outline实例. //当生成的Outline实例的isEmpty为true,则不能为View绘制阴影. //当生成的Outline实例的canClip为false,则不能为View进行轮廓裁切. public void setOutlineProvider(ViewOutlineProvider provider) { mOutlineProvider = provider; invalidateOutline(); } /** * Sets whether the View's Outline should be used to clip the contents of the View. * <p> * Only a single non-rectangular clip can be applied on a View at any time. * Circular clips from a {@link ViewAnimationUtils#createCircularReveal(View, int, int, float, float) * circular reveal} animation take priority over Outline clipping, and * child Outline clipping takes priority over Outline clipping done by a * parent. * <p> * Note that this flag will only be respected if the View's Outline returns true from * {@link Outline#canClip()}. * * @see #setOutlineProvider(ViewOutlineProvider) * @see #getClipToOutline() */ //设置是否使用View关联的Outline来进行轮廓裁剪. //即使clipToOutline为true,也需要Outline.canClip()fanhuitrue,该方法才会生效. //即生效条件: clipToOutline==true 且 Outline最终调用过setRoundRect. public void setClipToOutline(boolean clipToOutline) { damageInParent(); if (getClipToOutline() != clipToOutline) { mRenderNode.setClipToOutline(clipToOutline); } } ViewOutlineProvider.java //View默认的ViewOutlineProvider,最总调用了setRoundRect 1: Drawable background.getOutline(outline); --> Drawable.java public void getOutline(@NonNull Outline outline) { outline.setRect(getBounds()); outline.setAlpha(0); } --> Outline.java public void setRect(@NonNull Rect rect) { setRect(rect.left, rect.top, rect.right, rect.bottom); } public void setRect(int left, int top, int right, int bottom) { //最终调用了setRoundRect setRoundRect(left, top, right, bottom, 0.0f); } 2: outline.setRect(0, 0, view.getWidth(), view.getHeight()); --> setRoundRect public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { Drawable background = view.getBackground(); if (background != null) { background.getOutline(outline); } else { outline.setRect(0, 0, view.getWidth(), view.getHeight()); outline.setAlpha(0.0f); } } }; Outline.java @Mode //mMode 默认就是 MODE_EMPTY public int mMode = MODE_EMPTY; /** * Returns whether the Outline is empty. * <p> * Outlines are empty when constructed, or if {@link #setEmpty()} is called, * until a setter method is called * * @see #setEmpty() */ //当 mMode 是 MODE_EMPTY.返回true. public boolean isEmpty() { return mMode == MODE_EMPTY; } /** * Returns whether the outline can be used to clip a View. * <p> * Currently, only Outlines that can be represented as a rectangle, circle, * or round rect support clipping. * * @see android.view.View#setClipToOutline(boolean) */ //当 mMode 不是 MODE_CONVEX_PATH.返回true. public boolean canClip() { return mMode != MODE_CONVEX_PATH; } //看 mMode 如何变化: setEmpty: -> mMode = MODE_EMPTY; set(@NonNull Outline src): -> mMode = src.mMode; setRoundRect(int left, int top, int right, int bottom, float radius): -> mMode = MODE_ROUND_RECT; setOval(int left, int top, int right, int bottom): -> mMode = MODE_CONVEX_PATH; setConvexPath(@NonNull Path convexPath): -> mMode = MODE_CONVEX_PATH;