图片/视频混合选择器的简单实现

仿抖音图片/视频混合选择器

功能介绍

搜索所有视频和图片并混合显示,视频在右下角显示时长,点击时右上角显示当前为第几个选中的图片/视频,取消时补位。
这个选择器是基于RecyclerView来实现的
下面介绍关键类

QueryProcessor

QueryProcessor是用于异步查询所有图片/视频的类

public class QueryProcessor {

    //弱引用,防止内存泄漏
    private WeakReference<Context> context;
    private ContentResolver resolver;
    private Cursor cursor = null;

    public QueryProcessor(Context context) {
        this.context = new WeakReference<>(context);
        resolver = context.getContentResolver();
    }

    /**
     * 查询所有图片和视频
     * @param callback 查询完毕后的回调接口
     */
    public void queryAll(final QueryCallback callback) {
        if (context == null) {
            return;
        }
        //开辟一个新线程执行耗时查询
        new Thread(new Runnable() {
            @Override
            public void run() {
                ArrayList<MultiModel> multiModels = new ArrayList<>();
                queryImages(multiModels, callback);
                queryVideos(multiModels, callback);
                callback.querySuccess(multiModels);
                cursor.close();
            }
        }).start();
    }

    /**
     * 查询所有图片
     * @param multiModels 存放查询到的图片和视频的集合
     * @param callback    查询完毕后的回调接口
     */
    private void queryImages(ArrayList<MultiModel> multiModels, final QueryCallback callback) {
        cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null
                , null, null);
        if (cursor == null) {
            callback.queryFailed();
            return;
        }
        while (cursor.moveToNext()) {
            MultiModel multiModel = new MultiModel();
            multiModel.setPath(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)));
            multiModel.setDate(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)));
            multiModel.setType(0);
            multiModels.add(multiModel);
        }
    }

    /**
     * 查询所有视频
     * @param multiModels 存放查询到的图片和视频的集合
     * @param callback    查询完毕后的回调接口
     */
    private void queryVideos(ArrayList<MultiModel> multiModels, final QueryCallback callback) {
        cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null
                , null, null);
        if (cursor == null) {
            callback.queryFailed();
            return;
        }
        while (cursor.moveToNext()) {
            MultiModel multiModel = new MultiModel();
            multiModel.setPath(cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATA)));
            multiModel.setDate(cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATE_ADDED)));
            multiModel.setDuration(cursor.getLong(cursor.getColumnIndex(MediaStore.Video.Media.DURATION)));
            multiModel.setType(1);
            multiModels.add(multiModel);
        }
    }

    /**
     * 查询照片和视频结束后的回调接口
     */
    public interface QueryCallback {
        void querySuccess(ArrayList<MultiModel> multiModels);
        void queryFailed();
    }
}

GalleryAdapter

在Adapter中比较麻烦的一个是子View的点击事件,另一个就是点击已经选择过的处于中间位置的图片/视频时,其后面的图片/视频右上角的编号要依次补位,而这里又涉及到局部刷新,局部刷新的关键点就是notifyItemChanged(postion, payload)方法以及其回调onBindViewHolder(GalleryViewHolder holder, int position, List<Object> payloads)

public class GalleryAdapter extends RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder> {

    private Context context;
    //数据源,所有的图片/视频
    private List<MultiModel> data;
    //已选中的图片/视频
    private List<MultiModel> selectedModels;
    //记录已选中的图片在数据源中的索引位置
    private List<Integer> selectedPosList;
    //素材导入界面最上层显示的文字
    private TextView tvSelectedModelsNum;

    public GalleryAdapter(Context context, List<MultiModel> data, TextView tvSelectedModelsNum) {
        this.context = context;
        this.data = data;
        this.tvSelectedModelsNum = tvSelectedModelsNum;
        this.selectedModels = new ArrayList<>();
        this.selectedPosList = new ArrayList<>();
    }

