Android 自定义布局实现气泡弹窗,可控制气泡尖角方向及偏移量。
效果图
实现
首先自定义一个气泡布局。
/** * 气泡布局 */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) { 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); }}
- 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
- 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
样式 attrs.xml
<?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,用于显示气泡。
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; }}
- 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
- 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
view_popup_window.xml
<?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>
调用
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);
依赖
dependencies { compile 'com.yuyh.bubble:library:1.0.0'}
项目地址
https://github.com/smuyyh/BubblePopupWindow