您的位置:首页 > 其它

[置顶] 仿qq聊天消息长按弹窗(支持所有view及自定义属性扩展)

2018-03-19 15:21 771 查看

概述

如图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

效果图



相关代码

10075

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,谢谢

看到的朋友希望帮忙顶一个(#^.^#)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