Repository: SharryChoo/SAlbum Branch: release Commit: c41c1102996d Files: 228 Total size: 709.2 KB Directory structure: gitextract_4ugyik6s/ ├── .gitignore ├── README.md ├── SharryKey ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── release/ │ │ └── output.json │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── sharry/ │ │ └── app/ │ │ └── salbum/ │ │ ├── MainActivity.kt │ │ └── WatermarkPreviewerRenderer.java │ └── res/ │ ├── drawable/ │ │ ├── app_activity_main_launcher.xml │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ └── app_activity_main.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-zh/ │ │ └── strings.xml │ └── xml/ │ └── provider_paths.xml ├── assert/ │ └── SAlbum-1.0.1.apk ├── build.gradle ├── git ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── lib-album/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── base/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── ILoaderEngine.java │ │ ├── Loader.java │ │ └── MediaMeta.java │ ├── copper/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── CropperCallback.java │ │ ├── CropperCallbackLambda.java │ │ ├── CropperConfig.java │ │ ├── CropperFragment.java │ │ └── CropperManager.java │ ├── picker/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── FolderAdapter.java │ │ ├── FolderModel.java │ │ ├── PickerActivity.java │ │ ├── PickerAdapter.java │ │ ├── PickerCallback.java │ │ ├── PickerCallbackLambda.java │ │ ├── PickerConfig.java │ │ ├── PickerContract.java │ │ ├── PickerManager.java │ │ ├── PickerModel.java │ │ ├── PickerPresenter.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_album_picker_bottom_indicator.xml │ │ │ ├── ic_album_picker_camera_header.xml │ │ │ ├── ic_album_picker_fab.xml │ │ │ ├── ic_album_picker_gif.xml │ │ │ ├── ic_album_picker_right_arrow.xml │ │ │ ├── ic_album_picker_video_default.xml │ │ │ └── ic_album_picker_video_play.xml │ │ ├── layout/ │ │ │ ├── lib_album_activity_picker.xml │ │ │ ├── lib_album_recycle_item_folder.xml │ │ │ ├── lib_album_recycle_item_header_camera.xml │ │ │ ├── lib_album_recycle_item_picture.xml │ │ │ └── lib_album_recycle_item_video.xml │ │ ├── values/ │ │ │ ├── picker_colors.xml │ │ │ ├── picker_strings.xml │ │ │ └── picker_themes.xml │ │ └── values-zh/ │ │ └── picker_strings.xml │ ├── player/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── VideoPlayerActivity.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_album_player_video_pasue.xml │ │ │ └── ic_album_player_video_play.xml │ │ ├── layout/ │ │ │ └── lib_album_activity_video_player.xml │ │ ├── layout-land/ │ │ │ └── lib_album_activity_video_player.xml │ │ └── values/ │ │ └── player_color.xml │ ├── taker/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── AspectRatioFragment.java │ │ ├── ITakerContract.java │ │ ├── TakerActivity.java │ │ ├── TakerCallback.java │ │ ├── TakerCallbackLambda.java │ │ ├── TakerConfig.java │ │ ├── TakerManager.java │ │ ├── TakerPresenter.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_album_taker_aspect.xml │ │ │ ├── ic_album_taker_camera_switch.xml │ │ │ ├── ic_album_taker_denied.xml │ │ │ ├── ic_album_taker_full_screen.xml │ │ │ └── ic_album_taker_granted.xml │ │ ├── layout/ │ │ │ └── lib_ablum_activity_taker.xml │ │ ├── values/ │ │ │ ├── taker_colors.xml │ │ │ └── taker_strings.xml │ │ └── values-zh/ │ │ └── taker_strings.xml │ ├── utils/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── ActivityStateUtil.java │ │ ├── CallbackFragment.java │ │ ├── ColorUtil.java │ │ ├── CompressUtil.java │ │ ├── Constants.java │ │ ├── DateUtil.java │ │ ├── DensityUtil.java │ │ ├── FileUtil.java │ │ ├── PermissionsCallback.java │ │ ├── PermissionsFragment.java │ │ ├── PermissionsHelper.java │ │ ├── Preconditions.java │ │ ├── SharedElementHelper.java │ │ └── VersionUtil.java │ ├── watcher/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── album/ │ │ ├── DisplayAdapter.java │ │ ├── PickedPanelAdapter.java │ │ ├── WatcherActivity.java │ │ ├── WatcherCallback.java │ │ ├── WatcherCallbackLambda.java │ │ ├── WatcherConfig.java │ │ ├── WatcherContract.java │ │ ├── WatcherFragment.java │ │ ├── WatcherManager.java │ │ ├── WatcherPresenter.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_album_watcher_right_arrow.xml │ │ │ └── ic_album_watcher_video_play.xml │ │ ├── layout/ │ │ │ ├── lib_album_activity_watcher.xml │ │ │ └── lib_album_fragment_watcher_pager.xml │ │ ├── values/ │ │ │ ├── watcher_colors.xml │ │ │ ├── watcher_strings.xml │ │ │ └── watcher_themes.xml │ │ └── values-zh/ │ │ └── watcher_strings.xml │ └── widget/ │ └── com/ │ └── sharry/ │ └── lib/ │ └── album/ │ ├── CheckedIndicatorView.java │ ├── DraggableViewPager.java │ ├── PicturePickerFabBehavior.java │ ├── RecorderButton.java │ ├── photoview/ │ │ ├── Compat.java │ │ ├── CustomGestureDetector.java │ │ ├── OnGestureListener.java │ │ ├── OnMatrixChangedListener.java │ │ ├── OnOutsidePhotoTapListener.java │ │ ├── OnPhotoTapListener.java │ │ ├── OnScaleChangedListener.java │ │ ├── OnSingleFlingListener.java │ │ ├── OnViewDragListener.java │ │ ├── OnViewTapListener.java │ │ ├── PhotoView.java │ │ ├── PhotoViewAttacher.java │ │ └── Util.java │ └── toolbar/ │ ├── AppBarHelper.java │ ├── Builder.java │ ├── ImageViewOptions.java │ ├── Options.java │ ├── SToolbar.java │ ├── Style.java │ ├── TextViewOptions.java │ ├── Utils.java │ ├── ViewOptions.java │ └── res/ │ └── values/ │ └── lib_toolbar_attrs.xml ├── lib-media-recorder/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── Readme.markdown │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── api/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── media/ │ │ └── recorder/ │ │ ├── IMediaRecorder.java │ │ ├── IRecorderCallback.java │ │ ├── Options.java │ │ └── SMediaRecorder.java │ ├── cpp/ │ │ ├── ConstDefine.h │ │ ├── JNICall.cpp │ │ ├── JNICall.h │ │ ├── OpenSLRecorder.cpp │ │ ├── OpenSLRecorder.h │ │ ├── RecordBuffer.cpp │ │ ├── RecordBuffer.h │ │ └── native-bridge-recorder.cpp │ ├── encoder/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── media/ │ │ └── recorder/ │ │ ├── AACEncoder.java │ │ ├── EncodeType.java │ │ ├── EncoderFactory.java │ │ ├── H264Encoder.java │ │ ├── H264Render.java │ │ ├── IAudioEncoder.java │ │ └── IVideoEncoder.java │ ├── muxer/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── media/ │ │ └── recorder/ │ │ ├── IMuxer.java │ │ ├── MPEG4Muxer.java │ │ ├── MuxerFactory.java │ │ └── MuxerType.java │ ├── pcmprovider/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── media/ │ │ └── recorder/ │ │ ├── DefaultPCMProvider.java │ │ ├── IPCMProvider.java │ │ └── OpenSLESPCMProvider.java │ ├── recorder/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── media/ │ │ └── recorder/ │ │ ├── AudioRecorder.java │ │ ├── BaseMediaRecorder.java │ │ └── VideoRecorder.java │ └── utils/ │ └── com/ │ └── sharry/ │ └── lib/ │ └── media/ │ └── recorder/ │ ├── AVPoolExecutor.java │ ├── FileUtil.java │ ├── NetworkUtil.java │ └── VersionUtil.java ├── lib-opengles/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── opengles/ │ │ ├── surface/ │ │ │ └── ContextSharedGLSurfaceView.java │ │ ├── texture/ │ │ │ ├── GLTextureView.java │ │ │ └── ITextureRenderer.java │ │ └── util/ │ │ ├── EglCore.java │ │ ├── FboHelper.java │ │ ├── GlMatrixUtil.java │ │ └── GlUtil.java │ └── utils/ │ └── com/ │ └── sharry/ │ └── lib/ │ └── opengles/ │ ├── EglCore.java │ └── GlUtil.java ├── lib-scamera/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── api/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── camera/ │ │ └── SCameraView.java │ ├── common/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── camera/ │ │ ├── AspectRatio.java │ │ ├── CameraContext.java │ │ ├── Constants.java │ │ ├── Size.java │ │ └── SizeMap.java │ ├── device/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── camera/ │ │ ├── AbsCameraDevice.java │ │ ├── Camera1Device.java │ │ └── ICameraDevice.java │ ├── orientation/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── camera/ │ │ └── ScreenOrientationDetector.java │ ├── previewer/ │ │ └── com/ │ │ └── sharry/ │ │ └── lib/ │ │ └── camera/ │ │ ├── DefaultPreviewerRenderer.java │ │ ├── IPreviewer.java │ │ ├── Previewer.java │ │ ├── PreviewerRendererImpl.java │ │ ├── PreviewerRendererWrapper.java │ │ └── ScaleType.java │ └── res/ │ ├── raw/ │ │ ├── camera_fragment_shader.glsl │ │ └── camera_vertex_shader.glsl │ └── values/ │ ├── attrs.xml │ ├── public.xml │ └── styles.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # Intellij *.iml .idea # Keystore files *.jks ================================================ FILE: README.md ================================================ ## SAlbum SAlbum 是一款对 Android 端提供 **图片的选取、裁剪、拍摄和短视频录制等功能的图库框架** ## 功能介绍 - **图片的选取** - 支持 JPEG/PNG/WEBP/GIF 的选取 - 图片加载引擎由用户自定义实现 - **图片的浏览** - 共享元素跳转动画 - **图像的裁剪** - **相机的拍摄** - ~~CameraX-alpha4 尺寸选取存在问题, 暂时移除~~ - 提供 1:1、4:3、16:9 的比例选择 - 支持 CenterCrop 全屏预览 - 通过自定义 Renderer, 可拓展水印滤镜等效果 - **视频的录制** - 视频 - 使用 MediaCodec 实现 H.264 的硬编 - 支持 1080p, 720p, 480p 的录制分辨率 - 音频 - PCM 数据获取使用 OpenSL ES, 支持 v7a - 使用 MediaCodec 硬编为 AAC - 使用 MediaMuxer 合并为 mp4 文件 - **视频的播放** - 考虑到依赖体积, 使用系统提供的 VideoView 实现 - **已支持 Android 10** - Android 10 不支持随意访问外部存储 Storage 中的文件, 可通过 URI 进行图片加载 实现原理请查看 [wiki](https://github.com/SharryChoo/SAlbum/wiki) ## 功能集成 [![](https://jitpack.io/v/SharryChoo/SAlbum.svg)](https://jitpack.io/#SharryChoo/SAlbum) ### Step 1 Add it in your **module build.gradle** at the end of repositories ``` dependencies { ... // SAlbum dependency implementation 'com.github.SharryChoo:SAlbum:+' // Need Android dependencies implementation "androidx.constraintlayout:constraintlayout:+" implementation "androidx.appcompat:appcompat:+" implementation "androidx.recyclerview:recyclerview:+" implementation "com.google.android.material:material:+" } ``` ### Step 2 Add it in your **root build.gradle** at the end of repositories ``` allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` ## 效果展示 下载体验 [Demo](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/SAlbum-1.0.1.apk) ### 资源选取 ![资源选取](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/PicturePicker.jpg) ### 图像拍摄 ![图像拍摄](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/PictureTaker.png) ### 视频录制 ![视频录制](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/VideoRecord.png) ### 视频播放 ![视频的播放](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/VideoPlay.jpg) ## 使用指南 SPicturePicker 的所有功能提供, 均通过 **Manager** 对外提供, 其具体的功能选项通过 **Config** 来配置 功能 | Manager | Config :---:|:---:|:---: 选取 | PickerManager | PickerConfig 浏览 | WatcherManager | WatcherConfig 拍摄/录像 | TakerManager | TakerConfig 裁剪 | CropperManager | CropperConfig ### 一) 选取 ``` PickerManager.with(context) // 注入配置 .setPickerConfig( PickerConfig.Builder() // Toolbar 背景设置 .setToolbarBackgroundColor( ContextCompat.getColor(this, R.color.colorPrimary) ) // 指示器填充色 .setIndicatorSolidColor( ContextCompat.getColor(this, R.color.colorPrimary) ) // 选中指示器的颜色 .setIndicatorBorderColor( ContextCompat.getColor(this, R.color.colorPrimary), ContextCompat.getColor(this, android.R.color.white) ) // 指示器边界的颜色 .setPickerItemBackgroundColor( ContextCompat.getColor(this, android.R.color.white) ) // 阈值 .setThreshold(etAlbumThreshold.text.toString().toInt()) // 每行展示的数量 .setSpanCount(etSpanCount.text.toString().toInt()) // 是否开启 Toolbar Behavior 动画 .isToolbarScrollable(cbAnimation.isChecked) // 是否开启 Fab Behavior 动画 .isFabScrollable(cbAnimation.isChecked) // 是否选择 GIF 图 .isPickGif(cbGif.isChecked) // 是否选择视频 .isPickVideo(cbVideo.isChecked) // 注入用户已选中的图片集合 .setUserPickedSet(mPickedSet) // 设置相机配置, 非 null 说明支持相机(拍摄/录制) .setCameraConfig( if (cbCamera.isChecked) takerConfig else null ) // 设置裁剪配置, 非 null 说明支持裁剪 .setCropConfig( if (cbCrop.isChecked) cropperConfig else null ) .build() ) // 加载框架注入 .setLoaderEngine( object : ILoaderEngine { override fun loadPicture(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView) } override fun loadGif(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asGif().load(mediaMeta.contentUri).into(imageView) } override fun loadVideoThumbnails(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView) } } ) .start { // TODO 选中的资源, 通过 ArrayList 返回 } ``` 选取的方式如上所示, **首先按照需求构建 Config**, **然后注入图片加载的引擎**, 之后便可以在 start 的回调中获取到选中的图片资源了 - 关于相机 - 在 PickerConfig 中传入相机的配置, 则意为开启相机的功能 - 关于裁剪 - 在 PickerConfig 中传入裁剪的配置, 则意为开启裁剪的功能 ### 二) 浏览 浏览的功能与选取类似, 打开图片选择器时, 会根据 PickerConfig 自动生成浏览的配置, 若想在外界单独使用图片浏览的功能, 可以通过以下方式 ``` WatcherManager.with(context) .setConfig( WatcherConfig.Builder() // 配置 Indicator 的展示效果 .setIndicatorTextColor(mPickerConfig.getIndicatorTextColor()) .setIndicatorSolidColor(mPickerConfig.getIndicatorSolidColor()) .setIndicatorBorderColor( mPickerConfig.getIndicatorBorderCheckedColor(), mPickerConfig.getIndicatorBorderUncheckedColor() ) // 注入需要展示的图片 .setDisplayDataSet(mPickedSet, 0) // 设置最大选中数量, 若 > 0, 则说明图片查看器也支持选取的功能 .setThreshold(mPickerConfig.getThreshold()) // 注入用户选中的图片集合 .setUserPickedSet(mPickedSet) .build(); ) // 注入共享元素 .setSharedElement(sharedElement) // 注入图片加载器 .setLoaderEngine(Loader.getPictureLoader()) .startForResult(this); ``` 可以看到浏览的使用主要区别在于, 增加了 **共享元素(支持 5.0 以下的操作系统)** 的选项 - 当 threshold > 0 时, 表示需要为图片浏览添加图片选择功能, 反之仅做图片查看使用 ### 三) 拍摄 相机的使用与浏览类似, 可以集成在 Picker 中使用, 也可以单独使用 ``` TakerManager.with(context) .setConfig( TakerConfig.Builder() // 设置外部存储目录相对路径 .setRelativePath(RELATIVE_PATH) // 指定 FileProvider 的 authority, 用于获取文件 URI .setAuthority(FILE_PROVIDER) // 预览画面比例, 支持 1:1, 4:3, 16:9 .setPreviewAspect(ASPECT_1_1) // 是否全屏预览(在比例基础上进行 CenterCrop, 保证画面不畸形) .setFullScreen(false) // 设置自定义 Renderer 的路径 .setRenderer(WatermarkPreviewerRenderer::class.java) // 设置是否支持视频录制 .setVideoRecord(true) // 设置录制最大时长 .setMaxRecordDuration(15 * 1000) // 设置录制最短时长 .setMinRecordDuration(1 * 1000) // 设置录制的分辨率 .setRecordResolution(Options.Video.RESOLUTION_720P) // 拍摄后质量压缩 .setPictureQuality(80) // 注入裁剪配置, 非 null, 表示拍摄之后进行图片的裁剪 .setCropConfig(...) .build() ) .take(this); ``` 其中的注释比较清晰, 操作完成之后, 可通过回调获取到拍摄/录制的结果 #### 1. RelativePath Andorid 10 以后, 无法随意的在外部存储卡中创建文件, 因此使用了 RelativePath ``` // 绝对路径 "/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum" // 相对路径 "SAlbum" ``` 只需要设置了相对路径, SAlbum 会自动在 Android 媒体文件夹下创建工程的文件夹, 所有拍摄录制的图片均会保存在其中, 这也是 Android 希望我们遵守的规范 #### 2. Authority 需要获取文件的 URI, 7.0 之后获取 URI 需要通过 FileProvider, 因此这里需要传入 FileProvider 的 authority, 关于这一块网上的资料比较多, 这里就不再赘述了 #### 3. Camera 渲染器 **关于自定义 Camera 的渲染器, 需要用户自定义实现 IPreviewer.Renderer 这个接口**, Demo 中提供了一个水印效果的渲染器滤镜, 可以其参考实现自己的渲染器 ``` public Builder setRenderer(@NonNull Class rendererClass) { try { rendererClass.getDeclaredConstructor(Context.class); } catch (NoSuchMethodException e) { throw new UnsupportedOperationException("Please ensure " + rendererClass.getSimpleName() + " have a constructor like: " + rendererClass.getSimpleName() + "(Context context)"); } mConfig.rendererClsName = rendererClass.getName(); return this; } ``` 传入渲染器实现的 class 文件, 需要保证提供一个参数为 Context 的构造方法, 否则在构建 TakerConfig 时会出现异常 ### 四) 裁剪 ``` CropperManager.with(context) .setConfig( CropperConfig.Builder() // 要裁剪的图片的 URI .setOriginUri(...) // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI .setAuthority(FILE_PROVIDER) // 设置外部存储目录相对路径 .setRelativePath(RELATIVE_PATH) // 裁剪期望的尺寸 .setCropSize(1000, 1000) // 裁剪后的质量 .setCropQuality(80) .build() ) .crop(this); ``` 裁剪目前使用系统提供的裁剪方式, 其使用方式也比较简单, 这里就不再赘述了 更多功能请查看工程中提供的[示例](https://github.com/SharryChoo/SAlbum/blob/release/app/src/main/java/com/sharry/app/salbum/MainActivity.kt) ## 致谢 [PhotoView](https://github.com/chrisbanes/PhotoView) ================================================ FILE: app/.gitignore ================================================ /build # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # Intellij *.iml .idea/workspace.xml # Keystore files *.jks ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.compileSdkVersion defaultConfig { minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion vectorDrawables.useSupportLibrary true externalNativeBuild { ndk { abiFilters "armeabi-v7a" } } } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" // Google dependencies def constraintlayoutVersion = "1.1.3" implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion" def supportLibraryVersion = '1.1.0' implementation "androidx.appcompat:appcompat:$supportLibraryVersion" def recycleViewVersion = '1.0.0' implementation "androidx.recyclerview:recyclerview:$recycleViewVersion" def materialVersion = '1.0.0' implementation "com.google.android.material:material:$materialVersion" // Glide dependencies def glideVersion = '4.6.1' implementation "com.github.bumptech.glide:glide:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion" // SPicturePicker dependencies // implementation 'com.github.SharryChoo:SAlbum:1.0.0' api project(':lib-album') } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\Android\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/release/output.json ================================================ [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":-1,"enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/sharry/app/salbum/MainActivity.kt ================================================ package com.sharry.app.salbum import android.content.Context import android.os.Bundle import android.text.TextUtils import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.bumptech.glide.Glide import com.sharry.lib.album.* import com.sharry.lib.album.TakerConfig.ASPECT_4_3 import com.sharry.lib.album.toolbar.SToolbar import com.sharry.lib.media.recorder.Options import kotlinx.android.synthetic.main.app_activity_main.* /** * SAlbum 示例 Activity. * * @author Sharry Contact me. * @version 1.0 * @since 12/6/2018 10:49 AM */ private const val FILE_PROVIDER = "com.sharry.app.salbum.FileProvider" private const val RELATIVE_PATH = "SAlbum" class MainActivity : AppCompatActivity() { /** * 用与图片选取的配置 */ private lateinit var pickerConfig: PickerConfig /** * 用与相机拍摄的配置 */ private val takerConfig = TakerConfig.Builder() // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI .setAuthority(FILE_PROVIDER) // 设置外部存储目录相对路径 .setRelativePath(RELATIVE_PATH) // 预览画面比例 .setPreviewAspect(ASPECT_4_3) // 是否全屏预览(在比例基础上进行 CenterCrop, 保证画面不畸形) .setFullScreen(true) // 设置自定义 Renderer 的实现类 .setRenderer(WatermarkPreviewerRenderer::class.java) // 设置是否支持视频录制 .setVideoRecord(true) // 是否仅支持视频录制 .setJustVideoRecord(false) // 设置录制最大时长 .setMaxRecordDuration(15 * 1000) // 设置录制最短时长 .setMinRecordDuration(1 * 1000) // 设置录制的分辨率 .setRecordResolution(Options.Video.RESOLUTION_1080P) // 拍摄后质量压缩 .setPictureQuality(80) .build() /** * 用与裁剪的配置 */ private val cropperConfig = CropperConfig.Builder() // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI .setAuthority(FILE_PROVIDER) // 设置外部存储目录相对路径 .setRelativePath(RELATIVE_PATH) // 裁剪期望的尺寸 .setCropSize(1000, 1000) // 裁剪后的质量 .setCropQuality(80) .build() /** * 图片加载器 * * 注: Android 10 以后需要使用 URI 进行加载操作 */ private val pictureLoader = object : ILoaderEngine { override fun loadPicture(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView) } override fun loadGif(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asGif().load(mediaMeta.contentUri).into(imageView) } override fun loadVideoThumbnails(context: Context, mediaMeta: MediaMeta, imageView: ImageView) { // Android 10 以后, 需要使用 URI 进行加载 Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.app_activity_main) initTitle() initViews() initData() } private fun initTitle() { SToolbar.Builder(this) .setBackgroundColorRes(R.color.colorPrimary) .setTitleText(getString(R.string.app_name)) .apply() } private fun initViews() { btnLaunchAlbum.setOnClickListener { _ -> if (TextUtils.isEmpty(etAlbumThreshold.text) || TextUtils.isEmpty(etSpanCount.text)) { return@setOnClickListener } openAlbum() } } private fun initData() { pickerConfig = PickerConfig.Builder() // Toolbar 背景设置 .setToolbarBackgroundColor(ContextCompat.getColor(this, R.color.colorPrimary)) // 指示器填充色 .setIndicatorSolidColor(ContextCompat.getColor(this, R.color.colorPrimary)) // 指示器边界的颜色 .setPickerItemBackgroundColor(ContextCompat.getColor(this, android.R.color.white)) // 选中指示器的颜色 .setIndicatorBorderColor( ContextCompat.getColor(this, R.color.colorPrimary), ContextCompat.getColor(this, android.R.color.white) ) .build() } private fun openAlbum() { // 根据选择中数据重新构建 pickerConfig. pickerConfig.rebuild() // 阈值 .setThreshold(etAlbumThreshold.text.toString().toInt()) // 每行展示的数量 .setSpanCount(etSpanCount.text.toString().toInt()) // 是否开启 Toolbar Behavior 动画 .isToolbarScrollable(cbAnimation.isChecked) // 是否开启 Fab Behavior 动画 .isFabScrollable(cbAnimation.isChecked) // 是否选择图片 .isPickPicture(cbPicture.isChecked) // 是否选择 GIF 图 .isPickGif(cbGif.isChecked) // 是否选择视频 .isPickVideo(cbVideo.isChecked) // 设置相机配置, 非 null 说明支持相机(拍摄/录制) .setCameraConfig(if (cbCamera.isChecked) takerConfig else null) // 设置裁剪配置, 非 null 说明支持裁剪 .setCropConfig(if (cbCrop.isChecked) cropperConfig else null) .build() PickerManager.with(this) // 设置选择配置文件 .setPickerConfig(pickerConfig) // 图片加载框架注入 .setLoaderEngine(pictureLoader) // 开始选取 .start { it?.forEach { Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show() } } } } ================================================ FILE: app/src/main/java/com/sharry/app/salbum/WatermarkPreviewerRenderer.java ================================================ package com.sharry.app.salbum; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES20; import android.opengl.GLUtils; import com.sharry.lib.camera.PreviewerRendererImpl; import com.sharry.lib.camera.PreviewerRendererWrapper; import com.sharry.lib.opengles.util.FboHelper; import com.sharry.lib.opengles.util.GlUtil; import java.nio.FloatBuffer; /** * 带水印效果的渲染器 *