    @NonNull
    @Override
    public GalleryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.gallery_item, null);
        return new GalleryViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull final GalleryViewHolder holder, int position) {
        MultiModel model = data.get(position);
        //使用Fresco加载图片
        FrescoHelper.INSTANCE.bindImage(holder.importPreview, "file://" + model.getPath()
                , new ResizeOptions((int)UIUtils.dip2Px(context, 88.5f)
                        , (int)UIUtils.dip2Px(context, 88.5f))
                , Priority.HIGH, null);
        //判断是图片还是视频,若是视频需要在右下角显示时长
        if (model.getType() == 1) {
            holder.importVideoDuration.setVisibility(View.VISIBLE);
            holder.importVideoDuration.setText(timeParse(model.getDuration()));
        } else {
            holder.importVideoDuration.setVisibility(View.GONE);
        }
        //当子项出现(或重新出现)到屏幕内时,需要判断其是否已经被选择,若已经被选择则需要将其置为对应位置;若不是被选择的图片/视频,需要取消其右上角的数字显示(在这里有坑,不判断的话在新刷出来的子项里会有之前选择过的图片的数字在新的图片的右上角,原因尚未明确)
        if (selectedModels.contains(model)) {
            int index = selectedModels.indexOf(model);
            holder.iconContainer.setBackgroundResource(R.drawable.bg_import_icon_container);
            holder.importSelectNumber.setText(context.getString(R.string.import_selected_icon_num
                    , index + 1));
            holder.importSelectNumber.setVisibility(View.VISIBLE);
        } else {
            holder.importSelectNumber.setVisibility(View.GONE);
            holder.importSelectNumber.setText("");
            holder.iconContainer.setBackgroundResource(R.drawable.ic_import_unselected);
        }
    }

    /**
    * 三个参数的onBind方法,实际上onBind方法首先都是回调三参的onBind,若payloads里面没有数据,则再回调两个参数的onBind。notifyItemChanged方法也是如此,首先回调的是三参的onBind。
    **/
    @Override
    public void onBindViewHolder(@NonNull GalleryViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
            return;
        }
        Bundle payload = (Bundle) payloads.get(0);
        holder.importSelectNumber.setText(context.getString(R.string.import_selected_num_change
                , payload.getInt("change")));
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    /**
     * 将以毫秒为单位的时长转化为分钟形式(如02:30为二分三十秒)
     * @param duration 视频时长
     * @return xx:xx
     */
    private String timeParse(long duration) {
        String time = "" ;
        long minute = duration / 60000 ;
        long seconds = duration % 60000 ;
        long second = Math.round((float)seconds/1000) ;
        if( minute < 10 ){
            time += "0" ;
        }
        time += minute+":" ;
        if( second < 10 ){
            time += "0" ;
        }
        time += second ;
        return time ;
    }

    /**
     * 素材导入界面RecyclerView的ViewHolder
     */
    class GalleryViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        //RecyclerView子项最外层布局
        private SimpleDraweeView importPreview;
        //RecyclerView子项右上角图标的container
        private FrameLayout iconContainer;
        //RecyclerView子项右上角点击的数字
        private TextView importSelectNumber;
        //RecyclerView子项右下角显示的视频的时长
        private TextView importVideoDuration;

        public GalleryViewHolder(View itemView) {
            super(itemView);
            importPreview = itemView.findViewById(R.id.iv_import_preview);
            iconContainer = itemView.findViewById(R.id.import_icon_container);
            importSelectNumber = itemView.findViewById(R.id.iv_import_select_number);
            importVideoDuration = itemView.findViewById(R.id.tv_import_video_duration);
            initListener();
        }

        private void initListener() {
            iconContainer.setOnClickListener(this);
        }

        /**
         * item右上角点击事件的逻辑
         * @param view
         */
        @Override
        public void onClick(View view) {
            switch (view.getId()) {
                case R.id.import_icon_container: {
                    MultiModel model = data.get(getAdapterPosition());
                    if (importSelectNumber.getVisibility() == View.GONE) {
                        selectedModels.add(model);
                        selectedPosList.add(getAdapterPosition());
                        iconContainer.setBackgroundResource(R.drawable.bg_import_icon_container);
                        importSelectNumber.setText(context.getString(R.string.import_selected_icon_num
                                , selectedModels.size()));
                        tvSelectedModelsNum.setText(context.getString(R.string.import_selected_count
                                , selectedModels.size()));
                        importSelectNumber.setVisibility(View.VISIBLE);
                    } else if (importSelectNumber.getVisibility() == View.VISIBLE){
                        int index = selectedModels.indexOf(model);
                        selectedModels.remove(model);
                        selectedPosList.remove(index);
                        iconContainer.setBackgroundResource(R.drawable.ic_import_unselected);
                        importSelectNumber.setText("");
                        tvSelectedModelsNum.setText(context.getString(R.string.import_selected_count
                                , selectedModels.size()));
                        importSelectNumber.setVisibility(View.GONE);
                        for (int i = index; i < selectedPosList.size(); i++) {
                            Bundle payload = new Bundle();
                            payload.putInt("change", i + 1);
                            //局部刷新,仅刷新被要被改动的子项
                            notifyItemChanged(selectedPosList.get(i), payload);
                        }
                    }
                    break;
                }
            }
        }
    }
}

