V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
huanhailiuxin
V2EX  ›  分享创造

RoundShadowImageView - 圆形图片的阴影,自由定制!

  •  
  •   huanhailiuxin ·
    HuanHaiLiuXin · 2020-11-25 00:29:54 +08:00 · 549 次点击
    这是一个创建于 1461 天前的主题,其中的信息可能已经有所发展或是发生改变。

    RoundShadowImageView

    RoundShadowImageView 是 1 个为圆形图片的 ImageView 添加阴影的自定义控件.

    GitHub

    RoundShadowImageView

    为什么写这个库

    1. Android 未提供现成的工具,自定义控件阴影的颜色
    2. 开源社区中现有的库,使用了 ViewGroup 包装子 View 的形式,会增加布局层级
    3. 使用 Paint.setShadowLayer,颜色的透明度变化太快,只能在很窄的范围能看到颜色渐变

    RoundShadowImageView 的优势

    1. 不增加布局层级,性能相对更好
    2. 阴影的颜色,初始透明度,位置,相对中心点角度,阴影的显示尺寸 均可自由定制.

    RoundShadowImageView 的局限

    适用范围较窄,仅适用于为圆形图片 ImageView 定制阴影.

    使用步骤:

    步骤 1:

    将源码拷贝至你的项目.

    步骤 2:

    在布局文件中声明,或者直接通过 java 代码创建 RoundShadowImageView 实例.

    步骤 3:

    在 xml 中直接设置其阴影相关属性,或通过 java 方法进行设置.

    示例:

    RoundShadowImageView.gif

    源码:

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

    参考文章


    喜欢的同学点个 star 哈!! RoundShadowImageView

    1 条回复    2020-11-26 09:06:45 +08:00
    Cabana
        1
    Cabana  
       2020-11-26 09:06:45 +08:00 via Android
    正常情况下用 shape 作为 background 然后 setcliptooutline 为 true 可以解决大部分自定义剪裁和阴影问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3514 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 11:23 · PVG 19:23 · LAX 03:23 · JFK 06:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.