[置顶] 仿qq聊天消息长按弹窗(支持所有view及自定义属性扩展)
2018-03-19 15:21
771 查看
概述
如图1是qq聊天消息的长按的弹窗,最主要的特点是有一个指针,指针的位置是手指触摸手机屏幕的位置,而且弹窗会根据手指的触摸屏幕的不同位置显示在不同的位置,由于项目需要,仿写了一个相似功能的弹窗,并封装成库QPopuWindow,库的主要特点是控件的代码动态绘制及背景选择器的代码动态绘制,无xml资源的引用,此库我托管在
jitpack.io中,方便大家直接依赖使用
图1
QPopuWindow介绍和使用
GitHub地址
QPopuWindow继承自PopuWindow,支持所有的View及属性自定义扩展,通过builder链式调用来设置不同的属性和显示,使用简单,代码简洁.如何使用
step1.在项目的根
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) | 设置popitem单元的padding |
setTextSize(int size) | 设置popitem字体大小 |
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: 弹窗依附的viewxoff : 坐标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,谢谢
看到的朋友希望帮忙顶一个(#^.^#)相关文章推荐
- 自定义view——贝塞尔曲线之仿qq消息气泡拖拽让所有view拖动爆炸
- 自定义下拉刷新控件 - RefreshableView(支持所有控件的下拉刷新)
- 一个类似于环形的 ProgressBar 的,可以自定义 Color,style 等属性的 View,支持 Material 风格.
- [置顶] Android自定义View(二、深入解析自定义属性)
- [置顶] Android 仿QQ首页的消息和电话的切换(Viewpager+frgment)
- TextView 支持自定义字体和属性
- Android解析自定义属性的XML实现底部导航栏TabSelectedView,实现灵活的配置扩展
- 自定义 LINQ 中 Distinct 的 Compare,以及扩展 LINQ 的 Distinct,让它支持属性比较
- Android RichText 让Textview轻松的支持富文本(图像ImageSpan、点击效果等等类似QQ微信聊天)
- QQ列表展开收缩扩展(自定义属性,索引值练习)
- 自定义View之仿QQ消息滑动删除
- Material Design系列之自定义Behavior支持所有View
- Material Design系列,自定义Behavior支持所有View
- [置顶] 利用自定义View结合onTouchListener实现QQ侧滑菜单效果
- android自定义View 之仿QQ消息头像
- android自定义view之模拟qq消息拖拽删除效果
- 自定义view——贝塞尔曲线之仿qq消息气泡拖拽
- [置顶] Android自定义View(六) -- 属性动画(上)
- Android自定义view之path类描绘二阶贝塞尔曲线+属性动画(模仿QQ账号信息曲线动画)
- wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果