Android 实现气泡布局/弹窗,可控制气泡尖角方向及偏移量

Android 自定义布局实现气泡弹窗,可控制气泡尖角方向及偏移量。

效果图

实现

首先自定义一个气泡布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* 气泡布局
*/
public class BubbleRelativeLayout extends RelativeLayout {

/**
* 气泡尖角方向
*/
public enum BubbleLegOrientation {
TOP, LEFT, RIGHT, BOTTOM, NONE
}

public static int PADDING = 30;
public static int LEG_HALF_BASE = 30;
public static float STROKE_WIDTH = 2.0f;
public static float CORNER_RADIUS = 8.0f;
public static int SHADOW_COLOR = Color.argb(100, 0, 0, 0);
public static float MIN_LEG_DISTANCE = PADDING + LEG_HALF_BASE;

private Paint mFillPaint = null;
private final Path mPath = new Path();
private final Path mBubbleLegPrototype = new Path();
private final Paint mPaint = new Paint(Paint.DITHER_FLAG);

private float mBubbleLegOffset = 0.75f;
private BubbleLegOrientation mBubbleOrientation = BubbleLegOrientation.LEFT;

public BubbleRelativeLayout(Context context) {
this(context, null);
}

public BubbleRelativeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public BubbleRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}

private void init(final Context context, final AttributeSet attrs) {

//setGravity(Gravity.CENTER);

ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setLayoutParams(params);

if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bubble);

try {
PADDING = a.getDimensionPixelSize(R.styleable.bubble_padding, PADDING);
SHADOW_COLOR = a.getInt(R.styleable.bubble_shadowColor, SHADOW_COLOR);
LEG_HALF_BASE = a.getDimensionPixelSize(R.styleable.bubble_halfBaseOfLeg, LEG_HALF_BASE);
MIN_LEG_DISTANCE = PADDING + LEG_HALF_BASE;
STROKE_WIDTH = a.getFloat(R.styleable.bubble_strokeWidth, STROKE_WIDTH);
CORNER_RADIUS = a.getFloat(R.styleable.bubble_cornerRadius, CORNER_RADIUS);
} finally {
if (a != null) {
a.recycle();
}
}
}

mPaint.setColor(SHADOW_COLOR);
mPaint.setStyle(Style.FILL);
mPaint.setStrokeCap(Cap.BUTT);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(STROKE_WIDTH);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setPathEffect(new CornerPathEffect(CORNER_RADIUS));

if (Build.VERSION.SDK_INT >= 11) {
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
}

mFillPaint = new Paint(mPaint);
mFillPaint.setColor(Color.WHITE);
mFillPaint.setShader(new LinearGradient(100f, 0f, 100f, 200f, Color.WHITE, Color.WHITE, TileMode.CLAMP));

if (Build.VERSION.SDK_INT >= 11) {
setLayerType(LAYER_TYPE_SOFTWARE, mFillPaint);
}
mPaint.setShadowLayer(2f, 2F, 5F, SHADOW_COLOR);

renderBubbleLegPrototype();

setPadding(PADDING, PADDING, PADDING, PADDING);

}

@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/**
* 尖角path
*/
private void renderBubbleLegPrototype() {
mBubbleLegPrototype.moveTo(0, 0);
mBubbleLegPrototype.lineTo(PADDING * 1.5f, -PADDING / 1.5f);
mBubbleLegPrototype.lineTo(PADDING * 1.5f, PADDING / 1.5f);
mBubbleLegPrototype.close();
}

public void setBubbleParams(final BubbleLegOrientation bubbleOrientation, final float bubbleOffset) {
mBubbleLegOffset = bubbleOffset;
mBubbleOrientation = bubbleOrientation;
}

/**
* 根据显示方向,获取尖角位置矩阵
* @param width
* @param height
* @return
*/
private Matrix renderBubbleLegMatrix(final float width, final float height) {

final float offset = Math.max(mBubbleLegOffset, MIN_LEG_DISTANCE);

float dstX = 0;
float dstY = Math.min(offset, height - MIN_LEG_DISTANCE);
final Matrix matrix = new Matrix();

switch (mBubbleOrientation) {

case TOP:
dstX = Math.min(offset, width - MIN_LEG_DISTANCE);
dstY = 0;
matrix.postRotate(90);
break;

case RIGHT:
dstX = width;
dstY = Math.min(offset, height - MIN_LEG_DISTANCE);
matrix.postRotate(180);
break;

case BOTTOM:
dstX = Math.min(offset, width - MIN_LEG_DISTANCE);
dstY = height;
matrix.postRotate(270);
break;

}

matrix.postTranslate(dstX, dstY);
return matrix;
}

