仿微信相册,一个功能完善的相册库,包含选择图片、视频;拍摄图片、视频;裁剪压缩、多选等内容

主要功能:

1、相册选择图片、视频,支持多选

2、自定义拍照、拍摄10s短视频

3、图片压缩、短视频裁剪、压缩,采用鲁班压缩算法

4、图片、视频预览功能,已在库中处理动态权限问题

请看效果:


hi.gif
Screenshot_20180302-142722.jpg

Screenshot_20180302-142819.jpg
Screenshot_20180302-142911.jpg
Screenshot_20180302-142849.jpg

Screenshot_20180302-142929.jpg
Screenshot_20180302-142954.jpg
Screenshot_20180302-143144.jpg

在第一张gif中没有展示拍照和拍视频,因为用的模拟器背景一坨黑,整个功能模块已经封装到library库中,只需要简单的调用便可以跑起来的啦,项目结构如下:


q.png

我们的主要功能逻辑、界面等均在picture_library中,后面smallVideLib2是一个拍摄小视频专用库,在picture_library中进行调用,视频处理逻辑是比较复杂的:

视频 :

拍摄视频

用smallVideLib2库进行拍摄,采用ffmpeg+x264方案,软编码,拍摄的样式大小可控,输出的就是我们想要的

选择视频

可能大部分为手机自带相机拍摄,基于android sdk MedieaCodec采用硬编码,一般都很大,而且不转码的情况下很多网络播放器无法播放,考虑到实际项目中上传等待时间以及流量的问题,必须进行裁剪二次压缩编码

视频处理流程

1、显示视频列表,大于5分钟的不显示
2、小于10秒可以直接进行压缩上传;大于10秒小于5分钟的视频必须要进行裁剪
3、裁剪过程实际上是对视频进行了 解码解压------拆分------合并------编码压缩 的过程,所以对这个过程通常会是一个耗时操作,微信相册也是如此
4、保存视频,回传视频保存地址

接下来看下功能比较多的picture_library库的结构:

pic.png

isoviewer-1.0-RC-27.jar 是一个视频编辑处理开源库,项目承担了视频裁剪和压缩的工作,jniLibs下面是一堆ffmpeg编译好的.so文件,在项目中我们两个地方需要用到它,一个是拍摄软编码、另一个则是裁剪时的二次编码,这里的工程还是比较大的,其中主要几个我说一哈:

CameraActivity.java:

拍照和拍摄视频界面,公用一个界面,这里点击是拍照,长按是拍视频

PictureEditAudioActivity.java:

视频编辑界面,也就是裁剪压缩界面

CameraActivity.java:

拍照和拍摄视频界面,公用一个界面,这里点击是拍照,长按是拍视频

PicturePreviewActivity.java:

图片viewpager预览界面,和视频界面公用一个界面

PictureVideoPlayActivity.java:

视频播放界面

PictureSelectorActivity.java:

图片选择库主界面,也是最重要的一个界面

还有少数页面当前项目没有使用这里就不解释了,而其他的类或者包里面的类都是为这些界面服务的,整个库数据都是通过一层层回调出去的,请在研究源码时注意注意!!!
至此,整体就比较清晰了,关于实现细节,由于内容比较多完全可以分为多篇文章来讲解,不是这一篇文章就能说得清楚得,就留给读者在下载来慢慢研究吧,最后给大家展示下外部使用代码吧:

/**
 * crate by yy on 2018-3-1
 * describle:图片、视频选择库,功能如下:
 * 1、相册选择图片、视频,支持多选
 * 2、自定义拍照、拍摄10s短视频
 * 3、图片压缩、短视频裁剪、压缩,采用鲁班压缩算法
 * 4、预览功能,已在库中处理动态权限问题
 */
public class MainActivity extends AppCompatActivity {

    Button picture, video;
    LinearLayout result;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        picture = findViewById(R.id.picture);
        video = findViewById(R.id.video);
        result = findViewById(R.id.result);

