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

概述

如图1是qq聊天消息的长按的弹窗,最主要的特点是有一个指针,指针的位置是手指触摸手机屏幕的位置,而且弹窗会根据手指的触摸屏幕的不同位置显示在不同的位置,由于项目需要,仿写了一个相似功能的弹窗,并封装成库QPopuWindow,库的主要特点是控件的代码动态绘制及背景选择器的代码动态绘制,无xml资源的引用,此库我托管在jitpack.io中,方便大家直接依赖使用

图1

qq消息图片

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弹出位置的常用的两种方式showAsDropDownshowAtLocation

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.LEFTGravity.START效果一样.官方建议使用Gravity.START,Gravity.RIGHTGravity.END同理,指定不同的Gravity对x,y有不同的影响,其中Gravity.LEFTGravity.RIGHT影响x方向的偏移,Gravity.TOPGravity.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,谢谢

看到的朋友希望帮忙顶一个(#^.^#)

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android 仿QQ图片选择器,包含图片裁剪,浏览大图功能。   博客地址:http://blog.csdn.net/junzia/article/details/53091606简单使用示例如需使用图片选择器功能,将chooser加入为依赖工程,然后使用指定功能:选择单张图片如果需要选择单张图片,调用:Intent intent=new Intent(IcFinal.ACTION_ALBUM); intent.putExtra(IcFinal.INTENT_MAX_IMG,1);   startActivityForResult(intent,1);裁剪如果需要选择单张图片并且裁剪,调用:Intent intent=new Intent(IcFinal.ACTION_ALBUM); intent.putExtra(IcFinal.INTENT_IS_CROP,true); startActivityForResult(intent,1);默认为圆形图片,大小为500*500。如果需要自行设定,给intent增加以下参数(目前功能未实现):intent.putExtra(IcFinal.INTENT_CROP_SHAPE,CropPath.SHAPE_RECT);   //矩形intent.putExtra(IcFinal.INTENT_CROP_WIDTH,512); //裁剪宽度intent.putExtra(IcFinal.INTENT_CROP_HEIGHT,280); //裁剪高度选择多张图片Intent intent=new Intent(IcFinal.ACTION_ALBUM); intent.putExtra(IcFinal.INTENT_MAX_IMG,9); startActivityForResult(intent,1);更多设置也许图片选择器默认的UI不符合你的要求,你可以选择利用ChooserSetting中的静态参数来更改图片选择器的效果:/**标题的背景颜色*/public static int TITLE_COLOR=0xFF584512;/**图片选择页,每行显示数*/public static int NUM_COLUMNS=3;/**图片加载失败的图片*/public static int errorResId=0;/**图片加载的占位图片*/public static int placeResId=R.mipmap.image_chooser_placeholder;/**图片加载的动画*/public static int loadAnimateResId=0;/**选中图片的滤镜颜色*/public static int chooseFilter=0x55000000;/**未被选中的图片的滤镜颜色*/public static int unChooseFilter=0;/**最新的图片集合显示名字*/public static String newestAlbumName="最新图片";/**最新图片集合的最大数量*/public static int newestAlbumSize=100;public static int albumPopupHeight=600;public static String tantoToast="";/**照片选择指示器*/public static IChooseDrawable chooseDrawable=new CircleChooseDrawable(true,0xFF25c2e6);如果这些也无法满足你的UI要求,你也可以参照EntryActivity重新写相册的入口Activity,参照CropActivity重写裁剪的入口Activity。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值