@Override
protected void onDraw(Canvas canvas) {

final float width = canvas.getWidth();
final float height = canvas.getHeight();

mPath.rewind();
mPath.addRoundRect(new RectF(PADDING, PADDING, width - PADDING, height - PADDING), CORNER_RADIUS, CORNER_RADIUS, Direction.CW);
mPath.addPath(mBubbleLegPrototype, renderBubbleLegMatrix(width, height));

canvas.drawPath(mPath, mPaint);
canvas.scale((width - STROKE_WIDTH) / width, (height - STROKE_WIDTH) / height, width / 2f, height / 2f);

canvas.drawPath(mPath, mFillPaint);
}
}

样式 attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="bubble">
<attr name="shadowColor" format="color" />
<attr name="padding" format="dimension" />
<attr name="strokeWidth" format="float" />
<attr name="cornerRadius" format="float" />
<attr name="halfBaseOfLeg" format="dimension" />
</declare-styleable>

</resources>

然后自定义一个PopupWindow,用于显示气泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class BubblePopupWindow extends PopupWindow {

private BubbleRelativeLayout bubbleView;
private Context context;

public BubblePopupWindow(Context context) {
this.context = context;
setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);

setFocusable(true);
setOutsideTouchable(false);
setClippingEnabled(false);

ColorDrawable dw = new ColorDrawable(0);
setBackgroundDrawable(dw);
}

public void setBubbleView(View view) {
bubbleView = new BubbleRelativeLayout(context);
bubbleView.setBackgroundColor(Color.TRANSPARENT);
bubbleView.addView(view);
setContentView(bubbleView);
}

public void setParam(int width, int height) {
setWidth(width);
setHeight(height);
}

public void show(View parent) {
show(parent, Gravity.TOP, getMeasuredWidth() / 2);
}

public void show(View parent, int gravity) {
show(parent, gravity, getMeasuredWidth() / 2);
}

/**
* 显示弹窗
*
* @param parent
* @param gravity
* @param bubbleOffset 气泡尖角位置偏移量。默认位于中间
*/
public void show(View parent, int gravity, float bubbleOffset) {
BubbleRelativeLayout.BubbleLegOrientation orientation = BubbleRelativeLayout.BubbleLegOrientation.LEFT;
if (!this.isShowing()) {
switch (gravity) {
case Gravity.BOTTOM:
orientation = BubbleRelativeLayout.BubbleLegOrientation.TOP;
break;
case Gravity.TOP:
orientation = BubbleRelativeLayout.BubbleLegOrientation.BOTTOM;
break;
case Gravity.RIGHT:
orientation = BubbleRelativeLayout.BubbleLegOrientation.LEFT;
break;
case Gravity.LEFT:
orientation = BubbleRelativeLayout.BubbleLegOrientation.RIGHT;
break;
default:
break;
}
bubbleView.setBubbleParams(orientation, bubbleOffset); // 设置气泡布局方向及尖角偏移

int[] location = new int[2];
parent.getLocationOnScreen(location);

switch (gravity) {
case Gravity.BOTTOM:
showAsDropDown(parent);
break;
case Gravity.TOP:
showAtLocation(parent, Gravity.NO_GRAVITY, location[0], location[1] - getMeasureHeight());
break;
case Gravity.RIGHT:
showAtLocation(parent, Gravity.NO_GRAVITY, location[0] + parent.getWidth(), location[1] - (parent.getHeight() / 2));
break;
case Gravity.LEFT:
showAtLocation(parent, Gravity.NO_GRAVITY, location[0] - getMeasuredWidth(), location[1] - (parent.getHeight() / 2));
break;
default:
break;
}
} else {
this.dismiss();
}
}

/**
* 测量高度
*
* @return
*/
public int getMeasureHeight() {
getContentView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int popHeight = getContentView().getMeasuredHeight();
return popHeight;
}

/**
* 测量宽度
*
* @return
*/
public int getMeasuredWidth() {
getContentView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int popWidth = getContentView().getMeasuredWidth();
return popWidth;
}
}

view_popup_window.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<com.yuyh.library.BubbleRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/brlBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:cornerRadius="10"
app:halfBaseOfLeg="18dp"
app:padding="18dp"
app:shadowColor="#64000000"
app:strokeWidth="5">

</com.yuyh.library.BubbleRelativeLayout>

调用

1
2
3
4
5
6
BubblePopupWindow leftTopWindow = new BubblePopupWindow(MainActivity.this);
View bubbleView = inflater.inflate(R.layout.layout_popup_view, null);
TextView tvContent = (TextView) bubbleView.findViewById(R.id.tvContent);
tvContent.setText("HelloWorld");
leftTopWindow.setBubbleView(bubbleView); // 设置气泡内容
leftTopWindow.show(view, Gravity.BOTTOM, 0); // 显示弹窗

依赖

1
2
3
dependencies {
compile 'com.yuyh.bubble:library:1.0.0'
}

项目地址

https://github.com/smuyyh/BubblePopupWindow