        picture.setOnClickListener(view -> initPictureSelector(PictureMimeType.ofImage()));
        video.setOnClickListener(view -> initPictureSelector(PictureMimeType.ofVideo()));
    }

    /**
     * 打开相册初始化,回传数据在onActivityResult方法中
     *
     * @param chooseMode 打开的类型
     */
    public void initPictureSelector(int chooseMode) {
        PictureSelector.create(this)
                .openGallery(chooseMode)// 全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()
                .theme(R.style.picture_default_style)// 主题样式设置 具体参考 libray中values/styles
                .maxSelectNum(9)// 最大图片选择数量
                .minSelectNum(1)// 最小选择数量
                .imageSpanCount(4)// 每行显示个数
                .selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 PictureConfig.MULTIPLE : PictureConfig.SINGLE
                .previewImage(true)// 是否可预览图片
                .previewVideo(true)// 是否可预览视频
                .enablePreviewAudio(false)// 是否预览音频
//                .compressGrade(Luban.THIRD_GEAR)// luban压缩档次,默认3档 Luban.FIRST_GEAR、Luban.CUSTOM_GEAR
                .isCamera(true)// 是否显示拍照按钮
                .isZoomAnim(true)// 图片列表点击 缩放效果 默认true
                .setOutputCameraPath(Constant.IMAGE_CACHE)// 自定义拍照保存路径
                .compress(true)// 是否压缩
                .compressMode(PictureConfig.LUBAN_COMPRESS_MODE)//系统自带 or 鲁班压缩 PictureConfig.SYSTEM_COMPRESS_MODE or LUBAN_COMPRESS_MODE
//                //.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
                .glideOverride(160, 160)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
                .isGif(false)// 是否显示gif图片
                .openClickSound(false)// 是否开启点击声音
//                .selectionMedia(selectList)// 是否传入已选图片
//                //.previewEggs(false)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中)
//                .compressGrade(Luban.CUSTOM_GEAR)
                .compressGrade(Luban.CUSTOM_GEAR)
                .compressMaxKB(1024)//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效
                .minimumCompressSize(500) //add by tanhaiqin, 图片大小 <= 500KB(数字可变) 不需要压缩
//                //.compressWH() // 压缩宽高比 compressGrade()为Luban.CUSTOM_GEAR有效
//                //.videoQuality()// 视频录制质量 0 or 1
                .videoSecond(5 * 60)//显示多少秒以内的视频
//                //.recordVideoSecond()//录制视频秒数 默认60秒
                .forResult(PictureConfig.CHOOSE_REQUEST);
    }

    /**
     * 处理 PictureSelectorActivity.java 返回的数据
     * 注意 图片压缩 已经是在picture lib中处理, 界面仅仅是展示获取的LocalMedia数据,不做再次压缩!
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case PictureConfig.CHOOSE_REQUEST:
                    // 图片选择,共用一个数据通道:返回时图片,可能为列表,视频只能有一个
                    List<LocalMedia> selectList = PictureSelector.obtainMultipleResult(data);
                    LogUtils.i("TEST===> selectList.size = " + selectList.size());
                    for (int i = 0; i < selectList.size(); i++) {
                        handleLocalMedia(selectList.get(i));
                    }
                    break;
            }
        }
    }

    private void handleLocalMedia(LocalMedia media) {
        int pictureType = PictureMimeType.isPictureType(media.getPictureType());
        switch (pictureType) {
            case PictureConfig.TYPE_IMAGE:
                LogUtils.e("TEST===> media path = " + media.getPath()
                        + ",  compressPath = " + media.getCompressPath()
                        + ", height = " + media.getHeight()
                        + ", width = " + media.getWidth());
                TextView textView = new TextView(this);
                textView.setText(" media path =" + media.getPath());
                result.addView(textView);
                break;
            case PictureConfig.TYPE_VIDEO:
                if (TextUtils.isEmpty(media.getPath())) return;
                if (!StringUtils.fileIsExists(media.getPath())) {
                    LogUtils.e("文件可能不存在了~");
                    return;
                }
                LogUtils.e("TEST===> video path = " + media.getPath()
                        + ",  compressPath = " + media.getCompressPath()
                        + ", height = " + media.getHeight()
                        + ", width = " + media.getWidth());
                TextView textView1 = new TextView(this);
                textView1.setText(" video path =" + media.getPath());
                result.addView(textView1);
                break;
        }
    }
}

使用上其实是炒鸡简单得。哈哈,需要自定义得话就得去改库了哈,特此说明此项目参考了许多其他项目,我把参考列在文章结尾。写本文章得目的也是为了把在上家公司做得比较难得部分独立备份出来,对自己,对他人都是一个帮助,因为当时写得时候网上去东拼西凑得,而且别人得demo还有不少得bug,说多了泪。。。
整个库以及测试代码我已上传github:
一个功能完善的相册库,类似微信朋友圈相册

希望大家多多支持,多多star,谢谢!!

参考

1、Picture Selector Library for Android or 多图片选择器
2、A Java API to read, write and create MP4 files
3、利用FFmpeg视频录制微信小视频与其压缩处理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,194评论 25 707
  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,584评论 1 180
  • 大家都看过《摔跤吧!爸爸》了吗?火辣君昨天晚上被朋友拉去撸了第二遍,虽然已经对剧情烂熟于心,但是依然感动得一塌糊涂...
    張向武阅读 153评论 0 1
  • 在下小白阅读 140评论 0 0
  • 冰冷的冬天即将划桨而来 文/葱葱 我需要 清理身体里腐败的根茎叶 再把自己晾晒在凋敝的树枝上 地上黄色的叶 嘲笑世...
    葱葱_阅读 443评论 46 35