项目地址: https://github.com/JrDong/FitPopupWindow
效果图:
效果图

简介

项目是仿照网易新闻或者今日头条的不感兴趣功能。现在很多应用都加入了feed流,对自己不感兴趣的条目可以删除。
考虑到两个因素:1.每个item中叉号的位置并不是固定的,所以我们要根据点击的位置来判断弹框气泡的位置。2.list滑动时,当我想点击下面的item,则弹框应该向上弹出,反之亦然,所以要判断弹出的方向。
功能实现的话选择PopupWindow来实现。

PopupWindow

首先我们来自定义一个PopupWindow,先对PopupWindow进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void init(Activity context, int width, int height) {
this.context = context;
setWidth(width);
setHeight(height);
//点击空白处让PopupWindow消失
setBackgroundDrawable(new ColorDrawable(0x00000000));
setOutsideTouchable(true);
//PopupWindow弹出后,所有的触屏和物理按键都有PopupWindows处理
setFocusable(true);
//设置消失的监听
setOnDismissListener(this);
//设置动画
setAnimationStyle(R.style.popp_anim);
}

初始化好之后,就需要进行计算弹出的位置了。定义PopupWindow弹窗位置有三个方法:

showAsDropDown(View anchor):在某个控件正下方,无偏移

showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置,有偏移

showAtLocation(View parent, int gravity, int x, int y):第一个参数官方文档”a parent view to get the android.view.View.getWindowToken() token from“,这个parent的作用应该是调用其getWindowToken()方法获取窗口的Token,所以,只要是该窗口上的控件就可以了。gravity控制弹出位置,x,y分别控制偏移量。

接下来,计算偏移量:

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
/**
* @param anchorView 弹出window的view
* @param contentView PopupWindow的内容布局
* @return window显示的左上角的xOff, yOff坐标
*/
protected int[] calculatePopWindowPos(final View anchorView, final View contentView) {
final int windowPos[] = new int[2];
final int anchorLoc[] = new int[2];
// 获取锚点View在屏幕上的左上角坐标位置
anchorView.getLocationOnScreen(anchorLoc);
final int anchorHeight = anchorView.getHeight();
final int anchorWidth = anchorView.getWidth();
mXCoordinate = anchorLoc[0];
// 获取屏幕的高宽
final int screenHeight = ScreenUtils.getScreenHeight(anchorView.getContext());
final int screenWidth = ScreenUtils.getScreenWidth(anchorView.getContext());
contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
// 计算contentView的高宽
int windowHeight = contentView.getMeasuredHeight();
mWindowWidth = mWindowWidth > 0 ? mWindowWidth : contentView.getMeasuredWidth();
// 判断需要向上弹出还是向下弹出,如果要改变弹出策略,改变此处即可
// 目前是根据屏幕的一半进行判断
final boolean isNeedShowUp = (screenHeight - anchorLoc[1] - anchorHeight < screenHeight / 2);
// 判断需要向左弹出还是向右弹出
final boolean isNeedShowLeft = (anchorLoc[0] < mWindowWidth / 2);
//分别设置水平竖直弹出位置
setHorizontal(isNeedShowLeft ? FitPopupWindowLayout.LEFT : FitPopupWindowLayout.RIGHT);
setVertical(isNeedShowUp ? FitPopupWindowLayout.UP : FitPopupWindowLayout.DOWN);
windowPos[0] = (screenWidth - mWindowWidth) / 2;
windowPos[1] = isNeedShowUp ?
anchorLoc[1] - windowHeight - PADDING - FitPopupWindowLayout.SHARP_HEIGHT
: anchorLoc[1] + anchorHeight + PADDING;
return windowPos;
}

设置View的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @contentView 内容布局
* @anchorView 目标view,比如叉号
*/
public void setView(View contentView, View anchorView) {
this.anchorView = anchorView;
//计算偏移坐标
windowPos = calculatePopWindowPos(anchorView, contentView);
//自定义带气泡的布局,最外层
mFitPopupWindowLayout = new FitPopupWindowLayout(context);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, getHeight() - FitPopupWindowLayout.SHARP_HEIGHT);
layoutParams.bottomMargin = FitPopupWindowLayout.SHARP_HEIGHT;
contentView.setLayoutParams(layoutParams);
//给起泡布局设置方向,左上,右下等,并设置x方向上的偏移量
mFitPopupWindowLayout.setOrientation(getHorizontal(), getVertical(), getXCoordinate());
//将内容布局添加到自定义的气泡布局中
mFitPopupWindowLayout.addView(contentView);
//设置PopupWindow布局
setContentView(mFitPopupWindowLayout);
}

显示PopupWindow

1
2
3
4
5
6
7
8
9
10
11
public void show() {
showAtLocation(anchorView, Gravity.TOP | Gravity.END
, windowPos[0], windowPos[1]);
update();
//弹出PopupWindow时让背景置灰,在onDismiss()回调中再将背景恢复
Window window = context.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
lp.alpha = 0.7f;
window.setAttributes(lp);
}

接下来我们看下自定义气泡布局

气泡布局由两部分组成,一个是圆角矩形,一个是贝塞尔曲线画成的尖角。我们直接看下onDraw方法

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
@Override
protected void onDraw(Canvas canvas) {
//添加圆角矩形
mPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight() - SHARP_HEIGHT)
, RECT_CORNER, RECT_CORNER, Path.Direction.CW);
mPath.addPath(makeSharpPath());
canvas.drawPath(mPath, mPaint);
//此处的mHorizontal和mVertical是上面PopupWindow计算出弹出位置后设置进来的
if (mHorizontal == LEFT && mVertical == UP) {
setScaleX(1);
setScaleY(1);
} else if (mHorizontal == LEFT && mVertical == DOWN) {
setScaleX(1);
setScaleY(-1);
scaleChild(1, -1);
} else if (mHorizontal == RIGHT && mVertical == UP) {
setScaleX(-1);
setScaleY(1);
scaleChild(-1, 1);
} else if (mHorizontal == RIGHT && mVertical == DOWN) {
setScaleX(-1);
setScaleY(-1);
scaleChild(-1, -1);
}
}
private Path makeSharpPath() {
mSharpPath.moveTo(mXoffset, getMeasuredHeight() - SHARP_HEIGHT);
mSharpPath.cubicTo(mXoffset, getMeasuredHeight(), mXoffset, getMeasuredHeight() - SHARP_HEIGHT,
SHARP_WIDTH + mXoffset, getMeasuredHeight() - SHARP_HEIGHT);
return mSharpPath;
}

默认是按照左上角的位置画出的,也就是说先画出一个尖角在左上角的气泡view,
再根据偏移量用setScaleX,和 setScaleY进行旋转,但注意,旋转的话其中的子view也会跟着一起旋转,所以之后我们要再把其中的子view旋转回来。
mHorizontal和mVertical是上面PopupWindow计算出弹出位置后设置进来的。
这样一个带气泡的view就画好了。

我们再来整理一遍:
1.传入一个目标view,计算出PopupWindow的弹出方向,和x轴上的偏移量。
2.绘制一个气泡view,根据计算出来的方向和偏移量,画出气泡的方向。
3.传入一个内容布局,这个可以根据项目需求自定义。add到气泡ViewGroup中。
4.向上还是向下由showAtLocation方法决定,尖角的位置取决于通过目标view计算出来的方向和x轴偏移量,有了x轴的偏移量,就可以保证尖角在目标view的正下方。

以上就是自适应位置的全部思路,全部源码参考https://github.com/JrDong/FitPopupWindow