概述
如图1是qq聊天消息的长按的弹窗,最主要的特点是有一个指针,指针的位置是手指触摸手机屏幕的位置,而且弹窗会根据手指的触摸屏幕的不同位置显示在不同的位置,由于项目需要,仿写了一个相似功能的弹窗,并封装成库
QPopuWindow
,库的主要特点是控件的代码动态绘制及背景选择器的代码动态绘制,无xml资源的引用,此库我托管在jitpack.io
中,方便大家直接依赖使用
图1
QPopuWindow介绍和使用
GitHub地址
QPopuWindow继承自PopuWindow,支持所有的View及属性自定义扩展,通过builder链式调用来设置不同的属性和显示,使用简单,代码简洁.
如何使用
- step
1.在项目的根build.gradle
添加
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
2.在模块中添加依赖
dependencies {
compile 'com.github.AndyAls:QPopuWindow:v2.0.0'
}
3.使用
QPopuWindow.getInstance(ListViewActivity.this).builder//-->通过单例模式获取builder对象
.bindView(view,position)//------------------------>绑定view,此方法必须调用,view必须是长按的那个view,position为view在listview的位置
.setPopupItemList(new String[]{"复制","粘贴","转发","更多...."})//->设置pop的数据源,此方法必须调用
.setPointers(rawX,rawY)//-------------------------->设置手指在屏幕触摸的绝对位置坐标,此方法必须调用
.setOnPopuListItemClickListener(new QPopuWindow.OnPopuListItemClickListener() {//pop item的点击事件监听回调
/**
* @param anchorView 为pop的绑定view
* @param anchorViewPosition pop绑定view在ListView的position
* @param position pop点击item的position 第一个位置索引为0
*/
@Override
public void onPopuListItemClick(View anchorView, int anchorViewPosition, int position) {
Toast.makeText(ListViewActivity.this,anchorViewPosition+"---->"+position,Toast.LENGTH_SHORT).show();
}
}).show();
QPopuWindow的应用
绑定普通View
- 效果图
- 相关代码
textView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
QPopuWindow.getInstance(CommActivity.this).builder
.bindView(v,0)//由于view不在列表 没有position ,可以随便传一个int值
.setPopupItemList(new String[]{"复制","粘贴","转发","更多...."})
.setPointers(rawX,rawY)
.setOnPopuListItemClickListener(new QPopuWindow.OnPopuListItemClickListener() {
@Override
public void onPopuListItemClick(View anchorView, int anchorViewPosition, int position) {
Toast.makeText(CommActivity.this,anchorViewPosition+"---->"+position,Toast.LENGTH_SHORT).show();
}
}).show();
return true;
}
});
绑定ListView
- 效果图
- 相关代码
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
QPopuWindow.getInstance(ListViewActivity.this).builder
.bindView(view,position)
.setPopupItemList(new String[]{"复制","粘贴","转发","更多...."})
.setPointers(rawX,rawY)
.setOnPopuListItemClickListener(new QPopuWindow.OnPopuListItemClickListener() {
@Override
public void onPopuListItemClick(View anchorView, int anchorViewPosition, int position) {
Toast.makeText(ListViewActivity.this,anchorViewPosition+"---->"+position,Toast.LENGTH_SHORT).show();
}
}).show();
return true;
}
});
绑定RecylerView
效果图
相关代码
private class MyViewHolder extends RecyclerView.ViewHolder{
public MyViewHolder(View itemView) {
super(itemView);
itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
QPopuWindow.getInstance(RecyclerViewActivity.this).builder
.bindView(v,MyViewHolder.this.getAdapterPosition())
.setPopupItemList(new String[]{"复制","粘贴","转发","更多...."})
.setPointers(rawX,rawY)
.setOnPopuListItemClickListener(new QPopuWindow.OnPopuListItemClickListener() {
@Override
public void onPopuListItemClick(View anchorView, int anchorViewPosition, int position) {
Toast.makeText(RecyclerViewActivity.this,anchorViewPosition+"---->"+position,Toast.LENGTH_SHORT).show();
}
}).show();
return true;
}
});
}
}
Note : 以上.setPointers方法传入的值为手指触摸的绝对位置,如果传入的不对,弹窗的位置会错乱,我是用下面方法获取到的,重载当前Activity的dispatchTouchEvent方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
rawX= (int) ev.getRawX();
rawY= (int) ev.getRawY();
return super.dispatchTouchEvent(ev);
}
QPopuWindow builder
属性扩展方法列表
方法 | 描述 |
---|---|
bindView(View anchorView, int position) | 绑定view,此方法必须调用,view必须是长按的那个view,position为view在listview的位置 |
setPopupItemList(String[] itemDataSource) | 设置pop的数据源,此方法必须调用 |
setPointers(int rawX, int rawY) | 设置手指在屏幕触摸的绝对位置坐标,此方法必须调用 |
show() | pop显示,此方法必须调用,以下方法非必须调用,来对属性的扩展 |
setTextPadding(int left, int top, int right, int bottom) | 设置pop item单元的padding |
setTextSize(int size) | 设置pop item字体大小 |
setRadius(int radius) | 设置弹窗圆角 |
setIndicatorViewSize(int width, int height) | 设置指针大小 |
setPressedBackgroundColor(int color) | 设置item 单元点击状态颜色 |
setNormalBackgroundColor(int color) | 设置item 单元正常状态颜色 |
setTextColor(int color) | 设置item 单元的字体颜色 |
setTextDrawableRes(Integer[] drawableRes) | 设置item 单元的图标,默认在字体上方 |
setTextDrawableSize(int size) | 设置item 单元的图标大小 |
setOnPopuListItemClickListener(OnPopuListItemClickListener listener) | item 单元点击监听回调 |
setDividerVisibility(boolean visibility) | 设置item 分割线是否可见 |
buidler
属性扩展
只需重载上面方法,就能自定义弹窗的属性,比如
- 相关代码
QPopuWindow.getInstance(this).builder
.bindView(v,0)
.setPopupItemList(new String[]{"复制","粘贴","转发","更多...."})
.setPointers(rawX,rawY)
.setNormalBackgroundColor(Color.RED)
.setRadius(60)
.setTextDrawableRes(new Integer[]{R.mipmap.andy})//建议和setPopupItemList长度设置一样,一一对应
.setDividerVisibility(false)
.setOnPopuListItemClickListener(new QPopuWindow.OnPopuListItemClickListener() {
@Override
public void onPopuListItemClick(View anchorView, int anchorViewPosition, int position) {
Toast.makeText(CommActivity.this,anchorViewPosition+"---->"+position,Toast.LENGTH_SHORT).show();
}
}).show();
QPopuWindow
一些关键源码解析
通过单例模式来获取builder对象,此来构建不同属性的弹窗
private QPopuWindow(Context context) {
super(context);
this.mContext = context;
builder = new Builder();
}
public static synchronized QPopuWindow getInstance(Context context) {
if (popupList == null) {
popupList = new QPopuWindow(context);
}
return popupList;
}
builder 通过建造者模式来设置不同的属性
public class Builder {
private Config config;
private Builder() {
config = new Config();
}
/**
* 绑定anchorView <b>必须调用</b>
*
* @param anchorView anchorView
* @param position anchorView的条目位置,通过回调返回
*/
public Builder bindView(View anchorView, int position) {
config.position = position;
config.mAnchorView = anchorView;
return builder;
}
.......
/**
* 设置item 单元的图标,默认在字体上方
*
* @param drawableRes 建议和item的长度设置一样,一一对应
* @see #setPopupItemList(String[])
*/
public Builder setTextDrawableRes(@DrawableRes Integer[] drawableRes) {
if (drawableRes != null) {
List<Integer> drawables = Arrays.asList(drawableRes);
config.textDrawableList = new ArrayList<>();
config.textDrawableList.clear();
for (int i = 0; i < drawables.size(); i++) {
Drawable drawable = mContext.getResources().getDrawable(drawables.get(i));
config.textDrawableList.add(drawable);
}
}
return builder;
}
.......
确定PopupWindow
弹出的位置,这也是此库的核心点
为了更好的阅读体验,具体位置确定这块独立抽离出一篇文章,可点击查看
我们先了解一下
PopupWindow
弹出位置的常用的两种方式showAsDropDown和showAtLocation
1. showAsDropDown(View anchor, int xoff, int yoff)
- 参数anchor: 弹窗依附的view
- xoff : 坐标x方向的偏移 x+10表示向右偏移
- yoff: 坐标y方向的偏移 y+10表示向下偏移
这个方法查看源码,官方注释的已经很清楚的,大致的意思是以
anchor
的左下角坐标作为PopupWindow
的原点坐标显示在anchor
下方,如果下方显示的空间不足,anchor
的父控件有可滚动的ScrollView
,则anchor
会向上滚动来确保PopupWindow
足够的显示空间,如果父控件没有可滚动的控件,此时会以anchor
的左上角坐标作为PopupWindow
的原点坐标来显示.具体如下图
2 showAtLocation(View parent, int gravity, int x, int y)
- 参数parent: 对弹窗的位置没有影响,主要作用获取windowtoken
- 参数gravity: 指定弹窗偏移方向的边缘,下面会具体介绍
- 参数x: 坐标x方向的偏移
- 参数y: 坐标y方向的偏移
综述:
showAtLocation
这个方法指定弹窗相对于屏幕的精确位置,和具体的anchorView没有关系,后面三个参数来确定弹窗的位置,其中指定Gravity.NO_GRAVITY
相当于Gravity.TOP|Gravity.LEFT
,指定Gravity.CENTER
,Gravity.CENTER_VERTICAL
,Gravity.CENTER_HORIZONTAL
效果一样,指定Gravity.LEFT
和Gravity.START
效果一样.官方建议使用Gravity.START
,Gravity.RIGHT
和Gravity.END
同理,指定不同的Gravity对x,y有不同的影响,其中Gravity.LEFT
和Gravity.RIGHT
影响x方向的偏移,Gravity.TOP
和Gravity.BOTTOM
影响y方向的偏移下面我们分别介绍
A: Gravity.LEFT弹窗显示在屏幕左边界的中心位置,并以PopupWindow
左边界中心为坐标原点(0,0)来偏移,x +表示向右偏移 ,y +表示向下偏移
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.LEFT,
180,
180);
B: Gravity.RIGHT弹窗显示在屏幕右边界的中心位置,并以PopupWindow
右边界中心为坐标原点(0,0)来偏移,x + 表示向左偏移 ,y +表示向下偏移
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.RIGHT,
180,
180);
C: Gravity.TOP 弹窗显示在屏幕上边界的中心位置,并以PopupWindow
上边界中心为坐标原点(0,0)来偏移,x + 表示向左偏移,y +表示向下偏移
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.TOP,
180,
180);
D: Gravity.BOTTOM 弹窗显示在屏幕下边界的中心位置,并以PopupWindow
下边界中心为坐标原点(0,0)来偏移,x + 表示向左偏移,y +表示向上偏移
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.BOTTOM,
180,
180);
E: Gravity.CENTER弹窗显示在屏幕中心点坐标位置,并以PopupWindow
中心点坐标为坐标原点(0,0)来偏移,x + 表示向左偏移,y +表示向下偏移 ,Gravity.CENTER_HORIZONTAL,Gravity.CENTER_VERTICAL效果一样
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.CENTER,
180,
180);
F: 组合Gravity符合上面偏移规律,如 Gravity.TOP|Gravity.LEFT弹窗显示在屏幕坐标原点(0,0),并以PopupWindow
左上角坐标为坐标原点(0,0)来偏移,x + 表示向左偏移,y +表示向下偏移 ,其他组合请参考上面几条的偏移规律
如下段代码显示的效果
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.TOP|Gravity.LEFT,
180,
180);
总结 Gravity和x,y的偏移规律符合WindowManager.LayoutParams属性,所以我们平常自定义Window,Toast,Dialog,PoPuWindow来确定窗体的位置时,都可以利用以上的偏移原理
知道了上面的原理,我们QPopuWindow
根据手指的方向来显示弹窗的位置也很好确定,相关代码
config.mPopupWindow.showAtLocation(config.mAnchorView, Gravity.CENTER,
mRawX - getScreenWidth(mContext) / 2,
mRawY - getScreenHeight(mContext) / 2 - config.mPopupWindowHeight);
从图片中很显然能确定弹窗位置,其中粉红色是gravity来确定弹窗原来的位置,x偏移的位置就是手指触摸屏幕的位置-屏幕宽度/2,y偏移位置就是手指触摸屏幕的位置-屏幕高/2
代码动态绘制弹窗的背景和圆角 GradientDrawable
构建selector对象
/**
* 绘制背景和圆角
*/
private void setPopupListBgAndRadius(Config config) {
// left
GradientDrawable leftItemPressedDrawable = new GradientDrawable();
leftItemPressedDrawable.setColor(config.pressedBackgroundColor);
leftItemPressedDrawable.setCornerRadii(new float[]{
config.radius, config.radius,
0, 0,
0, 0,
config.radius, config.radius});
GradientDrawable leftItemNormalDrawable = new GradientDrawable();
leftItemNormalDrawable.setColor(Color.TRANSPARENT);
leftItemNormalDrawable.setCornerRadii(new float[]{
config.radius, config.radius,
0, 0,
0, 0,
config.radius, config.radius});
mLeftItemBackground = new StateListDrawable();
mLeftItemBackground.addState(new int[]{android.R.attr.state_pressed}, leftItemPressedDrawable);
mLeftItemBackground.addState(new int[]{}, leftItemNormalDrawable);
// right
GradientDrawable rightItemPressedDrawable = new GradientDrawable();
rightItemPressedDrawable.setColor(config.pressedBackgroundColor);
rightItemPressedDrawable.setCornerRadii(new float[]{
0, 0,
config.radius, config.radius,
config.radius, config.radius,
0, 0});
GradientDrawable rightItemNormalDrawable = new GradientDrawable();
rightItemNormalDrawable.setColor(Color.TRANSPARENT);
rightItemNormalDrawable.setCornerRadii(new float[]{
0, 0,
config.radius, config.radius,
config.radius, config.radius,
0, 0});
mRightItemBackground = new StateListDrawable();
mRightItemBackground.addState(new int[]{android.R.attr.state_pressed}, rightItemPressedDrawable);
mRightItemBackground.addState(new int[]{}, rightItemNormalDrawable);
// corner
GradientDrawable cornerItemPressedDrawable = new GradientDrawable();
cornerItemPressedDrawable.setColor(config.pressedBackgroundColor);
cornerItemPressedDrawable.setCornerRadius(config.radius);
GradientDrawable cornerItemNormalDrawable = new GradientDrawable();
cornerItemNormalDrawable.setColor(Color.TRANSPARENT);
cornerItemNormalDrawable.setCornerRadius(config.radius);
mCornerItemBackground = new StateListDrawable();
mCornerItemBackground.addState(new int[]{android.R.attr.state_pressed}, cornerItemPressedDrawable);
mCornerItemBackground.addState(new int[]{}, cornerItemNormalDrawable);
mCornerBackground = new GradientDrawable();
mCornerBackground.setColor(config.normalBackgroundColor);
mCornerBackground.setCornerRadius(config.radius);
}
总结
对源码感兴趣的朋友,欢迎移步GitHub,并给个star,谢谢
看到的朋友希望帮忙顶一个(#^.^#)