当前位置: 首页 > 工具软件 > GalleryView > 使用案例 >

IM即时通讯项目讲解(二) 自定义实现图片选择GalleryView

闻人浩波
2023-12-01

IM即时通讯项目讲解(二)--自定义实现图片选择GalleryView

标签(空格分隔): 开源项目


###该系列技术课程来源慕课IM实战

带后台的IM即时通讯App 全程MVP手把手打造

#####通过该课程可以学习到以下知识点

  • 1、了解和开发后台项目(这个是需要长期积累的,有了这个可以说入门没问题)
  • 2、学习到IM相关知识点,创建群、添加群、单聊、群聊
  • 3、可以学习到数据库的相关操作(建表、表之间的关联等知识)
  • 4、学习到MVP模式,更加深入了解MVP模式的架构和实现
  • 5、学习到关于IM相关的优化,比如如何快速刷新界面
  • 6、学习到如何进行推送等相关操作(服务器端推送,单推、群推)
  • 7。。。当然还有好多的,大家不妨去了解一下,学习到知识才是最重要的

###效果图来一发


是不是感觉界面还是挺简洁的呢,那下面就看下如何实现的吧,实现还真不难,反而很简单的。

###前言

项目总结一:实现类似qq微信表情面板无缝切换(简书地址) 实现类似qq微信表情面板无缝切换(CSDN地址)

###进入主题 #####先大致分析一下,主要有以下几点需要我们考虑。

  • 1、如何拿到手机本地图片
  • 2、使用什么控件进行该展示
  • 3、怎么控制照片选择状态
  • 4、要考虑到复用情况,毕竟手机照片可能会有好多张
  • 5、显示出来的是方形(但是加载的图片是形状不规则的)

#####上述问题解决方案

  • 问题一通过LoaderManager和相关类实现
  • 问题二、四
    • 可以看到展示的是四列(可以自己定制),而且每一个图片的展示都市相同的,那么我们可以考虑GridView或者RecyclerView,但是从使用好感上我还是选择了RecyclerView
    • 对于复用的问题,recyclerview也是做了很好的处理,内部强制使用Holder。(这里就不做详细探讨)
  • 问题三
    • 其实这个我们可以放个方式来问,那就是我们的一个item里面都有什么布局。
    • 首先一个ImageView,然后又CheckBox,还有就是点击的阴影效果
  • 问题五--->这个就需要自己定义一个显示方形的控件了,其实就是 重新onMeasure,然后在测量的时候,传入一个依据宽度的值(长和宽都是宽度就行了)

#####好了首先开始写之前的问题,我们都有了相应的解决方案,对于开发中出现的问题我们在遇到的时候当场解决吧。进入实战

###封装实战之前先来看下我们的item布局并附有相关解释 #####首先是方形控件SquareLayout

    //我们选择继承自FrameLayout  重写onMeasure
    
     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //高宽给父类  传递的测量值都是宽度  那么就可以形成基于宽度的正方形控件
        if (mBaseDirection == 1) {
            super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        } else if (mBaseDirection == 2) {
            super.onMeasure(heightMeasureSpec, heightMeasureSpec);
        } else {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);

            if (heightSize == 0) {
                super.onMeasure(widthMeasureSpec, widthMeasureSpec);
                return;
            }

            if (widthSize == 0) {
                super.onMeasure(heightMeasureSpec, heightMeasureSpec);
                return;
            }
            if (widthSize > heightSize)
                super.onMeasure(heightMeasureSpec, heightMeasureSpec);
            else
                super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        }
    }
    
复制代码

#####布局控件

<?xml version="1.0" encoding="utf-8"?>
<com.mingchu.common.widget.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="1dp"
    android:background="@color/white"
    android:orientation="vertical"
    app:comAccordTo="horizontal">

    <ImageView
        android:id="@+id/im_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/app_name" />

    <!--black_alpha_112 === #70000000-->>
    <View
        android:id="@+id/view_shade"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black_alpha_112"
        android:visibility="gone" />

    <CheckBox
        android:id="@+id/cb_select"
        android:layout_width="22dp"
        android:layout_height="22dp"
        android:layout_gravity="end"
        android:layout_margin="@dimen/len_2"
        android:button="@drawable/sel_cb_circle"
        android:clickable="false"
        android:drawablePadding="0dp"
        android:enabled="false"
        android:padding="0dp"
        app:buttonTint="@color/cb_gallery" />
</com.mingchu.common.widget.SquareLayout>
复制代码

cb_gallery.xml

<!--white_alpha_192 === #c0ffffff-->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/white" android:state_checked="true" />
    <item android:color="@color/white_alpha_192" />
</selector>
复制代码

相比没什么好解释的和之前描述的问题解答一样,这里就不多做解释。直接看下面的封装吧