RecyclerView的子项布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="2dp">

    <FrameLayout
        android:layout_width="88.5dp"
        android:layout_height="88.5dp">

        <com.facebook.drawee.view.SimpleDraweeView
            android:id="@+id/iv_import_preview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"/>

        <FrameLayout
            android:id="@+id/import_icon_container"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_gravity="end"
            android:layout_marginTop="4dp"
            android:layout_marginRight="4dp"
            android:layout_marginEnd="4dp"
            android:background="@drawable/ic_import_unselected">

            <TextView
                android:id="@+id/iv_import_select_number"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:textColor="#ffffff"
                android:layout_gravity="center"
                android:visibility="gone"/>

        </FrameLayout>

        <TextView
            android:id="@+id/tv_import_video_duration"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textColor="#ededed"
            android:gravity="end"
            android:layout_gravity="end|bottom"
            android:layout_marginRight="4dp"
            android:layout_marginEnd="4dp"
            android:layout_marginBottom="4dp"
            android:visibility="gone"/>

    </FrameLayout>

</LinearLayout>

在Activity中的用法

        //查询图片和视频并显示在RecyclerView上
        new QueryProcessor(this).queryAll(new QueryProcessor.QueryCallback() {
            @Override
            public void querySuccess(ArrayList<MultiModel> multiModels) {
                ImportActivity.this.galleryDataSource.addAll(multiModels);
                //将所有视频和图片按照创建时间进行一次排序,由最新的到最老的
                Collections.sort(ImportActivity.this.galleryDataSource);
                mAdapter = new GalleryAdapter(ImportActivity.this, ImportActivity.this.galleryDataSource,
                        tvSelectedModelsNum);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mGallery.setAdapter(mAdapter);
                    }
                });
            }

            @Override
            public void queryFailed() {
                Toast.makeText(ImportActivity.this, "视频或图片查询失败", Toast.LENGTH_SHORT).show();
            }
        });

效果基本和知乎开源的Matisse一致,就不放效果图了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近项目中用到多图选择上传的需求,考虑到android机型众多问题就自己花时间写了一个,测试了大概60款机型,出现过一些问题也都一一修复了,基本上稳定了特分享出来,界面UI也是商用级的开发者不用在做太多修改了,界面高度自定义,可以设置符合你项目主色调的风格,集成完成后就可以拿来用。 重要的事情说三遍记得添加权限 < uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> < uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> < uses-permission android:name="android.permission.CAMERA" /> 功能特点: 1.适配android7.0系统 2.解决部分机型裁剪闪退问题 3.解决图片过大oom闪退问题 4.动态获取系统权限,避免闪退 5.支持相片or视频的单选和多选 6.支持裁剪比例设置,如常用的 1:1、3:4、3:2、16:9 默认为图片大小 7.支持视频预览 8.支持gif图片 9.支持一些常用场景设置:如:是否裁剪、是否预览图片、是否显示相机等 10.新增自定义主题设置 11.新增图片勾选样式设置 12.新增图片裁剪宽高设置 13.新增图片压缩处理 14.新增录视频最大时间设置 15.新增视频清晰度设置 16.新增QQ选择风格,带数字效果 17.新增自定义 文字颜色 背景色让风格和项目更搭配 18.新增多图裁剪功能 19.新增LuBan多图压缩 20.新增单独拍照功能 javaapk之前也介绍过很多类似的项目,感兴趣的可以在javaapk图片处理分类中下载。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值