Android 自定義氣泡佈局

月光邊境發表於2018-01-19

Demo:

demo.png

功能:

  • 支援4個方向
  • 設定三角形位置
  • 設定陰影顏色,大小
  • 設定圓角
  • 自定義背景顏色

程式碼:

public class BubbleLayout extends FrameLayout {
    public static final int LEFT = 1;
    public static final int TOP = 2;
    public static final int RIGHT = 3;
    public static final int BOTTOM = 4;

    @IntDef({LEFT, TOP, RIGHT, BOTTOM})
    private @interface Direction {
    }

    /**
     * 圓角大小
     */
    private int mRadius;

    /**
     * 三角形的方向
     */
    @Direction
    private int mDirection;

    /**
     * 三角形的底邊中心點
     */
    private Point mDatumPoint;

    /**
     * 三角形位置偏移量(預設居中)
     */
    private int mOffset;

    private Paint mBorderPaint;

    private Path mPath;

    private RectF mRect;

    public BubbleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.BubbleLayout);
        //背景顏色
        int backGroundColor = ta.getColor(R.styleable.BubbleLayout_background_color, Color.WHITE);
        //陰影顏色
        int shadowColor = ta.getColor(R.styleable.BubbleLayout_shadow_color,
                Color.parseColor("#999999"));
        int defShadowSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
                4, getResources().getDisplayMetrics());
        //陰影尺寸
        int shadowSize = ta.getDimensionPixelSize(R.styleable.BubbleLayout_shadow_size, defShadowSize);
        mRadius = ta.getDimensionPixelSize(R.styleable.BubbleLayout_radius, 0);
        //三角形方向
        mDirection = ta.getInt(R.styleable.BubbleLayout_direction, BOTTOM);
        mOffset = ta.getDimensionPixelOffset(R.styleable.BubbleLayout_offset, 0);
        ta.recycle();

        mBorderPaint = new Paint();
        mBorderPaint.setAntiAlias(true);
        mBorderPaint.setColor(backGroundColor);
        mBorderPaint.setShadowLayer(shadowSize, 0, 0, shadowColor);

        mPath = new Path();
        mRect = new RectF();
        mDatumPoint = new Point();

        setWillNotDraw(false);
        //關閉硬體加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDatumPoint.x > 0 && mDatumPoint.y > 0)
            switch (mDirection) {
                case LEFT:
                    drawLeftTriangle(canvas);
                    break;
                case TOP:
                    drawTopTriangle(canvas);
                    break;
                case RIGHT:
                    drawRightTriangle(canvas);
                    break;
                case BOTTOM:
                    drawBottomTriangle(canvas);
                    break;
            }
    }

    private void drawLeftTriangle(Canvas canvas) {
        int triangularLength = getPaddingLeft();
        if (triangularLength == 0) {
            return;
        }

        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CCW);
        mPath.moveTo(mDatumPoint.x, mDatumPoint.y - triangularLength / 2);
        mPath.lineTo(mDatumPoint.x - triangularLength / 2, mDatumPoint.y);
        mPath.lineTo(mDatumPoint.x, mDatumPoint.y + triangularLength / 2);
        mPath.close();
        canvas.drawPath(mPath, mBorderPaint);
    }

    private void drawTopTriangle(Canvas canvas) {
        int triangularLength = getPaddingTop();
        if (triangularLength == 0) {
            return;
        }

        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CCW);
        mPath.moveTo(mDatumPoint.x + triangularLength / 2, mDatumPoint.y);
        mPath.lineTo(mDatumPoint.x, mDatumPoint.y - triangularLength / 2);
        mPath.lineTo(mDatumPoint.x - triangularLength / 2, mDatumPoint.y);
        mPath.close();
        canvas.drawPath(mPath, mBorderPaint);
    }

    private void drawRightTriangle(Canvas canvas) {
        int triangularLength = getPaddingRight();
        if (triangularLength == 0) {
            return;
        }

        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CCW);
        mPath.moveTo(mDatumPoint.x, mDatumPoint.y - triangularLength / 2);
        mPath.lineTo(mDatumPoint.x + triangularLength / 2, mDatumPoint.y);
        mPath.lineTo(mDatumPoint.x, mDatumPoint.y + triangularLength / 2);
        mPath.close();
        canvas.drawPath(mPath, mBorderPaint);
    }

    private void drawBottomTriangle(Canvas canvas) {
        int triangularLength = getPaddingBottom();
        if (triangularLength == 0) {
            return;
        }

        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CCW);
        mPath.moveTo(mDatumPoint.x + triangularLength / 2, mDatumPoint.y);
        mPath.lineTo(mDatumPoint.x, mDatumPoint.y + triangularLength / 2);
        mPath.lineTo(mDatumPoint.x - triangularLength / 2, mDatumPoint.y);
        mPath.close();
        canvas.drawPath(mPath, mBorderPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mRect.left = getPaddingLeft();
        mRect.top = getPaddingTop();
        mRect.right = w - getPaddingRight();
        mRect.bottom = h - getPaddingBottom();

        switch (mDirection) {
            case LEFT:
                mDatumPoint.x = getPaddingLeft();
                mDatumPoint.y = h / 2;
                break;
            case TOP:
                mDatumPoint.x = w / 2;
                mDatumPoint.y = getPaddingTop();
                break;
            case RIGHT:
                mDatumPoint.x = w - getPaddingRight();
                mDatumPoint.y = h / 2;
                break;
            case BOTTOM:
                mDatumPoint.x = w / 2;
                mDatumPoint.y = h - getPaddingBottom();
                break;
        }

        if (mOffset != 0) {
            applyOffset();
        }
    }

    /**
     * 設定三角形偏移位置
     *
     * @param offset 偏移量
     */
    public void setTriangleOffset(int offset) {
        this.mOffset = offset;
        applyOffset();
        invalidate();
    }

    private void applyOffset() {
        switch (mDirection) {
            case LEFT:
            case RIGHT:
                mDatumPoint.y += mOffset;
                break;
            case TOP:
            case BOTTOM:
                mDatumPoint.x += mOffset;
                break;
        }
    }
}
複製程式碼

使用:

  1. 複製BubbleLayout到專案中
  2. 複製以下屬性到res/values/attrs中
    <declare-styleable name="BubbleLayout">
        <attr name="background_color" format="color" />
        <attr name="shadow_color" format="color" />
        <attr name="shadow_size" format="dimension" />
        <attr name="radius" format="dimension" />
        <attr name="direction" format="enum">
            <enum name="left" value="1" />
            <enum name="top" value="2" />
            <enum name="right" value="3" />
            <enum name="bottom" value="4" />
        </attr>
        <attr name="offset" format="dimension" />
    </declare-styleable>
複製程式碼

3.xml中使用

    <me.wy.app.BubbleLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        //必須設定足夠的padding才可以繪製三角形和陰影
        android:padding="16dp"
        //背景顏色
        app:background_color="#FF4081"
        //三角形方向
        app:direction="left"
        //三角形相對偏移量
        app:offset="-40dp"
        //圓角大小
        app:radius="4dp"
        //陰影顏色
        app:shadow_color="#999999"
        //陰影大小
        app:shadow_size="4dp">

        ...
    </me.wy.app.BubbleLayout>
複製程式碼

Github地址

相關文章