###封装RecyclerView 既然已经选择了RecyclerView来进行我们的本地相片的列表展示,是时候来封装一波RecyclerView,也就是在RecyclerView的基础上自定义view了。 #####开始自定义view的第一步,继承RecyclerView 名字就是GalleryView

 //    代表直接在java代码中引用如setContentView(View)
    public GalleryView(Context context) {
        super(context);
        init();
    }

//    关联中的xml文件中当控件使用
    public GalleryView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

//    在xml引用,又要自己定义一些属性
    public GalleryView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

复制代码

#####第二步我们先来加载本地图片吧 首先在加载图片之前(为啥来个首先。。。难道还有好多么,哈哈,不是很多,但是你要定义一个bean吧,定义我们需要取哪些数据,哪些字段使我们需要的吧)

      /**
     * 图片Image   jvabean
     */
    private static class Image {
        int id;  //数据的id
        String path;  //图片的路径
        boolean isSelect; //图片是否选择
        long date; //图片创建的日期

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Image image = (Image) o;

            return path != null ? path.equals(image.path) : image.path == null;

        }

        @Override
        public int hashCode() {
            return path != null ? path.hashCode() : 0;
        }
    }

复制代码

加载本地并整合到集合中


    /**
     * 用于实际数据加载的Loader
     */
    private class LoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {


        //读取图片文件的参数
        private final String[] IMAGE_PROJECTION = {
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DATA,
                MediaStore.Images.Media.DATE_ADDED};

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            if (id == LOADER_ID) {
                return new CursorLoader(getContext(),
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION,
                        null, null, IMAGE_PROJECTION[2] + " DESC");
            }
            return null;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, final Cursor data) {
            //当Loader加载完成的时候回调方法
            List<Image> images = new ArrayList<>();
            if (data != null) {
                int count = data.getCount();
                if (count > 0) {
                    data.moveToFirst();
                    do {

                        //getColumnIndexOrThrow(String columnName)
                        //从零开始返回指定列名称,如果不存在将抛出IllegalArgumentException 异常
                        int id = data.getInt(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
                        //获取到图片本地地址
                        String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
                        //获取到照片的时间
                        long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));

                        File file = new File(path);
                        if (!file.exists() || file.length() < MIN_IMAGE_LEN)
                            continue;
                        //构建javabean
                        Image image = new Image();
                        image.id = id;
                        image.path = path;
                        image.date = dateTime;
                        
                        //添加到集合中
                        images.add(image);
                    } while (data.moveToNext());
                }
            }
            //加载完本地找之后进行更新资源
            updateSource(images);  
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            //当Loader销毁或者重置
            updateSource(null);
        }
    }

复制代码

因为LoaderManager需要配合Activity或者Fragment,所以我们需要对外提供一个方法来传入这两个的实例

/**
     * 初始化方法
     *
     * @param manager  LoaderManager Loader管理器
     * @param listener 选择改变监听
     * @return 任何一个LOADER_ID  可以用于销毁Loader
     */
    public int setup(LoaderManager manager, SelectedChangeListener listener) {
        mListener = listener;
        // 一个标识加载器的唯一ID    一个可选的参数以支持加载器的构建   一个LoaderManager.LoaderCallbacks的实现
        manager.initLoader(LOADER_ID, null, callback);
        return LOADER_ID;
    }
复制代码

相关变量

 private static final int LOADER_ID = 0x0100;
    private static final long MIN_IMAGE_LEN = 10 * 1024;  //最大的照片的大小  10MB
    private static final long MAX_IMAGE_COUNT = 9;  //最大选择的照片的数量
复制代码

关于这个方法我们在后面会有介绍updateSource(images)

#####图片已经加载到images里面了,该是我们的展示了 无非是写adapter和holder,然后inflater布局,绑定控件,然后设置数据

 private class GalleryAdapter extends RecyclerAdapter<Image> {

        @Override
        protected ViewHolder<Image> onCreateViewHolder(View root, int viewType) {
            return new GalleryView.ViewHolder(root);
        }

        @Override
        protected int getItemViewType(int position, Image image) {
            return R.layout.cell_gallery;
        }

    }
    
    
private class ViewHolder extends RecyclerAdapter.ViewHolder<Image> {
    //图片
    private ImageView mPic;
    //引用
    private View mShade;
    //checkbox
    private CheckBox mSelected;


    public ViewHolder(View itemView) {
        super(itemView);
        mPic = (ImageView) itemView.findViewById(R.id.im_image);
        mShade = itemView.findViewById(R.id.view_shade);
        mSelected = (CheckBox) itemView.findViewById(R.id.cb_select);
    }

    @Override
    protected void onBind(Image image) {
        //加载图片
        Glide.with(getContext())
                .load(image.path)
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .centerCrop()
                .placeholder(R.color.grey_200)
                .into(mPic);
        //设置选择阴影
        mShade.setVisibility(image.isSelect ? VISIBLE : INVISIBLE);
        //是否选择
        mSelected.setChecked(image.isSelect);
        //是否显示  未选择的图片checkbox不显示
        mSelected.setVisibility(image.isSelect ? VISIBLE : INVISIBLE);
    }
}


