在低版本中讓按鈕顯示陰影

在一些情況需要讓button顯示陰影表示懸空的狀態,在Android L 以上有 elevation屬性可以使用,低版本就需要自己畫陰影來表示懸浮狀態。對於一個按鈕一般只要支持圓角矩形就可以滿足需求了(圓型按鈕可以直接使用FloatingActionButton)。

  • 首先自定義View RoundRecButton 繼承自Button:

    public class RoundRecButton extends Button
  • 對於自定義的RoundRecButton 添加兩個自定義屬性:

    <declare-styleable name="RoundRecButton">
        <attr name="radius" format="dimension"/>
        <attr name="background_tint" format="color"/>
    </declare-styleable>

    這裏background_tint 其實就是background 沒有做tint的支持,不要被名字誤導了(懶得改了)

  • 在View的3個構造方法中 做初始化工作,分別獲取我們兩個自定義屬性的值,一個圓角的radius 和一個背景顏色(這裏使用 colorStateList 是爲了支持顏色選擇器<selector/>

    private void init(Context context, AttributeSet attributeSet){
    
        TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.RoundRecButton);
        int radius = a.getDimensionPixelSize(R.styleable.RoundRecButton_radius, 0);
        mStateList = a.getColorStateList(R.styleable.RoundRecButton_background_tint);
        a.recycle();
  • 對於 Android L 及以上版本 使用elevation 設置:

    private boolean elevationSupported() {
        return android.os.Build.VERSION.SDK_INT >= 21;
    }
    if (elevationSupported()) {
            mCircle = new ShapeDrawable(new RoundRectShape(new float[]{radius,radius,radius,radius,radius,radius,radius,radius},null,null));
            ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
        } 

    關於ShapeDrawable 就是對應於<shape android:shape="retangle"/>參數說明如下:

    /**
     * RoundRectShape constructor.
     * Specifies an outer (round)rect and an optional inner (round)rect.
     *
     * @param outerRadii An array of 8 radius values, for the outer roundrect. 
     *                   The first two floats are for the 
     *                   top-left corner (remaining pairs correspond clockwise). 
     *                   For no rounded corners on the outer rectangle, 
     *                   pass null.
     * @param inset      A RectF that specifies the distance from the inner 
     *                   rect to each side of the outer rect. 
     *                   For no inner, pass null.
     * @param innerRadii An array of 8 radius values, for the inner roundrect.
     *                   The first two floats are for the 
     *                   top-left corner (remaining pairs correspond clockwise). 
     *                   For no rounded corners on the inner rectangle, 
     *                   pass null.
     *                   If inset parameter is null, this parameter is ignored. 
     */
    public RoundRectShape(float[] outerRadii, RectF inset,
                          float[] innerRadii) {
  • 對於Android L 以下版本使用自定義的RoundRectShadow具體就是在原RoundRecShape 的基礎上在外側繪製陰影:

    private class RoundRectShadow extends RoundRectShape{
        private Paint mShadowPaint;
        private RadialGradient mRadialGradient;
        private RectF mRectF = new RectF();
        private int mRadius;
    
        public RoundRectShadow(float[] outerRadii, RectF inset, float[] innerRadii,int shadowRadius,int radius) {
            super(outerRadii, inset, innerRadii);
            mRadius = radius;
            mShadowPaint = new Paint();
            mShadowRadius = shadowRadius;
            mRadialGradient = new RadialGradient(RoundRecButton.this.getWidth()/2, RoundRecButton.this.getHeight() / 2,
                    mShadowRadius, new int[] {
                    FILL_SHADOW_COLOR, Color.TRANSPARENT
            }, null, Shader.TileMode.CLAMP);
            mShadowPaint.setShader(mRadialGradient);
        }
    
        @Override
        public void draw(Canvas canvas, Paint paint) {
            final int width = RoundRecButton.this.getWidth();
            final int height = RoundRecButton.this.getHeight();
            mRectF.set(0,0,width,height);
            canvas.drawRoundRect(mRectF, mRadius, mRadius, mShadowPaint);
            mRectF.inset(mShadowRadius, mShadowRadius);
            canvas.drawRoundRect(mRectF,mRadius,mRadius,paint);
    
        }
    }

    在Android L 以下版本使用 帶有陰影的自定義Shape:

    if(elevationSupported()){
    /.../
    }else {
            RoundRectShape oval = new RoundRectShadow(new float[]{radius,radius,radius,radius,radius,radius,radius,radius}, null,null,mShadowRadius,radius);
            mCircle = new ShapeDrawable(oval);
            ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, mCircle.getPaint());
            mCircle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
                    KEY_SHADOW_COLOR);
            final int padding = mShadowRadius;
            // set padding so the inner image sits correctly within the shadow.
            setPadding(padding, padding, padding, padding);
        }

    使用ShadowLayer 會模糊邊緣,因爲在shape中只是單純的繪製一層陰影。

  • 現在已經完成對陰影的支持,現在再加入對colorStateList的支持:
    設置初始顏色

    if(mStateList != null) {
            mCircle.getPaint().setColor(mStateList.getColorForState(getDrawableState(),Color.WHITE));
        }else {
            mCircle.getPaint().setColor(Color.WHITE);
        }

    當Drawable 狀態改變時 設置對應的顏色並刷新界面

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if(mStateList != null){
            mCircle.getPaint().setColor(mStateList.getColorForState(getDrawableState(),Color.WHITE));
            invalidate();
        }
    }
  • 之後再加入對RippleDrawable的支持(由於只是支持的顏色設置,所以這裏都是需要自己用Drawable包裝,其實可以稍作修改直接支持Drawable 就好了):

    if(Build.VERSION.SDK_INT >= 21){
            TypedArray a1 = context.obtainStyledAttributes(new int[]{android.R.attr.colorControlHighlight});
            ColorStateList colorStateList = a1.getColorStateList(0);
            a1.recycle();
            if(colorStateList != null) {
                setBackground(new RippleDrawable(colorStateList, mCircle, null));
            }
        }else {
            setBackgroundDrawable(mCircle);
        }

    總結

    其實關鍵就是使用自定義的Shape進行陰影支持,如果直接進行setBackground,那就連狀態選擇及Ripple等都不用做處理了。源碼如下:

    public class RoundRecButton extends Button {
    private static final int KEY_SHADOW_COLOR = 0x1E000000;
    private static final int FILL_SHADOW_COLOR = 0x3D000000;
    
    private static final float X_OFFSET = 0f;
    private static final float Y_OFFSET = 1.75f;
    private static final float SHADOW_RADIUS = 3.5f;
    private static final int SHADOW_ELEVATION = 4;
    
    private int mShadowRadius;
    
    private ColorStateList mStateList;
    private ShapeDrawable mCircle;
    public RoundRecButton(Context context) {
        super(context);
        init(context,null);
    }
    
    public RoundRecButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
    }
    
    public RoundRecButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);
    }
    
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if(mStateList != null){
            mCircle.getPaint().setColor(mStateList.getColorForState(getDrawableState(),Color.WHITE));
            invalidate();
        }
    }
    
    private void init(Context context, AttributeSet attributeSet){
        TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.RoundRecButton);
        int radius = a.getDimensionPixelSize(R.styleable.RoundRecButton_radius, 0);
        mStateList = a.getColorStateList(R.styleable.RoundRecButton_background_tint);
        a.recycle();
        final float density = getContext().getResources().getDisplayMetrics().density;
        final int shadowYOffset = (int) (density * Y_OFFSET);
        final int shadowXOffset = (int) (density * X_OFFSET);
        mShadowRadius = (int) (density * SHADOW_RADIUS);
        if (elevationSupported()) {
            mCircle = new ShapeDrawable(new RoundRectShape(new float[]{radius,radius,radius,radius,radius,radius,radius,radius},null,null));
            ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
        } else {
            RoundRectShape oval = new RoundRectShadow(new float[]{radius,radius,radius,radius,radius,radius,radius,radius}, null,null,mShadowRadius,radius);
            mCircle = new ShapeDrawable(oval);
            ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, mCircle.getPaint());
            mCircle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
                    KEY_SHADOW_COLOR);
            final int padding = mShadowRadius;
            // set padding so the inner image sits correctly within the shadow.
            setPadding(padding, padding, padding, padding);
        }
        if(mStateList != null) {
            mCircle.getPaint().setColor(mStateList.getColorForState(getDrawableState(),Color.WHITE));
        }else {
            mCircle.getPaint().setColor(Color.WHITE);
        }
        if(Build.VERSION.SDK_INT >= 21){
            TypedArray a1 = context.obtainStyledAttributes(new int[]{android.R.attr.colorControlHighlight});
            ColorStateList colorStateList = a1.getColorStateList(0);
            a1.recycle();
            if(colorStateList != null) {
                setBackground(new RippleDrawable(colorStateList, mCircle, null));
            }
        }else {
            setBackgroundDrawable(mCircle);
        }
    }
    
    @Override
    public void setBackgroundResource(int resid) {
        super.setBackgroundResource(resid);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!elevationSupported()) {
            setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight()
                    + mShadowRadius*2);
        }
    }
    
    private boolean elevationSupported() {
        return android.os.Build.VERSION.SDK_INT >= 21;
    }
    
    private class RoundRectShadow extends RoundRectShape{
        private Paint mShadowPaint;
        private RadialGradient mRadialGradient;
        private RectF mRectF = new RectF();
        private int mRadius;
        public RoundRectShadow(float[] outerRadii, RectF inset, float[] innerRadii,int shadowRadius,int radius) {
            super(outerRadii, inset, innerRadii);
            mRadius = radius;
            mShadowPaint = new Paint();
            mShadowRadius = shadowRadius;
            mRadialGradient = new RadialGradient(RoundRecButton.this.getWidth()/2, RoundRecButton.this.getHeight() / 2,
                    mShadowRadius, new int[] {
                    FILL_SHADOW_COLOR, Color.TRANSPARENT
            }, null, Shader.TileMode.CLAMP);
            mShadowPaint.setShader(mRadialGradient);
        }
        @Override
        public void draw(Canvas canvas, Paint paint) {
            final int width = RoundRecButton.this.getWidth();
            final int height = RoundRecButton.this.getHeight();
            mRectF.set(0,0,width,height);
            canvas.drawRoundRect(mRectF, mRadius, mRadius, mShadowPaint);
            mRectF.inset(mShadowRadius, mShadowRadius);
            canvas.drawRoundRect(mRectF,mRadius,mRadius,paint);
        }
    }
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章