* * @author Sharry Contact me. * @version 1.0 * @since 2019-08-01 16:04 */ public class WatermarkPreviewerRenderer extends PreviewerRendererWrapper { private static final String VERTEX_SHADER_STR = "attribute vec4 aVertexPosition;\n" + " attribute vec2 aTexturePosition;\n" + " varying vec2 vPosition;\n" + " void main() {\n" + " vPosition = aTexturePosition;\n" + " gl_Position = aVertexPosition;\n" + " }"; private static final String FRAGMENT_SHADER_STR = "precision mediump float;\n" + "varying vec2 vPosition;\n" + "uniform sampler2D uTexture;\n" + "void main() {\n" + " gl_FragColor=texture2D(uTexture, vPosition);\n" + "}"; /** * 相机顶点坐标 */ private final float[] mCameraVertexCoords = new float[]{ -1f, 1f, // 左上 -1f, -1f, // 左下 1f, 1f, // 右上 1f, -1f, // 右下 }; /** * 相机纹理映射坐标 */ private final float[] mCameraTextureCoords = new float[]{ 0f, 1f, // 左上 0f, 0f, // 左下 1f, 1f, // 右上 1f, 0f // 右下 }; /** * 水印顶点坐标 */ private final float[] mWatermarkVertexCoords = new float[]{ 0f, 0f, // 左上 0f, 0f, // 左下 0f, 0f, // 右上 0f, 0f, // 右下 }; /** * 水印纹理坐标, 水印从 Bitmap 中加载, 坐标系相反 */ private final float[] mWatermarkTextureCoords = new float[]{ 0f, 0f, // 左下 0f, 1f, // 左上 1f, 0f, // 右下 1f, 1f // 右上 }; /** * 相机纹理顶点和纹理坐标 */ private final FloatBuffer mCameraTextureVertexBuffer = GlUtil.createFloatBuffer(mCameraVertexCoords); private final FloatBuffer mCameraTextureBuffer = GlUtil.createFloatBuffer(mCameraTextureCoords); /** * 水印纹理顶点和纹理坐标 */ private final FloatBuffer mWatermarkVertexBuffer = GlUtil.createFloatBuffer(mWatermarkVertexCoords); private final FloatBuffer mWatermarkTextureBuffer = GlUtil.createFloatBuffer(mWatermarkTextureCoords); private final Context mContext; private final FboHelper mFboHelper; private int mProgramId; private int aVertexPosition; private int aTexturePosition; private int mVboId; private int uTexture; private int mWatermarkTextureId = 0; private Bitmap mWatermarkBitmap; public WatermarkPreviewerRenderer(Context context) { super(new PreviewerRendererImpl(context)); this.mContext = context; this.mFboHelper = new FboHelper(); } @Override public void onAttach() { super.onAttach(); mFboHelper.onAttach(); // 初始化程序 setupShaders(); // 初始化顶点坐标 setupCoordinates(); // 初始化水印纹理 setupWatermarkTexture(); } private void setupShaders() { mProgramId = GlUtil.createProgram(VERTEX_SHADER_STR, FRAGMENT_SHADER_STR); aVertexPosition = GLES20.glGetAttribLocation(mProgramId, "aVertexPosition"); aTexturePosition = GLES20.glGetAttribLocation(mProgramId, "aTexturePosition"); uTexture = GLES20.glGetUniformLocation(mProgramId, "uTexture"); } private void setupCoordinates() { // 创建 vbo int vboSize = 1; int[] vboIds = new int[vboSize]; GLES20.glGenBuffers(vboSize, vboIds, 0); // 将顶点坐标写入 vbo mVboId = vboIds[0]; GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId); // 开辟 VBO 空间 GLES20.glBufferData( GLES20.GL_ARRAY_BUFFER, mCameraVertexCoords.length * 4 + mCameraTextureCoords.length * 4 + mWatermarkVertexCoords.length * 4 + mWatermarkTextureCoords.length * 4, null, GLES20.GL_STATIC_DRAW ); // 写入相机顶点坐标 GLES20.glBufferSubData( GLES20.GL_ARRAY_BUFFER, 0, mCameraVertexCoords.length * 4, mCameraTextureVertexBuffer ); // 写入相机纹理坐标 GLES20.glBufferSubData( GLES20.GL_ARRAY_BUFFER, mCameraVertexCoords.length * 4, mCameraTextureCoords.length * 4, mCameraTextureBuffer ); // 写入水印顶点坐标 GLES20.glBufferSubData( GLES20.GL_ARRAY_BUFFER, mCameraVertexCoords.length * 4 + mCameraTextureCoords.length * 4, mWatermarkVertexCoords.length * 4, mWatermarkVertexBuffer ); // 写入水印纹理坐标 GLES20.glBufferSubData( GLES20.GL_ARRAY_BUFFER, mCameraVertexCoords.length * 4 + mCameraTextureCoords.length * 4 + mWatermarkVertexCoords.length * 4, mWatermarkTextureCoords.length * 4, mWatermarkTextureBuffer ); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } private void setupWatermarkTexture() { int[] textureIds = new int[1]; GLES20.glGenTextures(1, textureIds, 0); mWatermarkTextureId = textureIds[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mWatermarkTextureId); // 设置纹理环绕方式 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); // 设置纹理过滤方式 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); // 创建 Bitmap, 将其写入纹理 mWatermarkBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_demo_watermark); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mWatermarkBitmap, 0); // 解绑 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); } @Override public void onSizeChanged(int width, int height) { super.onSizeChanged(width, height); mFboHelper.onSizeChanged(width, height); // 启用透明 GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); GLES20.glViewport(0, 0, width, height); // 更新水印坐标 updateWatermarkCoors(width, height); } private void updateWatermarkCoors(int surfaceWidth, int surfaceHeight) { float height = mWatermarkBitmap.getHeight(); float width = mWatermarkBitmap.getWidth(); height = height * (1 / (float) surfaceHeight); width = width * (1 / (float) surfaceWidth); float left = -0.9f; float bottom = -0.9f; // 设置水印的位置 // 左上 mWatermarkVertexCoords[0] = left; mWatermarkVertexCoords[1] = bottom + height; // 左下 mWatermarkVertexCoords[2] = left; mWatermarkVertexCoords[3] = bottom; // 右上 mWatermarkVertexCoords[4] = left + width; mWatermarkVertexCoords[5] = bottom + height; // 右下 mWatermarkVertexCoords[6] = left + width; mWatermarkVertexCoords[7] = bottom; // 更新 Buffer mWatermarkVertexBuffer.put(mWatermarkVertexCoords, 0, mWatermarkVertexCoords.length) .position(0); // 更新 VBO GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId); // 写入水印顶点坐标 GLES20.glBufferSubData( GLES20.GL_ARRAY_BUFFER, mCameraVertexCoords.length * 4 + mCameraTextureCoords.length * 4, mWatermarkVertexCoords.length * 4, mWatermarkVertexBuffer ); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } @Override protected void onDrawTexture(int textureId) { mFboHelper.bindFramebuffer(); // 绘制纹理 drawOriginTexture(textureId); // 绘制水印 drawWatermark(); // 解绑 mFboHelper.unbindFramebuffer(); // 绘制到系统自带的缓冲上 drawToEGLSurface(); } private void drawOriginTexture(int textureId) { GLES20.glUseProgram(mProgramId); // 绑定相机的纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); // 写入顶点坐标 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId); GLES20.glEnableVertexAttribArray(aVertexPosition); GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false, 8, 0); // 写入纹理坐标 GLES20.glEnableVertexAttribArray(aTexturePosition); GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false, 8, mCameraVertexCoords.length * 4); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); // 给 uTexture 赋值 GLES20.glUniform1i(uTexture, 0); // 绘制到屏幕 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 解绑纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); } private void drawWatermark() { GLES20.glUseProgram(mProgramId); // 绑定纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mWatermarkTextureId); // 写入水印顶点坐标 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId); GLES20.glEnableVertexAttribArray(aVertexPosition); GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false, 8, (mCameraVertexCoords.length + mCameraTextureCoords.length) * 4); // 写入水印纹理坐标 GLES20.glEnableVertexAttribArray(aTexturePosition); GLES20.glVertexAttribPointer( aTexturePosition, 2, GLES20.GL_FLOAT, false, 8, (mCameraVertexCoords.length + mCameraTextureCoords.length + mWatermarkVertexCoords.length) * 4 ); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); // 给 uTexture 赋值 GLES20.glUniform1i(uTexture, 0); // 绘制到屏幕 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 解绑纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); } private void drawToEGLSurface() { GLES20.glUseProgram(mProgramId); // 绑定纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getPreviewerTextureId()); // 写入顶点坐标 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId); GLES20.glEnableVertexAttribArray(aVertexPosition); GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false, 8, 0); // 写入纹理坐标 GLES20.glEnableVertexAttribArray(aTexturePosition); GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false, 8, mCameraVertexCoords.length * 4); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); // 给 uTexture 赋值 GLES20.glUniform1i(uTexture, 0); // 绘制到屏幕 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 解绑纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); } @Override public int getPreviewerTextureId() { return mFboHelper.getTexture2DId(); } @Override public void onDetach() { super.onDetach(); mFboHelper.onDetach(); // 释放着色器程序 if (mProgramId != 0) { GLES20.glDeleteProgram(mProgramId); } // 释放 VBO if (mVboId != 0) { int size = 1; int[] vboIds = new int[size]; vboIds[0] = mVboId; GLES20.glDeleteBuffers(1, vboIds, 0); } // 释放纹理 if (mWatermarkTextureId != 0) { int size = 1; int[] textures = new int[size]; textures[0] = mWatermarkTextureId; GLES20.glDeleteTextures(1, textures, 0); } } } ================================================ FILE: app/src/main/res/drawable/app_activity_main_launcher.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/app_activity_main.xml ================================================