Summary of how Android implements triangle bubble effect

Time:2021-8-20

During the development process, we may often encounter such requirement styles:

Summary of how Android implements triangle bubble effect

This picture is a pop-up box for intercepting JD news notification. We can see that there is a triangular bubble effect at the top right. This is only one of them. The directions of the triangle can also be up, down, left and right.

It can be found from the screenshot that bubbles are composed of regular triangles and rounded rectangles. Therefore, triangular bubbles can be formed by combination. We will implement them in three ways below.

Implementation method:

1. It is realized through Fig. 9;
2. Realized by shape;
3. It is realized by customizing the view;

Implementation logic:

1. It is realized through Fig. 9

Needless to say, you can cut a. 9 picture from Miss UI and use it, but the picture in this way needs to occupy a certain volume.

2. Realized by shape

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
            android:fromDegrees="45"
            android:pivotX="-40%"
            android:pivotY="80%">
            <shape android:shape="rectangle">
                <size
                    android:width="15dp"
                    android:height="15dp" />
                <solid android:color="#ffffff" />
            </shape>
        </rotate>
    </item>
</layer-list>
  • Inverted triangle
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
     <item>
        <rotate
            android:fromDegrees="45"
            android:pivotX="135%"
            android:pivotY="15%">
            <shape android:shape="rectangle">
                <size
                    android:width="15dp"
                    android:height="15dp" />
                <solid android:color="#ffffff" />
            </shape>
        </rotate>
    </item>
</layer-list>
  • Left triangle
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
            android:fromDegrees="-45"
            android:pivotX="85%"
            android:pivotY="-35%">>
            <shape android:shape="rectangle">
                <size
                    android:width="15dp"
                    android:height="15dp" />
                <solid android:color="#ffffff" />
            </shape>
        </rotate>
    </item>
</layer-list>
  • Right triangle
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
            android:fromDegrees="-45"
            android:pivotX="15%"
            android:pivotY="135%">>
            <shape android:shape="rectangle">
                <size
                    android:width="15dp"
                    android:height="15dp" />
                <solid android:color="#ffffff" />
            </shape>
        </rotate>
    </item>
</layer-list>

The above is to realize the code in all directions through shape. The disadvantages of this method are obvious. If you want to change the position of different corners, you need to write different layouts.

3. It is implemented by customizing the view

Since it’s relatively simple, I won’t explain how to do it here. You can copy it and use it directly

  • Add custom attribute
<declare-styleable name="TriangleView">
        <attr name="trv_color" format="color" />
        <attr name="trv_direction">
            <enum name="top" value="0" />
            <enum name="bottom" value="1" />
            <enum name="right" value="2" />
            <enum name="left" value="3" />
        </attr>
 </declare-styleable>
  • Custom code file

public class TriangleView extends View {
    private static final int TOP = 0;
    private static final int BOTTOM = 1;
    private static final int RIGHT = 2;
    private static final int LEFT = 3;
    private static final int DEFUALT_WIDTH = 10;
    private static final int DEFUALT_HEIGHT = 6;
    private static final int DEFUALT_COLOR = R.color.FFF;
    private Paint mPaint;
    private int mColor;
    private int mWidth;
    private int mHeight;
    private int mDirection;
    private Path mPath;

    public TriangleView(final Context context) {
        this(context, null);
    }

    public TriangleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TriangleView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TriangleView, 0, 0);
        mColor = typedArray.getColor(R.styleable.TriangleView_trv_color, ContextCompat.getColor(getContext(), DEFUALT_COLOR));
        mDirection = typedArray.getInt(R.styleable.TriangleView_trv_direction, mDirection);
        typedArray.recycle();
        mPaint.setColor(mColor);
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPath = new Path();
        mDirection = TOP;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (mWidth == 0 || widthMode != MeasureSpec.EXACTLY) {
            mWidth = (int) PixelUtil.dp2px(DEFUALT_WIDTH);
        }
        if (mHeight == 0 || heightMode != MeasureSpec.EXACTLY) {
            mHeight = (int) PixelUtil.dp2px(DEFUALT_HEIGHT);
        }
        setMeasuredDimension(mWidth, mHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        switch (mDirection) {
            case TOP:
                mPath.moveTo(0, mHeight);
                mPath.lineTo(mWidth, mHeight);
                mPath.lineTo(mWidth / 2, 0);
                break;
            case BOTTOM:
                mPath.moveTo(0, 0);
                mPath.lineTo(mWidth / 2, mHeight);
                mPath.lineTo(mWidth, 0);
                break;
            case RIGHT:
                mPath.moveTo(0, 0);
                mPath.lineTo(0, mHeight);
                mPath.lineTo(mWidth, mHeight / 2);
                break;
            case LEFT:
                mPath.moveTo(0, mHeight / 2);
                mPath.lineTo(mWidth, mHeight);
                mPath.lineTo(mWidth, 0);
                break;
            default:
                break;
        }

        mPath.close();
        canvas.drawPath(mPath, mPaint);
    }
}
  • Layout file add
<com.sjl.keeplive.triange.TriangleView
        android:layout_width="10dp"
        android:layout_height="6dp"
        app:trv_color="@color/FFF"
        app:trv_direction="top" />

The user-defined method can handle the four directions, and can also be used in the code. It is a better way to add and change colors dynamically.

That’s it