复制代码

大家肯定会说继承的RecyclerAdapter(还有一个泛型Image是什么鬼),这个不要急,是封装的一个RecyclerView的adapter,这个在文章的结尾会给个地址的(如果篇幅过长,不要打我哈)

 //四列图片
        setLayoutManager(new GridLayoutManager(getContext(), 4));
        setAdapter(mAdapter);  //设置adapter
复制代码

这个时候运行一下就是可以出现了哈,但是相关的点击逻辑我们还没有实现哦,现在抓紧时间来实现吧。想法比较简单,逻辑也不复杂哈。

首先是更新数据,也就是loader加载拿到的图片集合后更新数据

  /**
     * 更新选择的数据
     *
     * @param images 相册中的图片集合
     */
    private void updateSource(List<Image> images) {
        mAdapter.replace(images);
    }

复制代码

接下来就是item的点击实现,然后实现选择和未选择的逻辑

 mAdapter.setAdapterItemClickListener(new RecyclerAdapter.AdapterItemClickListener<Image>() {
            @Override
            public void onItemClick(RecyclerAdapter.ViewHolder holder, Image image) {
                if (onItemSelectClick(image)) {
                    //noinspection unchecked
                    holder.updateData(image);
                }
            }

            @Override
            public void onLongItemClick(RecyclerAdapter.ViewHolder holder, Image data) {

            }
        });
复制代码
 /**
     * item点击事件逻辑处理
     *
     * @param image 图片Item
     * @return true 选择  false 未选择
     */
    private boolean onItemSelectClick(Image image) {
        boolean notifyRefresh;
        //判断是否已经选择过了
        if (mSelectedImages.contains(image)) {
            //如果选择过了就移除这个image
            mSelectedImages.remove(image);
            //选择标志置为false
            image.isSelect = false;
            notifyRefresh = true; //需要刷新的标志置为true
        } else {
            //判断选择的总共大小是否超出了自定义的可选择大小
            if (mSelectedImages.size() >= MAX_IMAGE_COUNT) {
                //Cell点击操作  如果说我们的点击是允许的  那么更新对应的Cell状态
                //然后去更新界面  如果不允许点击(已经达到我们最大的选择数量)  那么就不需要刷新数据
                Application.showToast(String.format(
                        getResources().getText(R.string.label_gallery_select_max_size).toString(),
                        MAX_IMAGE_COUNT));
                //不需要刷新
                notifyRefresh = false;
            } else {
                //如果不在已选择集合中  那么就添加到集合中
                mSelectedImages.add(image);
                image.isSelect = true; //选择标志置为true
                notifyRefresh = true; //需要通知刷新
            }
        }
        //如果是需要刷新的  添加 或者删除都需要进行刷新
        if (notifyRefresh)  //通知刷新
            notifySelectChanged();
        return notifyRefresh;
    }

复制代码

通知刷新一下

 /**
     * 通知选择改变的时候刷新
     */
    private void notifySelectChanged() {
        SelectedChangeListener listener = mListener;
        if (listener != null)
            listener.onSelectedCountChanged(mSelectedImages.size());
    }
复制代码

因为我们有一个最大选择个数,这里定义一个接口,返回我们的选择个数

  /**
     * 图片选择监听器
     */
    public interface SelectedChangeListener {
        /**
         * 选择的个数监听器
         *
         * @param count 图片个数
         */
        void onSelectedCountChanged(int count);
    }
复制代码

因为我们最终还需要和界面进行交互,因此我们需要定义一个方法来让外部通过这个方法获取图片地址(简单的就是向外提供选择的图片集合的本地地址)

 /**
     * 获取到选择过的图片的路径
     *
     * @return 图片路径集合
     */
    public String[] getSelectedPath() {

        String[] paths = new String[mSelectedImages.size()];
        int index = 0;
        for (Image mSelectedImage : mSelectedImages) {
            paths[index++] = mSelectedImage.path;
        }
        return paths;
    }
复制代码

好了这里已经实现了,基本上也就是获取本地图片--->封装成我们需要的javabean--->使用recyclerview进行加载--->点击item--->改变item的状态(是否选中,显示checkbox)--->给外部暴露一个获取图片集合路径的方法。好了思路清晰,方法明了。实现也是比较简单。今天就到这了哈。关于安卓实现获取本机的所有图片的方法和解释,这里在参考阅读中给了地址。就不详细描述了。

###想说 鉴于本篇文章已经很长了,这里就不贴全部的代码和封装的recyclerview的代码了,我这里直接提供git地址,这是从一个完整的项目中提取出来的相关总结,大家也可以下载看下,有问题可以讨论。

相册选择自定义View--->GalleryView源码

封装的RecyclerViewAdapter--->RecyclerAdapter

Adapter接口--->AdapterCallBack

###参考阅读

 类似资料: