天天看點

Android 自定義圓角+陰影布局

效果圖

Android 自定義圓角+陰影布局

為了更友善的實作UI效果圖裡的圓角+陰影,就自定義了布局RoundShadowLayout

邏輯思路

  1. 設定布局的圓角,将子view超出圓角的區域裁剪掉
  2. 設定陰影,不改變子view的大小,将布局大小擴充到可以容納陰影,并調整子view的位置

裁剪實作方案

  1. 使用canvas.clipXXX()方法裁剪畫圖區域(存在鋸齒,不使用)
  2. 使用paint的Xfermode進行處理,獲得需要效果(使用)
  3. 使用ViewOutLineProvider(需要21以上,不能單獨設定某個圓角,不使用)

陰影實作方案

  1. 使用paint.setShadowLayer()方法
               

 主要實作代碼

//設定布局大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 測量子view的最大寬高
    int width = 0;
    int height = 0;
    for (int i=0;i<getChildCount();i++) {
        View child = getChildAt(i);
        //測量子View的寬高,measureChild最終調用child.measure(w,h)
        final ViewGroup.LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec-(int) (shadowRadius)*2, getPaddingLeft() + getPaddingRight(), lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec-(int) (shadowRadius)*2, getPaddingTop() + getPaddingBottom(), lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
        int childWidth = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;
        int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
        width = Math.max(width, childWidth);
        height = Math.max(height, childHeight);
    }
    //如果使用陰影,則寬高加上陰影
    setMeasuredDimension(
        width + getPaddingLeft() + getPaddingRight() + (int) (shadowRadius)*2,
        height + getPaddingTop() + getPaddingBottom() + (int) (shadowRadius)*2
    );
}
           
//添加陰影bitmap,最後将bitmap設定為布局的background
private Bitmap createShadowBitmap(int shadowWidth, int shadowHeight, float shadowRadius,
                                      float dx, float dy, int shadowColor) {
    Bitmap output = Bitmap.createBitmap(shadowWidth, shadowHeight, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(output);

    shadowRect.set(shadowRadius,shadowRadius,shadowWidth - shadowRadius,shadowHeight - shadowRadius);

    if (dy > 0) {
        shadowRect.top += dy;
        shadowRect.bottom -= dy;
    } else if (dy < 0) {
        shadowRect.top += Math.abs(dy);
        shadowRect.bottom -= Math.abs(dy);
    }

    if (dx > 0) {
        shadowRect.left += dx;
        shadowRect.right -= dx;
    } else if (dx < 0) {
        shadowRect.left += Math.abs(dx);
        shadowRect.right -= Math.abs(dx);
    }

    shadowPaint.setAntiAlias(true);
    shadowPaint.setStyle(Paint.Style.FILL);
    shadowPaint.setColor(shadowColor);
    if (!isInEditMode()) {
        shadowPaint.setShadowLayer(shadowRadius, dx, dy, shadowColor);
    }
    shadowPath.reset();
    shadowPath.addRoundRect(shadowRect, radiusArray, Path.Direction.CW);
    canvas.drawPath(shadowPath, shadowPaint);

    return output;
}
           
//裁剪圓角區域
private void clipRound(Canvas canvas) {
    roundPath.reset();
    roundPath.addRoundRect(roundRect, radiusArray, Path.Direction.CW);
    //畫筆設定
    roundPaint.setColor(Color.WHITE);
    roundPaint.setAntiAlias(true);
    roundPaint.setStyle(Paint.Style.FILL);
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
        roundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawPath(roundPath, roundPaint);
    } else {
        roundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        final Path path = new Path();
        path.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW);
        path.op(roundPath, Path.Op.DIFFERENCE);
        canvas.drawPath(path, roundPaint);
    }
}
           

源代碼https://github.com/wudengwei/RoundShadowLayout

參考 rclayout、shadow-layout