[
  {
    "path": ".gitignore",
    "content": "# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea\n\n# Keystore files\n*.jks\n"
  },
  {
    "path": "README.md",
    "content": "## SAlbum\nSAlbum 是一款对 Android 端提供 **图片的选取、裁剪、拍摄和短视频录制等功能的图库框架**\n\n## 功能介绍\n- **图片的选取**\n  - 支持 JPEG/PNG/WEBP/GIF 的选取\n  - 图片加载引擎由用户自定义实现\n- **图片的浏览**\n  - 共享元素跳转动画\n- **图像的裁剪**\n- **相机的拍摄**\n  - ~~CameraX-alpha4 尺寸选取存在问题, 暂时移除~~\n  - 提供 1:1、4:3、16:9 的比例选择\n     - 支持 CenterCrop 全屏预览\n  - 通过自定义 Renderer, 可拓展水印滤镜等效果\n- **视频的录制**\n  - 视频\n    - 使用 MediaCodec 实现 H.264 的硬编\n    - 支持 1080p, 720p, 480p 的录制分辨率\n  - 音频\n    - PCM 数据获取使用 OpenSL ES, 支持 v7a\n    - 使用 MediaCodec 硬编为 AAC\n  - 使用 MediaMuxer 合并为 mp4 文件\n- **视频的播放**\n  - 考虑到依赖体积, 使用系统提供的 VideoView 实现\n- **已支持 Android 10**\n  - Android 10 不支持随意访问外部存储 Storage 中的文件, 可通过 URI 进行图片加载\n\n实现原理请查看 [wiki](https://github.com/SharryChoo/SAlbum/wiki)\n\n## 功能集成\n[![](https://jitpack.io/v/SharryChoo/SAlbum.svg)](https://jitpack.io/#SharryChoo/SAlbum)\n\n### Step 1\nAdd it in your **module build.gradle** at the end of repositories\n```\ndependencies {\n    ...\n    // SAlbum dependency\n    implementation 'com.github.SharryChoo:SAlbum:+'\n     \n    // Need Android dependencies\n    implementation \"androidx.constraintlayout:constraintlayout:+\"\n    implementation \"androidx.appcompat:appcompat:+\"\n    implementation \"androidx.recyclerview:recyclerview:+\"\n    implementation \"com.google.android.material:material:+\"\n}\n```\n\n### Step 2\nAdd it in your **root build.gradle** at the end of repositories\n```\nallprojects {\n    repositories {\n\t    ...\n\t    maven { url 'https://jitpack.io' }\n    }\n}\n```\n\n## 效果展示\n下载体验 [Demo](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/SAlbum-1.0.1.apk)\n\n### 资源选取\n![资源选取](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/PicturePicker.jpg)\n\n### 图像拍摄\n![图像拍摄](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/PictureTaker.png)\n\n### 视频录制\n![视频录制](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/VideoRecord.png)\n\n### 视频播放\n![视频的播放](https://raw.githubusercontent.com/SharryChoo/SAlbum/release/assert/VideoPlay.jpg)\n\n## 使用指南\nSPicturePicker 的所有功能提供, 均通过 **Manager** 对外提供, 其具体的功能选项通过 **Config** 来配置\n\n功能 | Manager | Config\n:---:|:---:|:---:\n选取 | PickerManager | PickerConfig\n浏览 | WatcherManager | WatcherConfig\n拍摄/录像 | TakerManager | TakerConfig\n裁剪 | CropperManager | CropperConfig\n\n### 一) 选取\n```\nPickerManager.with(context)\n        // 注入配置\n        .setPickerConfig(\n                PickerConfig.Builder()\n                        // Toolbar 背景设置\n                        .setToolbarBackgroundColor(\n                                ContextCompat.getColor(this, R.color.colorPrimary)\n                        )\n                        // 指示器填充色\n                        .setIndicatorSolidColor(\n                                ContextCompat.getColor(this, R.color.colorPrimary)\n                        )\n                        // 选中指示器的颜色\n                        .setIndicatorBorderColor(\n                                ContextCompat.getColor(this, R.color.colorPrimary),\n                                ContextCompat.getColor(this, android.R.color.white)\n                        )\n                        // 指示器边界的颜色\n                        .setPickerItemBackgroundColor(\n                                ContextCompat.getColor(this, android.R.color.white)\n                        )\n                        // 阈值\n                        .setThreshold(etAlbumThreshold.text.toString().toInt())\n                        // 每行展示的数量\n                        .setSpanCount(etSpanCount.text.toString().toInt())\n                        // 是否开启 Toolbar Behavior 动画\n                        .isToolbarScrollable(cbAnimation.isChecked)\n                        // 是否开启 Fab Behavior 动画\n                        .isFabScrollable(cbAnimation.isChecked)\n                        // 是否选择 GIF 图\n                        .isPickGif(cbGif.isChecked)\n                        // 是否选择视频\n                        .isPickVideo(cbVideo.isChecked)\n                        // 注入用户已选中的图片集合\n                        .setUserPickedSet(mPickedSet)\n                        // 设置相机配置, 非 null 说明支持相机(拍摄/录制)\n                        .setCameraConfig(\n                                if (cbCamera.isChecked) takerConfig else null\n                        )\n                        // 设置裁剪配置, 非 null 说明支持裁剪\n                        .setCropConfig(\n                                if (cbCrop.isChecked) cropperConfig else null\n                        )\n                        .build()\n        )\n        // 加载框架注入\n        .setLoaderEngine(\n                object : ILoaderEngine {\n                    override fun loadPicture(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n                        // Android 10 以后, 需要使用 URI 进行加载\n                        Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView)\n                    }\n\n                    override fun loadGif(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n                        // Android 10 以后, 需要使用 URI 进行加载\n                        Glide.with(context).asGif().load(mediaMeta.contentUri).into(imageView)\n                    }\n\n                    override fun loadVideoThumbnails(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n                        // Android 10 以后, 需要使用 URI 进行加载\n                        Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView)\n                    }\n                }\n        )\n        .start {\n            // TODO 选中的资源, 通过 ArrayList<MediaMeta> 返回\n        }\n```\n选取的方式如上所示, **首先按照需求构建 Config**, **然后注入图片加载的引擎**, 之后便可以在 start 的回调中获取到选中的图片资源了\n- 关于相机\n  - 在 PickerConfig 中传入相机的配置, 则意为开启相机的功能\n- 关于裁剪\n  - 在 PickerConfig 中传入裁剪的配置, 则意为开启裁剪的功能\n\n### 二) 浏览\n浏览的功能与选取类似, 打开图片选择器时, 会根据 PickerConfig 自动生成浏览的配置, 若想在外界单独使用图片浏览的功能, 可以通过以下方式\n```\nWatcherManager.with(context)\n        .setConfig(\n            WatcherConfig.Builder()\n                // 配置 Indicator 的展示效果\n                .setIndicatorTextColor(mPickerConfig.getIndicatorTextColor())\n                .setIndicatorSolidColor(mPickerConfig.getIndicatorSolidColor())\n                .setIndicatorBorderColor(\n                        mPickerConfig.getIndicatorBorderCheckedColor(),\n                        mPickerConfig.getIndicatorBorderUncheckedColor()\n                )\n                // 注入需要展示的图片\n                .setDisplayDataSet(mPickedSet, 0)\n                // 设置最大选中数量, 若 > 0, 则说明图片查看器也支持选取的功能\n                .setThreshold(mPickerConfig.getThreshold())\n                // 注入用户选中的图片集合\n                .setUserPickedSet(mPickedSet)\n                .build();\n        )\n        // 注入共享元素\n        .setSharedElement(sharedElement)\n        // 注入图片加载器\n        .setLoaderEngine(Loader.getPictureLoader())\n        .startForResult(this);\n```\n可以看到浏览的使用主要区别在于, 增加了\n**共享元素(支持 5.0 以下的操作系统)**\n的选项\n- 当 threshold > 0 时, 表示需要为图片浏览添加图片选择功能, 反之仅做图片查看使用\n\n\n### 三) 拍摄\n相机的使用与浏览类似, 可以集成在 Picker 中使用, 也可以单独使用\n```\nTakerManager.with(context)\n        .setConfig(\n            TakerConfig.Builder()\n                // 设置外部存储目录相对路径\n                .setRelativePath(RELATIVE_PATH)\n                // 指定 FileProvider 的 authority, 用于获取文件 URI\n                .setAuthority(FILE_PROVIDER)\n                // 预览画面比例, 支持 1:1, 4:3, 16:9\n                .setPreviewAspect(ASPECT_1_1)\n                // 是否全屏预览(在比例基础上进行 CenterCrop, 保证画面不畸形)\n                .setFullScreen(false)\n                // 设置自定义 Renderer 的路径\n                .setRenderer(WatermarkPreviewerRenderer::class.java)\n                // 设置是否支持视频录制\n                .setVideoRecord(true)\n                // 设置录制最大时长\n                .setMaxRecordDuration(15 * 1000)\n                // 设置录制最短时长\n                .setMinRecordDuration(1 * 1000)\n                // 设置录制的分辨率\n                .setRecordResolution(Options.Video.RESOLUTION_720P)\n                // 拍摄后质量压缩\n                .setPictureQuality(80)\n                // 注入裁剪配置, 非 null, 表示拍摄之后进行图片的裁剪\n                .setCropConfig(...)\n                .build()\n        )\n        .take(this);\n```\n其中的注释比较清晰, 操作完成之后, 可通过回调获取到拍摄/录制的结果\n\n#### 1. RelativePath \nAndorid 10 以后, 无法随意的在外部存储卡中创建文件, 因此使用了 RelativePath \n```\n// 绝对路径\n\"/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum\"\n// 相对路径\n\"SAlbum\"\n```\n只需要设置了相对路径, SAlbum 会自动在 Android 媒体文件夹下创建工程的文件夹, 所有拍摄录制的图片均会保存在其中, 这也是 Android 希望我们遵守的规范\n\n#### 2. Authority\n需要获取文件的 URI, 7.0 之后获取 URI 需要通过 FileProvider, 因此这里需要传入 FileProvider 的 authority, 关于这一块网上的资料比较多, 这里就不再赘述了\n\n#### 3. Camera 渲染器\n**关于自定义 Camera 的渲染器, 需要用户自定义实现 IPreviewer.Renderer 这个接口**, Demo 中提供了一个水印效果的渲染器滤镜, 可以其参考实现自己的渲染器\n```\npublic Builder setRenderer(@NonNull Class<? extends IPreviewer.Renderer> rendererClass) {\n    try {\n        rendererClass.getDeclaredConstructor(Context.class);\n    } catch (NoSuchMethodException e) {\n        throw new UnsupportedOperationException(\"Please ensure \" + rendererClass.getSimpleName()\n                + \" have a constructor like: \" + rendererClass.getSimpleName() + \"(Context context)\");\n    }\n    mConfig.rendererClsName = rendererClass.getName();\n    return this;\n}\n```\n传入渲染器实现的 class 文件, 需要保证提供一个参数为 Context 的构造方法, 否则在构建 TakerConfig 时会出现异常\n\n### 四) 裁剪\n```\nCropperManager.with(context)\n        .setConfig(\n            CropperConfig.Builder()\n                // 要裁剪的图片的 URI\n                .setOriginUri(...)\n                // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI\n                .setAuthority(FILE_PROVIDER)\n                // 设置外部存储目录相对路径\n                .setRelativePath(RELATIVE_PATH)\n                // 裁剪期望的尺寸\n                .setCropSize(1000, 1000)\n                // 裁剪后的质量\n                .setCropQuality(80)\n                .build()\n        )\n        .crop(this);\n```\n裁剪目前使用系统提供的裁剪方式, 其使用方式也比较简单, 这里就不再赘述了\n\n更多功能请查看工程中提供的[示例](https://github.com/SharryChoo/SAlbum/blob/release/app/src/main/java/com/sharry/app/salbum/MainActivity.kt)\n\n## 致谢\n[PhotoView](https://github.com/chrisbanes/PhotoView)\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea/workspace.xml\n\n# Keystore files\n*.jks"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\napply plugin: 'kotlin-kapt'\n\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n        vectorDrawables.useSupportLibrary true\n        externalNativeBuild {\n            ndk {\n                abiFilters \"armeabi-v7a\"\n            }\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion\"\n    // Google dependencies\n    def constraintlayoutVersion = \"1.1.3\"\n    implementation \"androidx.constraintlayout:constraintlayout:$constraintlayoutVersion\"\n    def supportLibraryVersion = '1.1.0'\n    implementation \"androidx.appcompat:appcompat:$supportLibraryVersion\"\n    def recycleViewVersion = '1.0.0'\n    implementation \"androidx.recyclerview:recyclerview:$recycleViewVersion\"\n    def materialVersion = '1.0.0'\n    implementation \"com.google.android.material:material:$materialVersion\"\n\n    // Glide dependencies\n    def glideVersion = '4.6.1'\n    implementation \"com.github.bumptech.glide:glide:$glideVersion\"\n    kapt \"com.github.bumptech.glide:compiler:$glideVersion\"\n\n    // SPicturePicker dependencies\n//    implementation 'com.github.SharryChoo:SAlbum:1.0.0'\n    api project(':lib-album')\n\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:\\Android\\sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/release/output.json",
    "content": "[{\"outputType\":{\"type\":\"APK\"},\"apkInfo\":{\"type\":\"MAIN\",\"splits\":[],\"versionCode\":-1,\"enabled\":true,\"outputFile\":\"app-release.apk\",\"fullName\":\"release\",\"baseName\":\"release\"},\"path\":\"app-release.apk\",\"properties\":{}}]"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.sharry.app.salbum\">\n\n    <!-- sd卡写权限 -->\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"GoogleAppIndexingWarning\">\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"com.sharry.app.salbum.FileProvider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/provider_paths\" />\n        </provider>\n\n        <activity android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/sharry/app/salbum/MainActivity.kt",
    "content": "package com.sharry.app.salbum\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.widget.ImageView\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport com.bumptech.glide.Glide\nimport com.sharry.lib.album.*\nimport com.sharry.lib.album.TakerConfig.ASPECT_4_3\nimport com.sharry.lib.album.toolbar.SToolbar\nimport com.sharry.lib.media.recorder.Options\nimport kotlinx.android.synthetic.main.app_activity_main.*\n\n\n/**\n * SAlbum 示例 Activity.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 12/6/2018 10:49 AM\n */\nprivate const val FILE_PROVIDER = \"com.sharry.app.salbum.FileProvider\"\nprivate const val RELATIVE_PATH = \"SAlbum\"\n\nclass MainActivity : AppCompatActivity() {\n\n    /**\n     * 用与图片选取的配置\n     */\n    private lateinit var pickerConfig: PickerConfig\n\n    /**\n     * 用与相机拍摄的配置\n     */\n    private val takerConfig = TakerConfig.Builder()\n            // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI\n            .setAuthority(FILE_PROVIDER)\n            // 设置外部存储目录相对路径\n            .setRelativePath(RELATIVE_PATH)\n            // 预览画面比例\n            .setPreviewAspect(ASPECT_4_3)\n            // 是否全屏预览(在比例基础上进行 CenterCrop, 保证画面不畸形)\n            .setFullScreen(true)\n            // 设置自定义 Renderer 的实现类\n            .setRenderer(WatermarkPreviewerRenderer::class.java)\n            // 设置是否支持视频录制\n            .setVideoRecord(true)\n            // 是否仅支持视频录制\n            .setJustVideoRecord(false)\n            // 设置录制最大时长\n            .setMaxRecordDuration(15 * 1000)\n            // 设置录制最短时长\n            .setMinRecordDuration(1 * 1000)\n            // 设置录制的分辨率\n            .setRecordResolution(Options.Video.RESOLUTION_1080P)\n            // 拍摄后质量压缩\n            .setPictureQuality(80)\n            .build()\n\n    /**\n     * 用与裁剪的配置\n     */\n    private val cropperConfig = CropperConfig.Builder()\n            // 指定 FileProvider 的 authority, 用于 7.0 获取文件 URI\n            .setAuthority(FILE_PROVIDER)\n            // 设置外部存储目录相对路径\n            .setRelativePath(RELATIVE_PATH)\n            // 裁剪期望的尺寸\n            .setCropSize(1000, 1000)\n            // 裁剪后的质量\n            .setCropQuality(80)\n            .build()\n\n    /**\n     * 图片加载器\n     *\n     * 注: Android 10 以后需要使用 URI 进行加载操作\n     */\n    private val pictureLoader = object : ILoaderEngine {\n        override fun loadPicture(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n            // Android 10 以后, 需要使用 URI 进行加载\n            Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView)\n        }\n\n        override fun loadGif(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n            // Android 10 以后, 需要使用 URI 进行加载\n            Glide.with(context).asGif().load(mediaMeta.contentUri).into(imageView)\n        }\n\n        override fun loadVideoThumbnails(context: Context, mediaMeta: MediaMeta, imageView: ImageView) {\n            // Android 10 以后, 需要使用 URI 进行加载\n            Glide.with(context).asBitmap().load(mediaMeta.contentUri).into(imageView)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.app_activity_main)\n        initTitle()\n        initViews()\n        initData()\n    }\n\n    private fun initTitle() {\n        SToolbar.Builder(this)\n                .setBackgroundColorRes(R.color.colorPrimary)\n                .setTitleText(getString(R.string.app_name))\n                .apply()\n    }\n\n    private fun initViews() {\n        btnLaunchAlbum.setOnClickListener { _ ->\n            if (TextUtils.isEmpty(etAlbumThreshold.text) || TextUtils.isEmpty(etSpanCount.text)) {\n                return@setOnClickListener\n            }\n            openAlbum()\n        }\n    }\n\n    private fun initData() {\n        pickerConfig = PickerConfig.Builder()\n                // Toolbar 背景设置\n                .setToolbarBackgroundColor(ContextCompat.getColor(this, R.color.colorPrimary))\n                // 指示器填充色\n                .setIndicatorSolidColor(ContextCompat.getColor(this, R.color.colorPrimary))\n                // 指示器边界的颜色\n                .setPickerItemBackgroundColor(ContextCompat.getColor(this, android.R.color.white))\n                // 选中指示器的颜色\n                .setIndicatorBorderColor(\n                        ContextCompat.getColor(this, R.color.colorPrimary),\n                        ContextCompat.getColor(this, android.R.color.white)\n                )\n                .build()\n    }\n\n    private fun openAlbum() {\n        // 根据选择中数据重新构建 pickerConfig.\n        pickerConfig.rebuild()\n                // 阈值\n                .setThreshold(etAlbumThreshold.text.toString().toInt())\n                // 每行展示的数量\n                .setSpanCount(etSpanCount.text.toString().toInt())\n                // 是否开启 Toolbar Behavior 动画\n                .isToolbarScrollable(cbAnimation.isChecked)\n                // 是否开启 Fab Behavior 动画\n                .isFabScrollable(cbAnimation.isChecked)\n                // 是否选择图片\n                .isPickPicture(cbPicture.isChecked)\n                // 是否选择 GIF 图\n                .isPickGif(cbGif.isChecked)\n                // 是否选择视频\n                .isPickVideo(cbVideo.isChecked)\n                // 设置相机配置, 非 null 说明支持相机(拍摄/录制)\n                .setCameraConfig(if (cbCamera.isChecked) takerConfig else null)\n                // 设置裁剪配置, 非 null 说明支持裁剪\n                .setCropConfig(if (cbCrop.isChecked) cropperConfig else null)\n                .build()\n        PickerManager.with(this)\n                // 设置选择配置文件\n                .setPickerConfig(pickerConfig)\n                // 图片加载框架注入\n                .setLoaderEngine(pictureLoader)\n                // 开始选取\n                .start {\n                    it?.forEach {\n                        Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show()\n                    }\n                }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/sharry/app/salbum/WatermarkPreviewerRenderer.java",
    "content": "package com.sharry.app.salbum;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.opengl.GLES20;\nimport android.opengl.GLUtils;\n\nimport com.sharry.lib.camera.PreviewerRendererImpl;\nimport com.sharry.lib.camera.PreviewerRendererWrapper;\nimport com.sharry.lib.opengles.util.FboHelper;\nimport com.sharry.lib.opengles.util.GlUtil;\n\nimport java.nio.FloatBuffer;\n\n/**\n * 带水印效果的渲染器\n * <p>\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-01 16:04\n */\npublic class WatermarkPreviewerRenderer extends PreviewerRendererWrapper {\n\n    private static final String VERTEX_SHADER_STR = \"attribute vec4 aVertexPosition;\\n\" +\n            \"    attribute vec2 aTexturePosition;\\n\" +\n            \"    varying vec2 vPosition;\\n\" +\n            \"    void main() {\\n\" +\n            \"        vPosition = aTexturePosition;\\n\" +\n            \"        gl_Position = aVertexPosition;\\n\" +\n            \"    }\";\n\n\n    private static final String FRAGMENT_SHADER_STR = \"precision mediump float;\\n\" +\n            \"varying vec2 vPosition;\\n\" +\n            \"uniform sampler2D uTexture;\\n\" +\n            \"void main() {\\n\" +\n            \"    gl_FragColor=texture2D(uTexture, vPosition);\\n\" +\n            \"}\";\n\n    /**\n     * 相机顶点坐标\n     */\n    private final float[] mCameraVertexCoords = new float[]{\n            -1f, 1f,  // 左上\n            -1f, -1f, // 左下\n            1f, 1f,   // 右上\n            1f, -1f,   // 右下\n    };\n\n    /**\n     * 相机纹理映射坐标\n     */\n    private final float[] mCameraTextureCoords = new float[]{\n            0f, 1f,   // 左上\n            0f, 0f,   // 左下\n            1f, 1f,   // 右上\n            1f, 0f    // 右下\n    };\n\n    /**\n     * 水印顶点坐标\n     */\n    private final float[] mWatermarkVertexCoords = new float[]{\n            0f, 0f,  // 左上\n            0f, 0f,  // 左下\n            0f, 0f,  // 右上\n            0f, 0f,  // 右下\n    };\n\n    /**\n     * 水印纹理坐标, 水印从 Bitmap 中加载, 坐标系相反\n     */\n    private final float[] mWatermarkTextureCoords = new float[]{\n            0f, 0f,   // 左下\n            0f, 1f,   // 左上\n            1f, 0f,   // 右下\n            1f, 1f    // 右上\n    };\n\n    /**\n     * 相机纹理顶点和纹理坐标\n     */\n    private final FloatBuffer mCameraTextureVertexBuffer = GlUtil.createFloatBuffer(mCameraVertexCoords);\n    private final FloatBuffer mCameraTextureBuffer = GlUtil.createFloatBuffer(mCameraTextureCoords);\n\n    /**\n     * 水印纹理顶点和纹理坐标\n     */\n    private final FloatBuffer mWatermarkVertexBuffer = GlUtil.createFloatBuffer(mWatermarkVertexCoords);\n    private final FloatBuffer mWatermarkTextureBuffer = GlUtil.createFloatBuffer(mWatermarkTextureCoords);\n\n    private final Context mContext;\n    private final FboHelper mFboHelper;\n    private int mProgramId;\n    private int aVertexPosition;\n    private int aTexturePosition;\n    private int mVboId;\n    private int uTexture;\n    private int mWatermarkTextureId = 0;\n    private Bitmap mWatermarkBitmap;\n\n    public WatermarkPreviewerRenderer(Context context) {\n        super(new PreviewerRendererImpl(context));\n        this.mContext = context;\n        this.mFboHelper = new FboHelper();\n    }\n\n    @Override\n    public void onAttach() {\n        super.onAttach();\n        mFboHelper.onAttach();\n        // 初始化程序\n        setupShaders();\n        // 初始化顶点坐标\n        setupCoordinates();\n        // 初始化水印纹理\n        setupWatermarkTexture();\n    }\n\n    private void setupShaders() {\n        mProgramId = GlUtil.createProgram(VERTEX_SHADER_STR, FRAGMENT_SHADER_STR);\n        aVertexPosition = GLES20.glGetAttribLocation(mProgramId, \"aVertexPosition\");\n        aTexturePosition = GLES20.glGetAttribLocation(mProgramId, \"aTexturePosition\");\n        uTexture = GLES20.glGetUniformLocation(mProgramId, \"uTexture\");\n    }\n\n    private void setupCoordinates() {\n        // 创建 vbo\n        int vboSize = 1;\n        int[] vboIds = new int[vboSize];\n        GLES20.glGenBuffers(vboSize, vboIds, 0);\n        // 将顶点坐标写入 vbo\n        mVboId = vboIds[0];\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 开辟 VBO 空间\n        GLES20.glBufferData(\n                GLES20.GL_ARRAY_BUFFER,\n                mCameraVertexCoords.length * 4 +\n                        mCameraTextureCoords.length * 4\n                        + mWatermarkVertexCoords.length * 4\n                        + mWatermarkTextureCoords.length * 4,\n                null,\n                GLES20.GL_STATIC_DRAW\n        );\n        // 写入相机顶点坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                0,\n                mCameraVertexCoords.length * 4,\n                mCameraTextureVertexBuffer\n        );\n        // 写入相机纹理坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                mCameraVertexCoords.length * 4,\n                mCameraTextureCoords.length * 4,\n                mCameraTextureBuffer\n        );\n        // 写入水印顶点坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                mCameraVertexCoords.length * 4 +\n                        mCameraTextureCoords.length * 4,\n                mWatermarkVertexCoords.length * 4,\n                mWatermarkVertexBuffer\n        );\n        // 写入水印纹理坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                mCameraVertexCoords.length * 4 +\n                        mCameraTextureCoords.length * 4 +\n                        mWatermarkVertexCoords.length * 4,\n                mWatermarkTextureCoords.length * 4,\n                mWatermarkTextureBuffer\n        );\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n    }\n\n    private void setupWatermarkTexture() {\n        int[] textureIds = new int[1];\n        GLES20.glGenTextures(1, textureIds, 0);\n        mWatermarkTextureId = textureIds[0];\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mWatermarkTextureId);\n        // 设置纹理环绕方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);\n        // 设置纹理过滤方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n        // 创建 Bitmap, 将其写入纹理\n        mWatermarkBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_demo_watermark);\n        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mWatermarkBitmap, 0);\n        // 解绑\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    @Override\n    public void onSizeChanged(int width, int height) {\n        super.onSizeChanged(width, height);\n        mFboHelper.onSizeChanged(width, height);\n        // 启用透明\n        GLES20.glEnable(GLES20.GL_BLEND);\n        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);\n        GLES20.glViewport(0, 0, width, height);\n        // 更新水印坐标\n        updateWatermarkCoors(width, height);\n    }\n\n    private void updateWatermarkCoors(int surfaceWidth, int surfaceHeight) {\n        float height = mWatermarkBitmap.getHeight();\n        float width = mWatermarkBitmap.getWidth();\n        height = height * (1 / (float) surfaceHeight);\n        width = width * (1 / (float) surfaceWidth);\n        float left = -0.9f;\n        float bottom = -0.9f;\n        // 设置水印的位置\n        // 左上\n        mWatermarkVertexCoords[0] = left;\n        mWatermarkVertexCoords[1] = bottom + height;\n        // 左下\n        mWatermarkVertexCoords[2] = left;\n        mWatermarkVertexCoords[3] = bottom;\n        // 右上\n        mWatermarkVertexCoords[4] = left + width;\n        mWatermarkVertexCoords[5] = bottom + height;\n        // 右下\n        mWatermarkVertexCoords[6] = left + width;\n        mWatermarkVertexCoords[7] = bottom;\n        // 更新 Buffer\n        mWatermarkVertexBuffer.put(mWatermarkVertexCoords, 0, mWatermarkVertexCoords.length)\n                .position(0);\n        // 更新 VBO\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 写入水印顶点坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                mCameraVertexCoords.length * 4 +\n                        mCameraTextureCoords.length * 4,\n                mWatermarkVertexCoords.length * 4,\n                mWatermarkVertexBuffer\n        );\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n    }\n\n    @Override\n    protected void onDrawTexture(int textureId) {\n        mFboHelper.bindFramebuffer();\n        // 绘制纹理\n        drawOriginTexture(textureId);\n        // 绘制水印\n        drawWatermark();\n        // 解绑\n        mFboHelper.unbindFramebuffer();\n        // 绘制到系统自带的缓冲上\n        drawToEGLSurface();\n    }\n\n    private void drawOriginTexture(int textureId) {\n        GLES20.glUseProgram(mProgramId);\n        // 绑定相机的纹理\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);\n        // 写入顶点坐标\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        GLES20.glEnableVertexAttribArray(aVertexPosition);\n        GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false, 8, 0);\n        // 写入纹理坐标\n        GLES20.glEnableVertexAttribArray(aTexturePosition);\n        GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false,\n                8, mCameraVertexCoords.length * 4);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        // 给 uTexture 赋值\n        GLES20.glUniform1i(uTexture, 0);\n        // 绘制到屏幕\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n        // 解绑纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    private void drawWatermark() {\n        GLES20.glUseProgram(mProgramId);\n        // 绑定纹理\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mWatermarkTextureId);\n        // 写入水印顶点坐标\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        GLES20.glEnableVertexAttribArray(aVertexPosition);\n        GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false,\n                8, (mCameraVertexCoords.length + mCameraTextureCoords.length) * 4);\n        // 写入水印纹理坐标\n        GLES20.glEnableVertexAttribArray(aTexturePosition);\n        GLES20.glVertexAttribPointer(\n                aTexturePosition,\n                2,\n                GLES20.GL_FLOAT,\n                false,\n                8,\n                (mCameraVertexCoords.length + mCameraTextureCoords.length + mWatermarkVertexCoords.length) * 4\n        );\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        // 给 uTexture 赋值\n        GLES20.glUniform1i(uTexture, 0);\n        // 绘制到屏幕\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n        // 解绑纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    private void drawToEGLSurface() {\n        GLES20.glUseProgram(mProgramId);\n        // 绑定纹理\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getPreviewerTextureId());\n        // 写入顶点坐标\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        GLES20.glEnableVertexAttribArray(aVertexPosition);\n        GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false,\n                8, 0);\n        // 写入纹理坐标\n        GLES20.glEnableVertexAttribArray(aTexturePosition);\n        GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false,\n                8, mCameraVertexCoords.length * 4);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        // 给 uTexture 赋值\n        GLES20.glUniform1i(uTexture, 0);\n        // 绘制到屏幕\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n        // 解绑纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    @Override\n    public int getPreviewerTextureId() {\n        return mFboHelper.getTexture2DId();\n    }\n\n    @Override\n    public void onDetach() {\n        super.onDetach();\n        mFboHelper.onDetach();\n        // 释放着色器程序\n        if (mProgramId != 0) {\n            GLES20.glDeleteProgram(mProgramId);\n        }\n        // 释放 VBO\n        if (mVboId != 0) {\n            int size = 1;\n            int[] vboIds = new int[size];\n            vboIds[0] = mVboId;\n            GLES20.glDeleteBuffers(1, vboIds, 0);\n        }\n        // 释放纹理\n        if (mWatermarkTextureId != 0) {\n            int size = 1;\n            int[] textures = new int[size];\n            textures[0] = mWatermarkTextureId;\n            GLES20.glDeleteTextures(1, textures, 0);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/app_activity_main_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <corners android:radius=\"6dp\"/>\n\n    <solid android:color=\"@color/colorAccent\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"108dp\"\n        android:height=\"108dp\"\n        android:viewportWidth=\"890.43475\"\n        android:viewportHeight=\"890.43475\">\n    <group android:translateX=\"-66.78261\"\n            android:translateY=\"-66.78261\">\n      <path\n          android:pathData=\"M521.15,511.94m-416,0a416,416 0,1 0,832 0,416 416,0 1,0 -832,0Z\"\n          android:fillColor=\"#AEECFF\"/>\n      <path\n          android:pathData=\"M733.38,205.06l-44.61,-44.22L111.94,434.56s-21.44,80.19 2.75,177.41c166.98,-107.97 503.55,-330.69 618.69,-406.91zM653.57,308.42L373.31,557.31l93.38,57.41 232,-273.41zM731.9,351.87l-116.35,282.88h104l56,-266.82zM818.69,331.14v324.86l100.67,-23.62 -60.99,-301.7z\"\n          android:fillColor=\"#E3FAFF\"/>\n      <path\n          android:pathData=\"M774.21,241.22m-145.22,0a145.22,145.22 0,1 0,290.43 0,145.22 145.22,0 1,0 -290.43,0Z\"\n          android:fillColor=\"#FFFF5F\"/>\n      <path\n          android:pathData=\"M521.09,928c108.16,0 206.4,-41.6 280.32,-109.31a110.08,110.08 0,0 0,-27.65 -47.55L447.81,445.12a112.51,112.51 0,0 0,-158.66 0L118.66,615.55c46.14,179.58 208.58,312.45 402.43,312.45z\"\n          android:fillColor=\"#66BF47\"/>\n      <path\n          android:pathData=\"M316.74,874.05a413.5,413.5 0,0 0,204.35 53.95,415.81 415.81,0 0,0 400.26,-303.74l-101.63,-101.57a112.51,112.51 0,0 0,-158.66 0l-325.95,326.02c-7.62,7.62 -13.44,16.32 -18.37,25.34z\"\n          android:fillColor=\"#3FB018\"/>\n      <path\n          android:pathData=\"M521.09,928a413.76,413.76 0,0 0,186.43 -44.48l-221.25,-221.18a128.32,128.32 0,0 0,-180.99 0l-109.06,109.06A414.85,414.85 0,0 0,521.09 928z\"\n          android:fillColor=\"#5AA93E\"/>\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/app_activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".MainActivity\">\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:padding=\"10dp\">\n\n        <EditText\n            android:id=\"@+id/etAlbumThreshold\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@null\"\n            android:hint=\"@string/app_activity_main_et_threshold_hint\"\n            android:inputType=\"number\"\n            android:padding=\"10dp\"\n            android:text=\"6\"\n            android:textColor=\"@color/colorAccent\"\n            android:textSize=\"15dp\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:padding=\"10dp\">\n\n        <EditText\n            android:id=\"@+id/etSpanCount\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@null\"\n            android:hint=\"@string/app_activity_main_et_span_count_hint\"\n            android:inputType=\"number\"\n            android:padding=\"10dp\"\n            android:text=\"3\"\n            android:textColor=\"@color/colorAccent\"\n            android:textSize=\"15dp\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\">\n\n        <CheckBox\n            android:id=\"@+id/cbPicture\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/app_activity_main_cb_picture\" />\n\n        <CheckBox\n            android:id=\"@+id/cbGif\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"20dp\"\n            android:text=\"@string/app_activity_main_cb_gif\" />\n\n        <CheckBox\n            android:id=\"@+id/cbVideo\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"20dp\"\n            android:text=\"@string/app_activity_main_cb_video\" />\n\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\">\n\n        <CheckBox\n            android:id=\"@+id/cbCamera\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/app_activity_main_cb_camera\" />\n\n        <CheckBox\n            android:id=\"@+id/cbCrop\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"20dp\"\n            android:text=\"@string/app_activity_main_cb_crop\" />\n\n        <CheckBox\n            android:id=\"@+id/cbAnimation\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"20dp\"\n            android:text=\"@string/app_activity_main_cb_anim\" />\n\n    </LinearLayout>\n\n    <Button\n        android:id=\"@+id/btnLaunchAlbum\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"30dp\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginRight=\"30dp\"\n        android:background=\"@drawable/app_activity_main_launcher\"\n        android:gravity=\"center\"\n        android:text=\"@string/activity_btn_launch\"\n        android:textAllCaps=\"false\"\n        android:textColor=\"#fff\"\n        android:textSize=\"20dp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimaryDark\">#00b0ff</color>\n    <color name=\"colorPrimary\">#00b0ff</color>\n    <color name=\"colorAccent\">#64b6f6</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#3576BB</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\" translatable=\"false\">SAlbum</string>\n\n    <string name=\"app_activity_main_et_threshold_hint\">Threshold</string>\n    <string name=\"app_activity_main_et_span_count_hint\">Span Count</string>\n\n    <string name=\"app_activity_main_cb_picture\">Picture</string>\n    <string name=\"app_activity_main_cb_gif\">Gif</string>\n    <string name=\"app_activity_main_cb_video\">Video</string>\n    <string name=\"app_activity_main_cb_camera\">Camera</string>\n    <string name=\"app_activity_main_cb_crop\">Crop</string>\n    <string name=\"app_activity_main_cb_anim\">Animation</string>\n    <string name=\"activity_btn_launch\">Launcher</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh/strings.xml",
    "content": "<resources>\n\n    <string name=\"app_activity_main_et_threshold_hint\">选中数量</string>\n    <string name=\"app_activity_main_et_span_count_hint\">每行数量</string>\n\n    <string name=\"app_activity_main_cb_picture\">图片</string>\n    <string name=\"app_activity_main_cb_gif\">Gif</string>\n    <string name=\"app_activity_main_cb_video\">视频</string>\n    <string name=\"app_activity_main_cb_camera\">相机</string>\n    <string name=\"app_activity_main_cb_crop\">裁剪</string>\n    <string name=\"app_activity_main_cb_anim\">动画</string>\n    <string name=\"activity_btn_launch\">启动</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/provider_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"external_files\"\n        path=\".\" />\n    <cache-path\n        name=\"cache_files\"\n        path=\".\" />\n    <files-path\n        name=\"files_files\"\n        path=\".\" />\n</paths>"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n    // Define versions in a single place\n    ext {\n        // Sdk and tools\n        compileSdkVersion = 29\n        minSdkVersion = 19\n        targetSdkVersion = 29\n        buildToolsVersion = '29.0.0'\n\n        // Root project dependencies\n        gradleVersion = '3.2.0'\n        kotlinVersion = '1.2.51'\n        mavenVersion = '2.1'\n\n        // common dependencies\n        supportLibraryVersion = '1.1.0'\n    }\n    dependencies {\n        classpath \"com.android.tools.build:gradle:$gradleVersion\"\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion\"\n        classpath \"com.github.dcendents:android-maven-gradle-plugin:$mavenVersion\"\n    }\n}\n\ntasks.withType(JavaCompile) {\n    options.encodin g = 'UTF-8'\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "git",
    "content": ""
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sun Apr 28 15:08:24 CST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-4.10-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\nandroid.enableJetifier=true\nandroid.useAndroidX=true\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "lib-album/.gitignore",
    "content": "/build\n# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea/workspace.xml\n\n# Keystore files\n*.jks"
  },
  {
    "path": "lib-album/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\n\ngroup = 'com.github.SharryChoo'\n\ndef resDirs = [\n        \"watcher/com/sharry/lib/album\",\n        \"picker/com/sharry/lib/album\",\n        \"taker/com/sharry/lib/album\",\n        \"player/com/sharry/lib/album\",\n        \"widget/com/sharry/lib/album/toolbar\"\n]\n\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n        vectorDrawables.useSupportLibrary true\n    }\n    sourceSets {\n        main {\n            java.srcDirs += 'src/main/base'\n            java.srcDirs += 'src/main/picker'\n            java.srcDirs += 'src/main/watcher'\n            java.srcDirs += 'src/main/player'\n            java.srcDirs += 'src/main/copper'\n            java.srcDirs += 'src/main/taker'\n            java.srcDirs += 'src/main/widget'\n            java.srcDirs += 'src/main/utils'\n            resDirs.forEach {\n                res.srcDirs += 'src/main/' + it + '/res'\n            }\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    // Android dependencies\n    def constraintlayoutVersion = \"1.1.3\"\n    implementation \"androidx.constraintlayout:constraintlayout:$constraintlayoutVersion\"\n    def supportLibraryVersion = '1.1.0'\n    implementation \"androidx.appcompat:appcompat:$supportLibraryVersion\"\n    def recycleViewVersion = '1.0.0'\n    implementation \"androidx.recyclerview:recyclerview:$recycleViewVersion\"\n    def materialVersion = '1.0.0'\n    implementation \"com.google.android.material:material:$materialVersion\"\n    // Core dependencies.\n    api project(':lib-media-recorder')\n}"
  },
  {
    "path": "lib-album/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "lib-album/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.sharry.lib.album\">\n\n    <!-- SD 卡读写权限 -->\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:supportsRtl=\"true\">\n        <activity\n            android:name=\"com.sharry.lib.album.PickerActivity\"\n            android:launchMode=\"singleTop\"\n            android:screenOrientation=\"portrait\"\n            android:theme=\"@style/PickerTheme\" />\n        <activity\n            android:name=\"com.sharry.lib.album.WatcherActivity\"\n            android:launchMode=\"singleTop\"\n            android:screenOrientation=\"portrait\"\n            android:theme=\"@style/WatcherTheme\" />\n        <activity\n            android:name=\"com.sharry.lib.album.TakerActivity\"\n            android:launchMode=\"singleTop\"\n            android:screenOrientation=\"portrait\" />\n        <activity\n            android:name=\"com.sharry.lib.album.VideoPlayerActivity\"\n            android:launchMode=\"singleTop\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "lib-album/src/main/base/com/sharry/lib/album/ILoaderEngine.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\n\n/**\n * 图片加载的接口, 由外界实现图片加载的策略\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/18 16:18\n */\npublic interface ILoaderEngine {\n\n    /**\n     * 加载图片的实现\n     */\n    void loadPicture(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView);\n\n    /**\n     * 加载 Gif 图\n     */\n    void loadGif(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView);\n\n    /**\n     * 加载视频缩略图\n     */\n    void loadVideoThumbnails(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/base/com/sharry/lib/album/Loader.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.util.Log;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * PicturePicker 加载图片的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/6/21 16:19\n */\nfinal class Loader {\n\n    private static final String TAG = Loader.class.getSimpleName();\n    private static ILoaderEngine sEngine;\n\n    static void setLoaderEngine(@Nullable ILoaderEngine engine) {\n        if (engine != null) {\n            sEngine = engine;\n        }\n    }\n\n    static ILoaderEngine getPictureLoader() {\n        return sEngine;\n    }\n\n    static void loadPicture(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView) {\n        if (sEngine == null) {\n            Log.e(TAG, \"Loader.loadPicture -> please invoke Loader.setLoaderEngine first\");\n            return;\n        }\n        sEngine.loadPicture(context, mediaMeta, imageView);\n    }\n\n    static void loadGif(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView) {\n        if (sEngine == null) {\n            Log.e(TAG, \"Loader.loadPicture -> please invoke Loader.setLoaderEngine first\");\n            return;\n        }\n        sEngine.loadGif(context, mediaMeta, imageView);\n    }\n\n    static void loadVideo(@NonNull Context context, @NonNull MediaMeta mediaMeta, @NonNull ImageView imageView) {\n        if (sEngine == null) {\n            Log.e(TAG, \"Loader.loadPicture -> please invoke Loader.setLoaderEngine first\");\n            return;\n        }\n        sEngine.loadVideoThumbnails(context, mediaMeta, imageView);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/base/com/sharry/lib/album/MediaMeta.java",
    "content": "package com.sharry.lib.album;\n\nimport android.net.Uri;\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * 媒体资源描述\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-02 14:01\n */\npublic class MediaMeta implements Parcelable {\n\n    /**\n     * 创建图片资源\n     */\n    public static MediaMeta createPicture(@NonNull Uri uri) {\n        return create(uri, \"\", true);\n    }\n\n    /**\n     * 创建视频资源\n     */\n    public static MediaMeta createVideo(@NonNull Uri uri) {\n        return create(uri, \"\", false);\n    }\n\n    static MediaMeta create(@NonNull Uri uri, String filePath, boolean isPicture) {\n        return new MediaMeta(uri, filePath, isPicture);\n    }\n\n    public static final Creator<MediaMeta> CREATOR = new Creator<MediaMeta>() {\n        @Override\n        public MediaMeta createFromParcel(Parcel in) {\n            return new MediaMeta(in);\n        }\n\n        @Override\n        public MediaMeta[] newArray(int size) {\n            return new MediaMeta[size];\n        }\n    };\n\n    protected MediaMeta(Parcel in) {\n        contentUri = in.readParcelable(Uri.class.getClassLoader());\n        path = in.readString();\n        isPicture = in.readByte() != 0;\n        size = in.readLong();\n        date = in.readLong();\n        duration = in.readLong();\n        thumbnailPath = in.readString();\n        mimeType = in.readString();\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeParcelable(contentUri, flags);\n        dest.writeString(path);\n        dest.writeByte((byte) (isPicture ? 1 : 0));\n        dest.writeLong(size);\n        dest.writeLong(date);\n        dest.writeLong(duration);\n        dest.writeString(thumbnailPath);\n        dest.writeString(mimeType);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    /**\n     * 文件的 URI\n     * <p>\n     * Android 10 以上, 只能够使用 URI 进行文件读写\n     */\n    @NonNull\n    Uri contentUri;\n\n    /**\n     * 文件路径\n     */\n    @Deprecated\n    String path;\n\n    /**\n     * 判断是否是图片\n     */\n    final boolean isPicture;\n\n    /**\n     * 文件大小\n     */\n    long size = 0;\n\n    /**\n     * 文件创建时间\n     */\n    long date = 0;\n\n    /**\n     * 时长(视频)\n     * <p>\n     * Unit ms\n     */\n    long duration = 0;\n\n    /**\n     * 视频缩略图\n     */\n    @Nullable\n    String thumbnailPath;\n\n    /**\n     * 媒体文件类型\n     */\n    String mimeType;\n\n    private MediaMeta(@NonNull Uri uri, @NonNull String filePath, boolean isPicture) {\n        this.contentUri = uri;\n        this.path = filePath;\n        this.isPicture = isPicture;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        MediaMeta mediaMeta = (MediaMeta) o;\n        return contentUri.equals(mediaMeta.contentUri);\n    }\n\n    @Override\n    public int hashCode() {\n        return contentUri.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return \"MediaMeta{\" +\n                \"contentUri='\" + contentUri + '\\'' + \", \\n\" +\n                \"path='\" + path + '\\'' + \", \\n\" +\n                \"isPicture=\" + isPicture + \", \\n\" +\n                \"size=\" + size + \", \\n\" +\n                \"date=\" + date + \", \\n\" +\n                \"duration=\" + duration + \", \\n\" +\n                \"thumbnailPath='\" + thumbnailPath + '\\'' + \", \\n\" +\n                \"mimeType='\" + mimeType + '\\'' + \"\\n\" +\n                '}';\n    }\n\n    @NonNull\n    public Uri getContentUri() {\n        return contentUri;\n    }\n\n    @NonNull\n    @Deprecated\n    public String getPath() {\n        return path;\n    }\n\n    public boolean isPicture() {\n        return isPicture;\n    }\n\n    public long getSize() {\n        return size;\n    }\n\n    public long getDate() {\n        return date;\n    }\n\n    public long getDuration() {\n        return duration;\n    }\n\n    @Nullable\n    public String getThumbnailPath() {\n        return thumbnailPath;\n    }\n\n    public String getMimeType() {\n        return mimeType;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/copper/com/sharry/lib/album/CropperCallback.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\n\n/**\n * 图片裁剪的回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:07 PM\n */\npublic interface CropperCallback {\n\n    /**\n     * 裁剪完成的回调\n     */\n    void onCropComplete(@NonNull MediaMeta meta);\n\n    void onCropFailed();\n\n}\n"
  },
  {
    "path": "lib-album/src/main/copper/com/sharry/lib/album/CropperCallbackLambda.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.Nullable;\n\n/**\n * 图片裁剪的回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:07 PM\n */\npublic interface CropperCallbackLambda {\n\n    void onCropped(@Nullable MediaMeta meta);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/copper/com/sharry/lib/album/CropperConfig.java",
    "content": "package com.sharry.lib.album;\n\nimport android.net.Uri;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * 图片裁剪的相关参数\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.1\n * @since 2018/11/29 16:57\n */\npublic class CropperConfig implements Parcelable {\n\n    protected CropperConfig(Parcel in) {\n        originUri = in.readParcelable(Uri.class.getClassLoader());\n        relativePath = in.readString();\n        isCropCircle = in.readByte() != 0;\n        authority = in.readString();\n        aspectX = in.readInt();\n        aspectY = in.readInt();\n        outputX = in.readInt();\n        outputY = in.readInt();\n        destQuality = in.readInt();\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeParcelable(originUri, flags);\n        dest.writeString(relativePath);\n        dest.writeByte((byte) (isCropCircle ? 1 : 0));\n        dest.writeString(authority);\n        dest.writeInt(aspectX);\n        dest.writeInt(aspectY);\n        dest.writeInt(outputX);\n        dest.writeInt(outputY);\n        dest.writeInt(destQuality);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public static final Creator<CropperConfig> CREATOR = new Creator<CropperConfig>() {\n        @Override\n        public CropperConfig createFromParcel(Parcel in) {\n            return new CropperConfig(in);\n        }\n\n        @Override\n        public CropperConfig[] newArray(int size) {\n            return new CropperConfig[size];\n        }\n    };\n\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    private Uri originUri;              // 需要裁剪的图片路径\n    private String relativePath;        // 可用的输出目录\n    private boolean isCropCircle;       // 是否为圆形裁剪\n    private String authority;           // fileProvider 的 authority 属性, 用于 7.0 之后, 查找文件的 URI\n    private int aspectX = 1;            // 方形 X 的比率\n    private int aspectY = 1;            // 方形 X 的比率\n    private int outputX = 500;          // 图像输出时的宽\n    private int outputY = 500;          // 图像输出的高\n    private int destQuality = 80;       // 裁剪后图片输出的质量\n\n    private CropperConfig() {\n    }\n\n    public Uri getOriginUri() {\n        return originUri;\n    }\n\n    public String getRelativePath() {\n        return relativePath;\n    }\n\n    public boolean isCropCircle() {\n        return isCropCircle;\n    }\n\n    public String getAuthority() {\n        return authority;\n    }\n\n    public int getAspectX() {\n        return aspectX;\n    }\n\n    public int getAspectY() {\n        return aspectY;\n    }\n\n    public int getOutputX() {\n        return outputX;\n    }\n\n    public int getOutputY() {\n        return outputY;\n    }\n\n    public int getDestQuality() {\n        return destQuality;\n    }\n\n    public Builder rebuild() {\n        return new Builder(this);\n    }\n\n    /**\n     * 构建 Config 对象\n     */\n    public static class Builder {\n\n        private CropperConfig mConfig;\n\n        private Builder() {\n            mConfig = new CropperConfig();\n        }\n\n        private Builder(@NonNull CropperConfig config) {\n            this.mConfig = config;\n        }\n\n        /**\n         * 设置是否为圆形裁剪区域\n         */\n        public Builder setCropCircle(boolean isCropCircle) {\n            this.mConfig.isCropCircle = isCropCircle;\n            return this;\n        }\n\n        /**\n         * 设置裁剪的尺寸\n         */\n        public Builder setCropSize(int width, int height) {\n            this.mConfig.outputX = width;\n            this.mConfig.outputY = height;\n            return this;\n        }\n\n        /**\n         * 设置裁剪的比例\n         */\n        public Builder setAspectSize(int x, int y) {\n            this.mConfig.aspectX = x;\n            this.mConfig.aspectY = y;\n            return this;\n        }\n\n        /**\n         * 设置 FileProvider 的路径, 7.0 以后用于查找 URI\n         */\n        public Builder setAuthority(@NonNull String authorities) {\n            Preconditions.checkNotEmpty(authorities);\n            mConfig.authority = authorities;\n            return this;\n        }\n\n        /**\n         * 设置需要裁剪的图片 URI 地址\n         */\n        public Builder setOriginUri(@NonNull Uri originUri) {\n            this.mConfig.originUri = originUri;\n            return this;\n        }\n\n        /**\n         * 设置文件输出相对路径, 拍摄后的图片会生成在目录下\n         * <p>\n         * 绝对路径: \"/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum\"\n         * 相对路径: \"SAlbum\"\n         * <p>\n         * 注:\n         * Android 10 无法在外部存储卡随意创建文件, 因此会在对应的媒体目录下追加相对路径\n         * 如: \"/storage/emulated/0/\" + {@link android.os.Environment#DIRECTORY_PICTURES} + \"SAlbum\"\n         *\n         * @param relativePath 若是传 null, 则会在 \"/storage/emulated/0/\"\n         *                     + {@link android.os.Environment#DIRECTORY_PICTURES} 中创建\n         */\n        public Builder setRelativePath(@Nullable String relativePath) {\n            this.mConfig.relativePath = relativePath;\n            return this;\n        }\n\n        /**\n         * 设置裁剪后压缩的质量\n         */\n        public Builder setCropQuality(int quality) {\n            mConfig.destQuality = quality;\n            return this;\n        }\n\n        @NonNull\n        public CropperConfig build() {\n            if (TextUtils.isEmpty(mConfig.authority)) {\n                throw new UnsupportedOperationException(\"Please invoke setAuthority correct\");\n            }\n            return mConfig;\n        }\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/copper/com/sharry/lib/album/CropperFragment.java",
    "content": "package com.sharry.lib.album;\n\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.app.FragmentManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.ParcelFileDescriptor;\nimport android.provider.MediaStore;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * 调用系统裁剪工具\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 4:53 PM\n */\npublic class CropperFragment extends Fragment {\n\n    public static final String TAG = CropperFragment.class.getSimpleName();\n    private static final int REQUEST_CODE_CROP = 446;\n    public static final String INTENT_ACTION_START_CROP = \"com.android.camera.action.CROP\";\n\n    /**\n     * Get callback fragment from here.\n     */\n    @Nullable\n    public static CropperFragment getInstance(@NonNull Activity bind) {\n        if (ActivityStateUtil.isIllegalState(bind)) {\n            return null;\n        }\n        CropperFragment callbackFragment = findFragmentFromActivity(bind);\n        if (callbackFragment == null) {\n            callbackFragment = new CropperFragment();\n            FragmentManager fragmentManager = bind.getFragmentManager();\n            fragmentManager.beginTransaction()\n                    .add(callbackFragment, TAG)\n                    .commitAllowingStateLoss();\n            fragmentManager.executePendingTransactions();\n        }\n        return callbackFragment;\n    }\n\n    /**\n     * 在 Activity 中通过 TAG 去寻找我们添加的 Fragment\n     */\n    private static CropperFragment findFragmentFromActivity(@NonNull Activity activity) {\n        return (CropperFragment) activity.getFragmentManager().findFragmentByTag(TAG);\n    }\n\n    private File mTempFile;\n    private Context mContext;\n    private CropperConfig mConfig;\n    private CropperCallback mCropperCallback;\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        mContext = context;\n    }\n\n    @Override\n    public void onAttach(Activity activity) {\n        super.onAttach(activity);\n        mContext = activity;\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setRetainInstance(true);\n    }\n\n    /**\n     * 开始裁剪\n     */\n    public void cropPicture(CropperConfig config, CropperCallback callback) {\n        this.mConfig = config;\n        this.mCropperCallback = callback;\n        // Create temp file associated with crop function.\n        mTempFile = FileUtil.createTempJpegFile(mContext);\n        try {\n            // Get URI associated with target file.\n            Uri tempUri = FileUtil.getUriFromFile(mContext, config.getAuthority(), mTempFile);\n            // Completion intent instance.\n            Intent intent = new Intent(INTENT_ACTION_START_CROP);\n            completion(intent, config, config.getOriginUri(), tempUri);\n            // launch crop Activity\n            startActivityForResult(intent, REQUEST_CODE_CROP);\n        } catch (Throwable e) {\n            mCropperCallback.onCropFailed();\n        }\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (mCropperCallback == null) {\n            return;\n        }\n        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_CROP) {\n            try {\n                // 创建最终的目标文件, 将图片从临时文件压缩到指定的目录\n                if (VersionUtil.isQ()) {\n                    Uri uri = FileUtil.createJpegPendingItem(mContext, mConfig.getRelativePath());\n                    ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri, \"w\");\n                    CompressUtil.doCompress(mTempFile.getAbsolutePath(), pfd.getFileDescriptor(), mConfig.getDestQuality());\n                    FileUtil.publishPendingItem(mContext, uri);\n                    MediaMeta mediaMeta = MediaMeta.create(uri, FileUtil.getImagePath(mContext, uri), true);\n                    mCropperCallback.onCropComplete(mediaMeta);\n                } else {\n                    File file = FileUtil.createJpegFile(mContext, mConfig.getRelativePath());\n                    Uri uri = FileUtil.getUriFromFile(mContext, mConfig.getAuthority(), file);\n                    ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri, \"w\");\n                    CompressUtil.doCompress(mTempFile.getAbsolutePath(), pfd.getFileDescriptor(), mConfig.getDestQuality());\n                    FileUtil.notifyMediaStore(mContext, file.getAbsolutePath());\n                    MediaMeta mediaMeta = MediaMeta.create(uri, file.getAbsolutePath(), true);\n                    mCropperCallback.onCropComplete(mediaMeta);\n                }\n            } catch (Exception e) {\n                mCropperCallback.onCropFailed();\n            } finally {\n                if (mTempFile != null) {\n                    mTempFile.delete();\n                }\n            }\n        } else {\n            mCropperCallback.onCropFailed();\n        }\n    }\n\n    private void completion(Intent intent, CropperConfig config, Uri originUri, Uri tempUri) {\n        // 可以选择图片类型, 如果是*表明所有类型的图片\n        intent.setDataAndType(originUri, \"image/*\");\n        // 设置可裁剪状态\n        intent.putExtra(\"crop\", true);\n        // 裁剪时是否保留图片的比例, 这里的比例是1:1\n        intent.putExtra(\"scale\", config.getAspectX() == config.getAspectY());\n        // X方向上的比例\n        intent.putExtra(\"aspectX\", config.getAspectX());\n        // Y方向上的比例\n        intent.putExtra(\"aspectY\", config.getAspectY());\n        // 裁剪区域的宽\n        intent.putExtra(\"outputX\", config.getOutputX());\n        // 裁剪区域的宽\n        intent.putExtra(\"outputY\", config.getOutputY());\n        // 是否将数据保留在Bitmap中返回, 返回的缩略图效果模糊\n        intent.putExtra(\"return-data\", false);\n        // 设置输出的格式\n        intent.putExtra(\"outputFormat\", Bitmap.CompressFormat.JPEG.toString());\n        // 裁剪后的保存路径, 这里的 URI 不需要区分\n        intent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri);\n        // 不启用人脸识别\n        intent.putExtra(\"noFaceDetection\", true);\n        // 安卓 6.0 以上版本添加读写权限请求\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);\n            // 将存储图片的 uri 读写权限授权给剪裁工具应用\n            List<ResolveInfo> resInfoList = mContext.getPackageManager()\n                    .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n            for (ResolveInfo resolveInfo : resInfoList) {\n                String packageName = resolveInfo.activityInfo.packageName;\n                int modeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n                        | Intent.FLAG_GRANT_READ_URI_PERMISSION;\n                getActivity().grantUriPermission(packageName, originUri, modeFlags);\n                getActivity().grantUriPermission(packageName, tempUri, modeFlags);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/copper/com/sharry/lib/album/CropperManager.java",
    "content": "package com.sharry.lib.album;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\n/**\n * 图片裁剪的入口\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:09 PM\n */\npublic class CropperManager {\n\n    private static final String TAG = CropperManager.class.getSimpleName();\n    private static String[] sPermissions = {\n            Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            Manifest.permission.READ_EXTERNAL_STORAGE\n    };\n\n    public static CropperManager with(@NonNull Context context) {\n        if (context instanceof Activity) {\n            Activity activity = (Activity) context;\n            return new CropperManager(activity);\n        } else {\n            throw new IllegalArgumentException(TAG + \".with -> Context can not cast to Activity\");\n        }\n    }\n\n    private Activity mBind;\n    private CropperConfig mConfig;\n\n    private CropperManager(Activity activity) {\n        this.mBind = activity;\n    }\n\n    /**\n     * 设置配置属性\n     */\n    public CropperManager setConfig(@NonNull CropperConfig config) {\n        this.mConfig = Preconditions.checkNotNull(config, \"Please ensure config not null!\");\n        return this;\n    }\n\n    /**\n     * 裁剪图片\n     */\n    public void crop(@NonNull final CropperCallbackLambda callback) {\n        crop(new CropperCallback() {\n            @Override\n            public void onCropComplete(@NonNull MediaMeta meta) {\n                callback.onCropped(meta);\n            }\n\n            @Override\n            public void onCropFailed() {\n                callback.onCropped(null);\n            }\n        });\n    }\n\n    /**\n     * 裁剪图片\n     */\n    public void crop(@NonNull final CropperCallback callback) {\n        Preconditions.checkNotNull(callback, \"Please ensure callback not null!\");\n        Preconditions.checkNotNull(mConfig, \"Please ensure setConfig correct!\");\n        PermissionsHelper.with(mBind)\n                .request(sPermissions)\n                .execute(new PermissionsCallback() {\n                    @Override\n                    public void onResult(boolean granted) {\n                        if (granted) {\n                            cropActual(callback);\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 裁剪图片\n     */\n    private void cropActual(@NonNull final CropperCallback callback) {\n        // 若未指定目的路径, 则在系统相册的路径下创建图片文件\n        if (mConfig.getOriginUri() == null) {\n            throw new UnsupportedOperationException(TAG + \".takeActual -> Please ensure crop \" +\n                    \"target uri is valuable.\");\n        }\n        // 指定默认, FileProvider 的 authority\n        if (TextUtils.isEmpty(mConfig.getAuthority())) {\n            throw new UnsupportedOperationException(TAG + \"Please ensure u set FileProvider authority correct!.\");\n        }\n        // 执行回调\n        CropperFragment callbackFragment = CropperFragment.getInstance(mBind);\n        if (callbackFragment == null) {\n            Log.e(TAG, \"Launch crop activity failed.\");\n            return;\n        }\n        callbackFragment.cropPicture(mConfig, callback);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/FolderAdapter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.List;\n\n/**\n * 图片文件夹的 Adapter\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 15:02\n */\nclass FolderAdapter extends RecyclerView.Adapter<FolderAdapter.ViewHolder> {\n\n    final Context context;\n    final List<FolderModel> data;\n    final AdapterInteraction callback;\n\n    FolderAdapter(Context context, List<FolderModel> data) {\n        if (context instanceof AdapterInteraction) {\n            this.callback = (AdapterInteraction) context;\n        } else {\n            throw new IllegalArgumentException(context + \"must implements \" +\n                    FolderAdapter.class.getSimpleName() + \".Interaction\");\n        }\n        this.context = context;\n        this.data = data;\n    }\n\n    @NonNull\n    @Override\n    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext()).inflate(\n                R.layout.lib_album_recycle_item_folder, parent, false);\n        return new ViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {\n        FolderModel folder = data.get(holder.getAdapterPosition());\n        if (folder == null || folder.getMetas() == null || folder.getMetas().isEmpty()) {\n            return;\n        }\n        MediaMeta firstMeta = folder.getMetas().get(0);\n        if (firstMeta.isPicture) {\n            Loader.loadPicture(context, firstMeta, holder.ivPreview);\n        } else {\n            Loader.loadVideo(context, firstMeta, holder.ivPreview);\n        }\n        holder.tvFolderName.setText(folder.getName());\n    }\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    /**\n     * Communicate with Activity.\n     */\n    public interface AdapterInteraction {\n\n        void onFolderChecked(int position);\n\n    }\n\n    class ViewHolder extends RecyclerView.ViewHolder {\n        private ImageView ivPreview;\n        private TextView tvFolderName;\n\n        private ViewHolder(View itemView) {\n            super(itemView);\n            tvFolderName = itemView.findViewById(R.id.tv_folder_name);\n            ivPreview = itemView.findViewById(R.id.iv_preview);\n            itemView.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    callback.onFolderChecked(getAdapterPosition());\n                }\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/FolderModel.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Describe pictures that in the same folder.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/8/31 22:29\n */\nclass FolderModel {\n\n    private final String name;\n    private final List<MediaMeta> metas = new ArrayList<>();\n\n    FolderModel(String name) {\n        this.name = name;\n    }\n\n    String getName() {\n        return name;\n    }\n\n    List<MediaMeta> getMetas() {\n        return metas;\n    }\n\n    synchronized void addMeta(@NonNull MediaMeta meta) {\n        int insertIndex = 0;\n        for (; insertIndex < metas.size(); insertIndex++) {\n            if (metas.get(insertIndex).date < meta.date) {\n                break;\n            }\n        }\n        metas.add(insertIndex, meta);\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerActivity.java",
    "content": "package com.sharry.lib.album;\n\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.content.res.ColorStateList;\nimport android.graphics.Canvas;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.core.content.ContextCompat;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.material.appbar.AppBarLayout;\nimport com.google.android.material.bottomsheet.BottomSheetBehavior;\nimport com.google.android.material.floatingactionbutton.FloatingActionButton;\nimport com.google.android.material.snackbar.Snackbar;\nimport com.sharry.lib.album.toolbar.SToolbar;\nimport com.sharry.lib.album.toolbar.TextViewOptions;\n\nimport java.util.ArrayList;\n\n/**\n * 图片选择器的 Activity\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.3\n * @since 2018/9/1 10:17\n */\npublic class PickerActivity extends AppCompatActivity implements PickerContract.IView,\n        PickerAdapter.Interaction,\n        FolderAdapter.AdapterInteraction,\n        View.OnClickListener {\n\n    /**\n     * Constants.\n     */\n    public static final int REQUEST_CODE = 267;\n    public static final String RESULT_EXTRA_PICKED_PICTURES = \"result_intent_extra_picked_pictures\";\n    private static final String EXTRA_CONFIG = \"start_intent_extra_config\";\n\n    /**\n     * U can launch PickerActivity from here.\n     * If U picked success, it will return picked data, U can got it like\n     * {@code ArrayList<String> paths = data.getStringArrayListExtra(PickerActivity.RESULT_EXTRA_PICKED_PICTURES)}\n     *\n     * @param from     The Activity that request launch PickerActivity.\n     * @param resultTo Result data will return to this instance.\n     * @param config   Launch PickerActivity required data.\n     */\n    public static void launchActivityForResult(Activity from, Fragment resultTo, PickerConfig config) {\n        Intent intent = new Intent(from, PickerActivity.class);\n        intent.putExtra(PickerActivity.EXTRA_CONFIG, config);\n        resultTo.startActivityForResult(intent, REQUEST_CODE);\n    }\n\n    /**\n     * Presenter associated with this Activity.\n     */\n    private PickerContract.IPresenter mPresenter;\n\n    /**\n     * Views\n     */\n    private SToolbar mToolbar;\n    private ProgressBar mProgressBar;\n    private TextView mTvToolbarFolderName;\n    private TextView mTvToolbarEnsure;\n    private RecyclerView mRvPicker;\n    private ViewGroup mMenuNavContainer;\n    private ImageView mIvNavIndicator;\n    private TextView mTvFolderName;\n    private TextView mTvPreview;\n    private RecyclerView mRvFolders;\n    private FloatingActionButton mFab;\n\n    /**\n     * CoordinatorLayout behaviors.\n     */\n    private BottomSheetBehavior mBottomMenuBehavior;\n    private PicturePickerFabBehavior mFabBehavior;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.lib_album_activity_picker);\n        initTitle();\n        initViews();\n        initPresenter();\n        registerLocalBroadcast();\n    }\n\n    protected void initTitle() {\n        // 初始化视图\n        mToolbar = findViewById(R.id.toolbar);\n        // 设置标题文本\n        mToolbar.setTitleText(getString(R.string.lib_album_picker_all_picture));\n        mTvToolbarFolderName = mToolbar.getTitleText();\n        // 添加图片确认按钮\n        mToolbar.addRightMenuText(\n                TextViewOptions.Builder()\n                        .setText(getString(R.string.lib_album_picker_ensure))\n                        .setTextSize(15)\n                        .setListener(this)\n                        .build()\n        );\n        mTvToolbarEnsure = mToolbar.getRightMenuView(0);\n    }\n\n    protected void initViews() {\n        // Pictures recycler view.\n        mRvPicker = findViewById(R.id.rv_picker);\n        mRvPicker.addItemDecoration(new RecyclerView.ItemDecoration() {\n            @Override\n            public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n                super.onDraw(c, parent, state);\n                mPresenter.handleRecycleViewDraw(parent);\n            }\n        });\n        // Bottom navigation menu.\n        mMenuNavContainer = findViewById(R.id.rv_menu_nav_container);\n        mIvNavIndicator = findViewById(R.id.iv_nav_indicator);\n        mTvFolderName = findViewById(R.id.tv_folder_name);\n        mTvPreview = findViewById(R.id.tv_preview);\n        mRvFolders = findViewById(R.id.recycle_folders);\n        mTvFolderName.setOnClickListener(this);\n        mTvPreview.setOnClickListener(this);\n        mRvFolders.setLayoutManager(new LinearLayoutManager(this));\n        mRvFolders.setHasFixedSize(true);\n        mBottomMenuBehavior = BottomSheetBehavior.from(findViewById(R.id.ll_bottom_menu));\n        mBottomMenuBehavior.setBottomSheetCallback(new BottomMenuNavigationCallback());\n        // Floating action bar.\n        mFab = findViewById(R.id.fab);\n        mFab.setOnClickListener(this);\n        mFabBehavior = PicturePickerFabBehavior.from(mFab);\n        // Progress bar.\n        mProgressBar = findViewById(R.id.progress_bar);\n    }\n\n    protected void initPresenter() {\n        PickerConfig config = getIntent().getParcelableExtra(EXTRA_CONFIG);\n        if (config != null) {\n            mPresenter = new PickerPresenter(this, config);\n        }\n    }\n\n    private final BroadcastReceiver mBrPickedSetChanged = new BroadcastReceiver() {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            if (mPresenter != null) {\n                MediaMeta mediaMeta = intent.getParcelableExtra(WatcherActivity.BROADCAST_EXTRA_DATA);\n                mPresenter.handlePickedSetChanged(mediaMeta);\n            }\n        }\n    };\n\n    private final BroadcastReceiver mBrPickedSetEnsure = new BroadcastReceiver() {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            mPresenter.handleEnsureClicked();\n        }\n    };\n\n    private void registerLocalBroadcast() {\n        LocalBroadcastManager.getInstance(this)\n                .registerReceiver(\n                        mBrPickedSetChanged,\n                        new IntentFilter(WatcherActivity.BROADCAST_PICKED_SET_CHANGED)\n                );\n        LocalBroadcastManager.getInstance(this)\n                .registerReceiver(\n                        mBrPickedSetEnsure,\n                        new IntentFilter(WatcherActivity.BROADCAST_PICKED_SET_ENSURE)\n                );\n    }\n\n    @Override\n    public void onBackPressed() {\n        if (BottomSheetBehavior.STATE_COLLAPSED != mBottomMenuBehavior.getState()) {\n            mBottomMenuBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);\n        } else {\n            super.onBackPressed();\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        LocalBroadcastManager.getInstance(this).unregisterReceiver(mBrPickedSetChanged);\n        LocalBroadcastManager.getInstance(this).unregisterReceiver(mBrPickedSetEnsure);\n        mPresenter.handleViewDestroy();\n        super.onDestroy();\n    }\n\n    //////////////////////////////////////////////PickerContract.IView/////////////////////////////////////////////////\n\n    @Override\n    public void setToolbarBackgroundColor(int color) {\n        mToolbar.setBackgroundColor(color);\n    }\n\n    @Override\n    public void setToolbarBackgroundDrawable(int drawableId) {\n        mToolbar.setBackgroundDrawableRes(drawableId);\n    }\n\n    @Override\n    public void setToolbarScrollable(boolean isScrollable) {\n        if (isScrollable) {\n            AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();\n            params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL\n                    | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS\n                    | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);\n            mToolbar.setLayoutParams(params);\n        }\n    }\n\n    @Override\n    public void setBackgroundColor(int color) {\n        mRvPicker.setBackgroundColor(color);\n    }\n\n    @Override\n    public void setSpanCount(int spanCount) {\n        mRvPicker.setLayoutManager(new GridLayoutManager(this, spanCount));\n    }\n\n    @Override\n    public void setPickerAdapter(@NonNull PickerConfig config,\n                                 @NonNull ArrayList<MediaMeta> metas,\n                                 @NonNull ArrayList<MediaMeta> userPickedMetas) {\n        mRvPicker.setAdapter(new PickerAdapter(this, config,\n                metas, userPickedMetas));\n    }\n\n    @Override\n    public void setFolderAdapter(@NonNull ArrayList<FolderModel> folders) {\n        mRvFolders.setAdapter(new FolderAdapter(this, folders));\n    }\n\n    @Override\n    public void setFabColor(int color) {\n        mFab.setBackgroundTintList(ColorStateList.valueOf(color));\n    }\n\n    @Override\n    public void setProgressBarVisible(boolean visible) {\n        mProgressBar.setVisibility(visible ? View.VISIBLE : View.GONE);\n    }\n\n    @Override\n    public void setFabVisible(boolean isVisible) {\n        if (isVisible) {\n            mFab.show();\n        } else {\n            mFab.hide();\n        }\n    }\n\n    @Override\n    public void setPictureFolderText(@NonNull String folderName) {\n        // 更新文件夹名称\n        mTvFolderName.setText(folderName);\n        mTvToolbarFolderName.setText(folderName);\n    }\n\n    @Override\n    public void setToolbarEnsureText(@NonNull CharSequence content) {\n        mTvToolbarEnsure.setText(content);\n    }\n\n    @Override\n    public void setPreviewText(@NonNull CharSequence content) {\n        mTvPreview.setText(content);\n    }\n\n    @Override\n    public void notifyDisplaySetItemChanged(int changedIndex) {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvPicker.getAdapter()) != null) {\n            adapter.notifyItemChanged(changedIndex);\n        }\n    }\n\n    @Override\n    public void notifyDisplaySetChanged() {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvPicker.getAdapter()) != null) {\n            adapter.notifyDataSetChanged();\n        }\n    }\n\n    @Override\n    public void notifyNewMetaInsertToFirst() {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvPicker.getAdapter()) != null) {\n            adapter.notifyItemInserted(1);\n        }\n    }\n\n    @Override\n    public void notifyFolderDataSetChanged() {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvFolders.getAdapter()) != null) {\n            adapter.notifyDataSetChanged();\n        }\n    }\n\n    @Override\n    public void showMsg(@NonNull String msg) {\n        Snackbar.make(mFab, msg, Snackbar.LENGTH_LONG).show();\n    }\n\n    @Override\n    public void setResultAndFinish(@NonNull ArrayList<MediaMeta> pickedPaths) {\n        Intent intent = new Intent();\n        intent.putExtra(PickerActivity.RESULT_EXTRA_PICKED_PICTURES, pickedPaths);\n        setResult(Activity.RESULT_OK, intent);\n        finish();\n    }\n\n    //////////////////////////////////////////////PickerAdapter.Interaction/////////////////////////////////////////////////\n\n    @Override\n    public void onCameraClicked() {\n        mPresenter.handleCameraClicked();\n    }\n\n    @Override\n    public void onPictureClicked(@NonNull View itemView, @NonNull Uri uri, int position) {\n        mPresenter.handlePictureClicked(position, itemView);\n    }\n\n    @Override\n    public boolean onPictureChecked(@NonNull MediaMeta checkedMeta) {\n        return mPresenter.handlePictureChecked(checkedMeta);\n    }\n\n    @Override\n    public void onPictureRemoved(@NonNull MediaMeta removedMeta) {\n        mPresenter.handlePictureUnchecked(removedMeta);\n    }\n\n    //////////////////////////////////////////////FolderAdapter.Interaction/////////////////////////////////////////////////\n\n    @Override\n    public void onFolderChecked(int position) {\n        mPresenter.handleFolderChecked(position);\n        mBottomMenuBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);\n    }\n\n    //////////////////////////////////////////////View.OnClickListener/////////////////////////////////////////////////\n\n    @Override\n    public void onClick(View v) {\n        // 底部菜单按钮\n        if (v.getId() == R.id.tv_folder_name) {\n            mBottomMenuBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);\n        }\n        // 预览按钮\n        else if (v.getId() == R.id.tv_preview) {\n            mPresenter.handlePreviewClicked();\n        }\n        // 确认按钮\n        else if (v == mTvToolbarEnsure || v.getId() == R.id.fab) {\n            mPresenter.handleEnsureClicked();\n        }\n    }\n\n    /**\n     * Callback associated with bottom menu navigation bar.\n     * Method will be invoked when menu scrolled.\n     */\n    private class BottomMenuNavigationCallback extends BottomSheetBehavior.BottomSheetCallback {\n\n        private final Drawable indicatorDrawable;\n        private final int bgCollapsedColor;\n        private final int bgExpandColor;\n        private final int textCollapsedColor;\n        private final int textExpandColor;\n\n        BottomMenuNavigationCallback() {\n            indicatorDrawable = mIvNavIndicator.getDrawable();\n            bgCollapsedColor = ContextCompat.getColor(PickerActivity.this,\n                    R.color.lib_album_picker_bottom_menu_nav_bg_collapsed_color);\n            bgExpandColor = ContextCompat.getColor(PickerActivity.this,\n                    R.color.lib_album_picker_bottom_menu_navi_bg_expand_color);\n            textCollapsedColor = ContextCompat.getColor(PickerActivity.this,\n                    R.color.lib_album_picker_bottom_menu_nav_text_collapsed_color);\n            textExpandColor = ContextCompat.getColor(PickerActivity.this,\n                    R.color.lib_album_picker_bottom_menu_navi_text_expand_color);\n        }\n\n        @Override\n        public void onStateChanged(@NonNull View view, int state) {\n            mFabBehavior.setBehaviorValid(BottomSheetBehavior.STATE_COLLAPSED == state);\n        }\n\n        @Override\n        public void onSlide(@NonNull View view, float fraction) {\n            // Get background color associate with the bottom menu navigation bar.\n            int bgColor = ColorUtil.gradualChanged(fraction, bgCollapsedColor, bgExpandColor);\n            mMenuNavContainer.setBackgroundColor(bgColor);\n            // Get text color associate with the bottom menu navigation bar.\n            int textColor = ColorUtil.gradualChanged(fraction, textCollapsedColor, textExpandColor);\n            // Set text drawable color before set text color with the purpose of decrease view draw.\n            if (VersionUtil.isLollipop()) {\n                indicatorDrawable.setTint(textColor);\n            }\n            // Set texts colors associate with the bottom menu.\n            mTvFolderName.setTextColor(textColor);\n            mTvPreview.setTextColor(textColor);\n        }\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerAdapter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Adapter associated with PicturePicker.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.3\n * @since 2018/9/1 10:19\n */\nclass PickerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {\n\n    private static final int ITEM_TYPE_PICTURE = 838;\n    private static final int ITEM_TYPE_CAMERA_HEADER = 347;\n    private static final int ITEM_TYPE_VIDEO = 664;\n\n    private final Context mContext;\n    private final PickerConfig mConfig;\n    private final List<MediaMeta> mDataSet;\n    private final List<MediaMeta> mPickedSet;\n    private final Interaction mInteraction;\n    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());\n    private final Runnable mRefreshIndicatorRunnable = new Runnable() {\n        @Override\n        public void run() {\n            notifyDataSetChanged();\n        }\n    };\n\n    PickerAdapter(Context context,\n                  PickerConfig config,\n                  ArrayList<MediaMeta> dataSet,\n                  ArrayList<MediaMeta> pickedSet) {\n        if (context instanceof Interaction) {\n            this.mInteraction = (Interaction) context;\n        } else {\n            throw new IllegalArgumentException(context + \"must implements \" +\n                    PickerAdapter.class.getSimpleName() + \".Interaction\");\n        }\n        this.mContext = context;\n        this.mConfig = config;\n        this.mDataSet = dataSet;\n        this.mPickedSet = pickedSet;\n    }\n\n    @Override\n    public int getItemViewType(int position) {\n        if (mConfig.isCameraSupport() && position == 0) {\n            return ITEM_TYPE_CAMERA_HEADER;\n        }\n        int relativePosition = mConfig.isCameraSupport() ? position - 1 : position;\n        MediaMeta meta = mDataSet.get(relativePosition);\n        int result;\n        if (meta == null || meta.isPicture) {\n            result = ITEM_TYPE_PICTURE;\n        } else {\n            result = ITEM_TYPE_VIDEO;\n        }\n        return result;\n    }\n\n    @NonNull\n    @Override\n    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        RecyclerView.ViewHolder vh;\n        switch (viewType) {\n            case ITEM_TYPE_CAMERA_HEADER:\n                vh = new CameraHeaderHolder(parent);\n                break;\n            case ITEM_TYPE_VIDEO:\n                vh = new VideoViewHolder(parent);\n                break;\n            case ITEM_TYPE_PICTURE:\n            default:\n                vh = new PictureViewHolder(parent);\n                break;\n        }\n        return vh;\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {\n        if (holder instanceof CameraHeaderHolder) {\n            // nothing.\n        } else if (holder instanceof PictureViewHolder) {\n            int relativePosition = mConfig.isCameraSupport() ? position - 1 : position;\n            if (relativePosition < 0) {\n                return;\n            }\n            final MediaMeta meta = mDataSet.get(relativePosition);\n            if (meta == null) {\n                return;\n            }\n            bindPictureItem((PictureViewHolder) holder, meta);\n        } else if (holder instanceof VideoViewHolder) {\n            int relativePosition = mConfig.isCameraSupport() ? position - 1 : position;\n            if (relativePosition < 0) {\n                return;\n            }\n            final MediaMeta meta = mDataSet.get(relativePosition);\n            if (meta == null) {\n                return;\n            }\n            bindVideoItem((VideoViewHolder) holder, meta);\n        } else {\n            // nothing.\n        }\n    }\n\n    @Override\n    public int getItemCount() {\n        return mDataSet.size() + (mConfig.isCameraSupport() ? 1 : 0);\n    }\n\n    /**\n     * 绑定图像视图\n     */\n    private void bindPictureItem(final PictureViewHolder holder, final MediaMeta meta) {\n        holder.ivPicture.setBackgroundColor(mConfig.getPickerItemBackgroundColor());\n        holder.ivPicture.setScaleType(ImageView.ScaleType.CENTER_CROP);\n        holder.ivGifTag.setVisibility(Constants.MIME_TYPE_GIF.equals(meta.mimeType) ? View.VISIBLE : View.GONE);\n        Loader.loadPicture(mContext, meta, holder.ivPicture);\n        // 判断当前 uri 是否被选中了\n        final int index = mPickedSet.indexOf(meta);\n        // 设置点击监听\n        holder.checkIndicator.setVisibility(View.VISIBLE);\n        holder.checkIndicator.setCheckedWithoutAnimator(index != -1);\n        holder.checkIndicator.setText(String.valueOf(index + 1));\n    }\n\n    /**\n     * 绑定视频视图\n     */\n    private void bindVideoItem(final VideoViewHolder holder, final MediaMeta meta) {\n        holder.ivPicture.setBackgroundColor(mConfig.getPickerItemBackgroundColor());\n        holder.ivPicture.setScaleType(ImageView.ScaleType.CENTER_CROP);\n        // 加载视频第一帧\n        Loader.loadVideo(mContext, meta, holder.ivPicture);\n        // 判断当前 uri 是否被选中了\n        final int index = mPickedSet.indexOf(meta);\n        // 设置点击监听\n        holder.checkIndicator.setVisibility(View.VISIBLE);\n        holder.checkIndicator.setCheckedWithoutAnimator(index != -1);\n        holder.checkIndicator.setText(String.valueOf(index + 1));\n        // 设置时长\n        holder.tvDuration.setText(DateUtil.format(meta.duration));\n    }\n\n    /**\n     * 通知选中图片的角标变更\n     */\n    private void notifyCheckedIndicatorChanged() {\n        mMainThreadHandler.postDelayed(mRefreshIndicatorRunnable, 300);\n    }\n\n    /**\n     * Camera header item view holder.\n     */\n    class CameraHeaderHolder extends RecyclerView.ViewHolder implements View.OnClickListener {\n\n        CameraHeaderHolder(ViewGroup parent) {\n            super(LayoutInflater.from(parent.getContext()).inflate(\n                    R.layout.lib_album_recycle_item_header_camera,\n                    parent,\n                    false\n            ));\n            // 将 ItemView 的高度修正为宽度 parent 的宽度的三分之一\n            int itemSize = (parent.getMeasuredWidth() - parent.getPaddingLeft()\n                    - parent.getPaddingRight()) / mConfig.getSpanCount();\n            ViewGroup.LayoutParams itemParams = itemView.getLayoutParams();\n            itemParams.height = itemSize;\n            itemView.setLayoutParams(itemParams);\n            // 注入点击事件\n            itemView.setOnClickListener(this);\n        }\n\n        @Override\n        public void onClick(View v) {\n            mInteraction.onCameraClicked();\n        }\n    }\n\n    /**\n     * Picture item view holder\n     */\n    class PictureViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {\n\n        final ImageView ivPicture;\n        final CheckedIndicatorView checkIndicator;\n        final ImageView ivGifTag;\n        final Runnable pictureClickedRunnable = new Runnable() {\n            @Override\n            public void run() {\n                performPictureClicked();\n            }\n        };\n\n        PictureViewHolder(ViewGroup parent) {\n            super(\n                    LayoutInflater.from(parent.getContext())\n                            .inflate(R.layout.lib_album_recycle_item_picture, parent, false)\n            );\n            // Initialize ivPicture.\n            ivPicture = itemView.findViewById(R.id.iv_picture);\n            ivPicture.setOnClickListener(this);\n            // Initialize ivGifTag\n            ivGifTag = itemView.findViewById(R.id.iv_gif_tag);\n            // Initialize checkIndicator.\n            checkIndicator = itemView.findViewById(R.id.check_indicator);\n            checkIndicator.setTextColor(mConfig.getIndicatorTextColor());\n            checkIndicator.setSolidColor(mConfig.getIndicatorSolidColor());\n            checkIndicator.setBorderColor(\n                    mConfig.getIndicatorBorderCheckedColor(),\n                    mConfig.getIndicatorBorderUncheckedColor()\n            );\n            checkIndicator.setOnClickListener(this);\n            adjustItemView(parent);\n        }\n\n        @Override\n        public void onClick(View v) {\n            if (ivPicture == v) {\n                // 延时 100 mm , 等待水波纹动画结束\n                mMainThreadHandler.postDelayed(pictureClickedRunnable, 100);\n            } else if (checkIndicator == v) {\n                performCheckIndicatorClicked();\n            }\n        }\n\n        private void adjustItemView(ViewGroup parent) {\n            // 将 ItemView 的高度修正为宽度 parent 的宽度的三分之一\n            int itemSize = (parent.getMeasuredWidth() - parent.getPaddingLeft()\n                    - parent.getPaddingRight()) / mConfig.getSpanCount();\n            ViewGroup.LayoutParams itemParams = itemView.getLayoutParams();\n            itemParams.height = itemSize;\n            itemView.setLayoutParams(itemParams);\n            // 设置指示器的宽高为 ItemView 的五分之一\n            int indicatorSize = itemSize / 5;\n            ViewGroup.MarginLayoutParams indicatorParams =\n                    (ViewGroup.MarginLayoutParams) checkIndicator.getLayoutParams();\n            // 动态调整大小\n            indicatorParams.width = indicatorSize;\n            indicatorParams.height = indicatorSize;\n            // 动态调整 Margin\n            indicatorParams.rightMargin = indicatorSize / 5;\n            indicatorParams.topMargin = indicatorSize / 5;\n            checkIndicator.setLayoutParams(indicatorParams);\n            // 设置指示器的文本尺寸为指示器宽高的二分之一\n            checkIndicator.setTextSize(TypedValue.COMPLEX_UNIT_PX, indicatorSize / 2);\n        }\n\n        private void performPictureClicked() {\n            int relativePosition = mConfig.isCameraSupport() ? getAdapterPosition() - 1 : getAdapterPosition();\n            if (relativePosition < 0) {\n                return;\n            }\n            mInteraction.onPictureClicked(itemView, mDataSet.get(relativePosition).contentUri, relativePosition);\n        }\n\n        private void performCheckIndicatorClicked() {\n            // 获取当前点击图片的 path\n            int relativePosition = mConfig.isCameraSupport() ? getAdapterPosition() - 1 : getAdapterPosition();\n            if (relativePosition < 0) {\n                return;\n            }\n            MediaMeta meta = mDataSet.get(relativePosition);\n            // Checked-> Unchecked\n            if (checkIndicator.isChecked()) {\n                // 移除选中数据与状态\n                mInteraction.onPictureRemoved(meta);\n                checkIndicator.setChecked(false);\n                // 需要延时的更新索引角标\n                notifyCheckedIndicatorChanged();\n            }\n            // Unchecked -> Checked\n            else {\n                // 判断是否达到选择上限\n                checkIndicator.setChecked(mInteraction.onPictureChecked(meta));\n                // 设置文本\n                checkIndicator.setText(String.valueOf(mPickedSet.size()));\n            }\n        }\n\n    }\n\n    /**\n     * Video item view holder\n     */\n    class VideoViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {\n\n        final ImageView ivPicture;\n        final CheckedIndicatorView checkIndicator;\n        final TextView tvDuration;\n        final Runnable pictureClickedRunnable = new Runnable() {\n            @Override\n            public void run() {\n                performPictureClicked();\n            }\n        };\n\n        VideoViewHolder(ViewGroup parent) {\n            super(\n                    LayoutInflater.from(parent.getContext()).inflate(\n                            R.layout.lib_album_recycle_item_video,\n                            parent,\n                            false\n                    )\n            );\n            // Initialize ivPicture.\n            ivPicture = itemView.findViewById(R.id.iv_picture);\n            ivPicture.setOnClickListener(this);\n            // Initialize checkIndicator.\n            checkIndicator = itemView.findViewById(R.id.check_indicator);\n            checkIndicator.setTextColor(mConfig.getIndicatorTextColor());\n            checkIndicator.setSolidColor(mConfig.getIndicatorSolidColor());\n            checkIndicator.setBorderColor(\n                    mConfig.getIndicatorBorderCheckedColor(),\n                    mConfig.getIndicatorBorderUncheckedColor()\n            );\n            checkIndicator.setOnClickListener(this);\n            // Initialize tvDuration\n            tvDuration = itemView.findViewById(R.id.tv_duration);\n            // adjust.\n            adjustItemView(parent);\n        }\n\n        @Override\n        public void onClick(View v) {\n            if (ivPicture == v) {\n                // 延时 100 mm , 等待水波纹动画结束\n                mMainThreadHandler.postDelayed(pictureClickedRunnable, 100);\n            } else if (checkIndicator == v) {\n                performCheckIndicatorClicked();\n            }\n        }\n\n        private void adjustItemView(ViewGroup parent) {\n            // 将 ItemView 的高度修正为宽度 parent 的宽度的三分之一\n            int itemSize = (parent.getMeasuredWidth() - parent.getPaddingLeft()\n                    - parent.getPaddingRight()) / mConfig.getSpanCount();\n            ViewGroup.LayoutParams itemParams = itemView.getLayoutParams();\n            itemParams.height = itemSize;\n            itemView.setLayoutParams(itemParams);\n            // 设置指示器的宽高为 ItemView 的五分之一\n            int indicatorSize = itemSize / 5;\n            ViewGroup.MarginLayoutParams indicatorParams =\n                    (ViewGroup.MarginLayoutParams) checkIndicator.getLayoutParams();\n            // 动态调整大小\n            indicatorParams.width = indicatorSize;\n            indicatorParams.height = indicatorSize;\n            // 动态调整 Margin\n            indicatorParams.rightMargin = indicatorSize / 5;\n            indicatorParams.topMargin = indicatorSize / 5;\n            checkIndicator.setLayoutParams(indicatorParams);\n            // 设置指示器的文本尺寸为指示器宽高的二分之一\n            checkIndicator.setTextSize(TypedValue.COMPLEX_UNIT_PX, indicatorSize / 2);\n        }\n\n        private void performPictureClicked() {\n            int relativePosition = mConfig.isCameraSupport() ? getAdapterPosition() - 1 : getAdapterPosition();\n            if (relativePosition < 0) {\n                return;\n            }\n            mInteraction.onPictureClicked(itemView, mDataSet.get(relativePosition).contentUri, relativePosition);\n        }\n\n        private void performCheckIndicatorClicked() {\n            // 获取当前点击图片的 path\n            int relativePosition = mConfig.isCameraSupport() ? getAdapterPosition() - 1 : getAdapterPosition();\n            if (relativePosition < 0) {\n                return;\n            }\n            MediaMeta meta = mDataSet.get(relativePosition);\n            // Checked-> Unchecked\n            if (checkIndicator.isChecked()) {\n                // 移除选中数据与状态\n                mInteraction.onPictureRemoved(meta);\n                checkIndicator.setChecked(false);\n                // 需要延时的更新索引角标\n                notifyCheckedIndicatorChanged();\n            }\n            // Unchecked -> Checked\n            else {\n                // 判断是否达到选择上限\n                checkIndicator.setChecked(mInteraction.onPictureChecked(meta));\n                // 设置文本\n                checkIndicator.setText(String.valueOf(mPickedSet.size()));\n            }\n        }\n\n    }\n\n    /**\n     * Communicate with Activity.\n     */\n    interface Interaction {\n\n        void onCameraClicked();\n\n        void onPictureClicked(@NonNull View itemView, @NonNull Uri uri, int position);\n\n        boolean onPictureChecked(@NonNull MediaMeta checkedMeta);\n\n        void onPictureRemoved(@NonNull MediaMeta removedMeta);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerCallback.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\n\n/**\n * 图片选择器的回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:03 PM\n */\npublic interface PickerCallback {\n\n    /**\n     * 获取选中集合\n     *\n     * @param userPickedSet 用户选中的集合\n     */\n    void onPickedComplete(@NonNull ArrayList<MediaMeta> userPickedSet);\n\n    /**\n     * 未进行图片选取\n     */\n    void onPickedFailed();\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerCallbackLambda.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n/**\n * 图片选择器的回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:03 PM\n */\npublic interface PickerCallbackLambda {\n\n    /**\n     * 获取选中集合\n     *\n     * @param userPickedSet 用户选中的集合\n     */\n    void onPicked(@Nullable ArrayList<MediaMeta> userPickedSet);\n\n}"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerConfig.java",
    "content": "package com.sharry.lib.album;\n\nimport android.graphics.Color;\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n/**\n * 图片选择器的配置属性类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.1\n * @since 2018/11/29 17:07\n */\npublic class PickerConfig implements Parcelable {\n\n    static final int INVALIDATE_VALUE = -1;\n    static final int COLOR_DEFAULT = Color.parseColor(\"#ff64b6f6\");\n\n    protected PickerConfig(Parcel in) {\n        userPickedSet = in.createTypedArrayList(MediaMeta.CREATOR);\n        threshold = in.readInt();\n        spanCount = in.readInt();\n        toolbarBkgColor = in.readInt();\n        toolbarBkgDrawableResId = in.readInt();\n        pickerBackgroundColor = in.readInt();\n        pickerItemBackgroundColor = in.readInt();\n        indicatorTextColor = in.readInt();\n        indicatorSolidColor = in.readInt();\n        indicatorBorderCheckedColor = in.readInt();\n        indicatorBorderUncheckedColor = in.readInt();\n        isToolbarBehavior = in.readByte() != 0;\n        isFabBehavior = in.readByte() != 0;\n        isPickPicture = in.readByte() != 0;\n        isPickVideo = in.readByte() != 0;\n        isPickGif = in.readByte() != 0;\n        takerConfig = in.readParcelable(TakerConfig.class.getClassLoader());\n        cropperConfig = in.readParcelable(CropperConfig.class.getClassLoader());\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeTypedList(userPickedSet);\n        dest.writeInt(threshold);\n        dest.writeInt(spanCount);\n        dest.writeInt(toolbarBkgColor);\n        dest.writeInt(toolbarBkgDrawableResId);\n        dest.writeInt(pickerBackgroundColor);\n        dest.writeInt(pickerItemBackgroundColor);\n        dest.writeInt(indicatorTextColor);\n        dest.writeInt(indicatorSolidColor);\n        dest.writeInt(indicatorBorderCheckedColor);\n        dest.writeInt(indicatorBorderUncheckedColor);\n        dest.writeByte((byte) (isToolbarBehavior ? 1 : 0));\n        dest.writeByte((byte) (isFabBehavior ? 1 : 0));\n        dest.writeByte((byte) (isPickPicture ? 1 : 0));\n        dest.writeByte((byte) (isPickVideo ? 1 : 0));\n        dest.writeByte((byte) (isPickGif ? 1 : 0));\n        dest.writeParcelable(takerConfig, flags);\n        dest.writeParcelable(cropperConfig, flags);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public static final Creator<PickerConfig> CREATOR = new Creator<PickerConfig>() {\n        @Override\n        public PickerConfig createFromParcel(Parcel in) {\n            return new PickerConfig(in);\n        }\n\n        @Override\n        public PickerConfig[] newArray(int size) {\n            return new PickerConfig[size];\n        }\n    };\n\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    /**\n     * 用户已经选中的集合\n     */\n    private ArrayList<MediaMeta> userPickedSet;\n\n    /**\n     * 最大选取阈值\n     */\n    private int threshold = 9;\n\n    /**\n     * 每行展示数量\n     */\n    private int spanCount = 3;\n\n    /**\n     * Toolbar 背景\n     */\n    private int toolbarBkgColor = COLOR_DEFAULT;\n    private int toolbarBkgDrawableResId = INVALIDATE_VALUE;\n\n    /**\n     * 整体背景色\n     */\n    private int pickerBackgroundColor = INVALIDATE_VALUE;\n    private int pickerItemBackgroundColor = Color.WHITE;\n\n    /**\n     * 指示器背景色\n     */\n    private int indicatorTextColor = Color.WHITE;\n    private int indicatorSolidColor = COLOR_DEFAULT;\n    private int indicatorBorderCheckedColor = indicatorSolidColor;\n    private int indicatorBorderUncheckedColor = Color.WHITE;\n\n    /**\n     * 控制 Flag\n     */\n    private boolean isToolbarBehavior = false;\n    private boolean isFabBehavior = false;\n    private boolean isPickPicture = true;\n    private boolean isPickVideo = false;\n    private boolean isPickGif = false;\n\n    /**\n     * 其他功能的 Config\n     */\n    private TakerConfig takerConfig;\n    private CropperConfig cropperConfig;\n\n    private PickerConfig() {\n    }\n\n    @NonNull\n    public ArrayList<MediaMeta> getUserPickedSet() {\n        return userPickedSet;\n    }\n\n    public int getThreshold() {\n        return threshold;\n    }\n\n    public int getSpanCount() {\n        return spanCount;\n    }\n\n    public int getToolbarBkgColor() {\n        return toolbarBkgColor;\n    }\n\n    public int getToolbarBkgDrawableResId() {\n        return toolbarBkgDrawableResId;\n    }\n\n    public int getPickerBackgroundColor() {\n        return pickerBackgroundColor;\n    }\n\n    public int getPickerItemBackgroundColor() {\n        return pickerItemBackgroundColor;\n    }\n\n    public int getIndicatorTextColor() {\n        return indicatorTextColor;\n    }\n\n    public int getIndicatorSolidColor() {\n        return indicatorSolidColor;\n    }\n\n    public int getIndicatorBorderCheckedColor() {\n        return indicatorBorderCheckedColor;\n    }\n\n    public int getIndicatorBorderUncheckedColor() {\n        return indicatorBorderUncheckedColor;\n    }\n\n    public boolean isToolbarBehavior() {\n        return isToolbarBehavior;\n    }\n\n    public boolean isFabBehavior() {\n        return isFabBehavior;\n    }\n\n    public boolean isPickVideo() {\n        return isPickVideo;\n    }\n\n    public boolean isPickGif() {\n        return isPickGif;\n    }\n\n    public boolean isPickPicture() {\n        return isPickPicture;\n    }\n\n    @Nullable\n    public TakerConfig getTakerConfig() {\n        return takerConfig;\n    }\n\n    @Nullable\n    public CropperConfig getCropperConfig() {\n        return cropperConfig;\n    }\n\n    /**\n     * Used in package.\n     */\n    boolean isCameraSupport() {\n        return takerConfig != null;\n    }\n\n    /**\n     * Used in package.\n     */\n    boolean isCropSupport() {\n        return cropperConfig != null;\n    }\n\n    public Builder rebuild() {\n        return new Builder(this);\n    }\n\n    public static class Builder {\n\n        private PickerConfig mConfig;\n\n        private Builder() {\n            mConfig = new PickerConfig();\n        }\n\n        private Builder(@NonNull PickerConfig config) {\n            Preconditions.checkNotNull(config);\n            this.mConfig = config;\n        }\n\n        /**\n         * 设置相册可选的最大数量\n         *\n         * @param threshold 阈值\n         */\n        public Builder setThreshold(int threshold) {\n            mConfig.threshold = threshold;\n            return this;\n        }\n\n        /**\n         * 设置用户已经选中的图片, 相册会根据 Path 比较, 在相册中打钩\n         *\n         * @param pickedPictures 已选中的图片\n         */\n        public Builder setPickedPictures(@Nullable ArrayList<MediaMeta> pickedPictures) {\n            if (null != pickedPictures) {\n                mConfig.userPickedSet.addAll(pickedPictures);\n            }\n            return this;\n        }\n\n        public Builder setSpanCount(int count) {\n            mConfig.spanCount = count;\n            return this;\n        }\n\n        /**\n         * 设置 Toolbar 的背景色\n         */\n        public Builder setToolbarBackgroundColor(@ColorInt int color) {\n            mConfig.toolbarBkgColor = color;\n            return this;\n        }\n\n        /**\n         * 设置 Toolbar 的背景图片\n         *\n         * @param drawableRes drawable 资源 ID\n         */\n        public Builder setToolbarBackgroundDrawableRes(@DrawableRes int drawableRes) {\n            mConfig.toolbarBkgDrawableResId = drawableRes;\n            return this;\n        }\n\n        /**\n         * 设置图片选择器的背景色\n         */\n        public Builder setPickerBackgroundColor(@ColorInt int color) {\n            mConfig.pickerBackgroundColor = color;\n            return this;\n        }\n\n        /**\n         * 设置图片选择器的背景色\n         */\n        public Builder setPickerItemBackgroundColor(@ColorInt int color) {\n            mConfig.pickerItemBackgroundColor = color;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param textColor 边框的颜色\n         */\n        public Builder setIndicatorTextColor(@ColorInt int textColor) {\n            mConfig.indicatorTextColor = textColor;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param solidColor 边框的颜色\n         */\n        public Builder setIndicatorSolidColor(@ColorInt int solidColor) {\n            mConfig.indicatorSolidColor = solidColor;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param checkedColor   选中的边框颜色的 Res Id\n         * @param uncheckedColor 未选中的边框颜色的Res Id\n         */\n        public Builder setIndicatorBorderColor(@ColorInt int checkedColor, @ColorInt int uncheckedColor) {\n            mConfig.indicatorBorderCheckedColor = checkedColor;\n            mConfig.indicatorBorderUncheckedColor = uncheckedColor;\n            return this;\n        }\n\n        /**\n         * 是否设置 Toolbar Behavior 动画\n         */\n        public Builder isToolbarScrollable(boolean isToolbarScrollable) {\n            mConfig.isToolbarBehavior = isToolbarScrollable;\n            return this;\n        }\n\n        /**\n         * 是否设置 Fab Behavior 滚动动画\n         */\n        public Builder isFabScrollable(boolean isFabScrollable) {\n            mConfig.isFabBehavior = isFabScrollable;\n            return this;\n        }\n\n        /**\n         * 是否支持选取视频\n         *\n         * @param isPickVideo if true is support.\n         */\n        public Builder isPickVideo(boolean isPickVideo) {\n            mConfig.isPickVideo = isPickVideo;\n            return this;\n        }\n\n        /**\n         * 是否支持选取视频\n         *\n         * @param isPickPicture if true is support.\n         */\n        public Builder isPickPicture(boolean isPickPicture) {\n            mConfig.isPickPicture = isPickPicture;\n            return this;\n        }\n\n\n        /**\n         * 是否支持选取 GIF 图\n         *\n         * @param isPickGif if true is support.\n         */\n        public Builder isPickGif(boolean isPickGif) {\n            mConfig.isPickGif = isPickGif;\n            return this;\n        }\n\n        /**\n         * 裁剪项的配置\n         *\n         * @param cropperConfig if null is deny crop, if not null is granted.\n         */\n        public Builder setCropConfig(@Nullable CropperConfig cropperConfig) {\n            mConfig.cropperConfig = cropperConfig;\n            return this;\n        }\n\n        /**\n         * 拍摄项的配置\n         *\n         * @param takerConfig if null is deny taker, if not null is granted.\n         */\n        public Builder setCameraConfig(@Nullable TakerConfig takerConfig) {\n            mConfig.takerConfig = takerConfig;\n            return this;\n        }\n\n        public PickerConfig build() {\n            if (mConfig.threshold > 0 && mConfig.userPickedSet == null) {\n                mConfig.userPickedSet = new ArrayList<>(mConfig.threshold);\n            }\n            return mConfig;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerContract.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.view.View;\n\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.ArrayList;\n\n/**\n * PicturePicture MVP 的约束\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/6/13.\n */\ninterface PickerContract {\n\n    interface IView {\n\n        void setToolbarScrollable(boolean isScrollable);\n\n        void setToolbarBackgroundColor(int color);\n\n        void setToolbarBackgroundDrawable(@DrawableRes int drawableId);\n\n        void setFabColor(int color);\n\n        void setFabVisible(boolean isVisible);\n\n        void setBackgroundColor(int color);\n\n        void setSpanCount(int spanCount);\n\n        void setPickerAdapter(@NonNull PickerConfig config, @NonNull ArrayList<MediaMeta> metas,\n                              @NonNull ArrayList<MediaMeta> userPickedMetas);\n\n        void setFolderAdapter(@NonNull ArrayList<FolderModel> allFolders);\n\n        void setPictureFolderText(@NonNull String folderName);\n\n        void setToolbarEnsureText(@NonNull CharSequence content);\n\n        void setPreviewText(@NonNull CharSequence content);\n\n        void notifyDisplaySetItemChanged(int changedIndex);\n\n        void notifyDisplaySetChanged();\n\n        void notifyFolderDataSetChanged();\n\n        void notifyNewMetaInsertToFirst();\n\n        void showMsg(@NonNull String msg);\n\n        String getString(@StringRes int resId);\n\n        void setProgressBarVisible(boolean visible);\n\n        void setResultAndFinish(@NonNull ArrayList<MediaMeta> pickedPaths);\n    }\n\n    interface IPresenter {\n\n        boolean handlePictureChecked(@Nullable MediaMeta checkedMeta);\n\n        void handlePictureUnchecked(@Nullable MediaMeta removedMeta);\n\n        void handlePickedSetChanged(MediaMeta mediaMeta);\n\n        void handleCameraClicked();\n\n        void handlePictureClicked(int position, @Nullable View sharedElement);\n\n        void handleFolderChecked(int position);\n\n        void handlePreviewClicked();\n\n        void handleEnsureClicked();\n\n        void handleRecycleViewDraw(RecyclerView parent);\n\n        void handleViewDestroy();\n    }\n\n    interface IModel {\n\n        interface Callback {\n\n            void onFetched(@NonNull ArrayList<FolderModel> folderModels);\n\n        }\n\n        void fetchData(Context context, boolean pickPicture, boolean supportGif, boolean supportVideo, final Callback listener);\n\n        void stopIfFetching();\n\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerManager.java",
    "content": "package com.sharry.lib.album;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\n\nimport static android.app.Activity.RESULT_OK;\n\n/**\n * 图片选择器的管理类\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:03 PM\n */\npublic class PickerManager {\n\n    public static final String TAG = PickerManager.class.getSimpleName();\n    private static String[] sRequirePermissions = {\n            Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            Manifest.permission.READ_EXTERNAL_STORAGE\n    };\n\n    public static PickerManager with(@NonNull Context context) {\n        if (context instanceof Activity) {\n            Activity activity = (Activity) context;\n            return new PickerManager(activity);\n        } else {\n            throw new IllegalArgumentException(\"PickerManager.with -> Context can not cast to Activity\");\n        }\n    }\n\n    private Activity mActivity;\n    private PickerConfig mConfig;\n\n    private PickerManager(Activity activity) {\n        this.mActivity = activity;\n    }\n\n    /**\n     * 设置图片加载方案\n     */\n    public PickerManager setLoaderEngine(@NonNull ILoaderEngine loader) {\n        Preconditions.checkNotNull(loader, \"Please ensure ILoaderEngine not null!\");\n        Loader.setLoaderEngine(loader);\n        return this;\n    }\n\n    /**\n     * 设置图片选择的配置\n     */\n    public PickerManager setPickerConfig(@NonNull PickerConfig config) {\n        this.mConfig = Preconditions.checkNotNull(config, \"Please ensure PickerConfig not null!\");\n        return this;\n    }\n\n    /**\n     * 发起请求\n     *\n     * @param callbackLambda 图片选中的回调\n     */\n    public void start(@NonNull final PickerCallbackLambda callbackLambda) {\n        Preconditions.checkNotNull(callbackLambda, \"Please ensure PickerCallback not null!\");\n        start(new PickerCallback() {\n            @Override\n            public void onPickedComplete(@NonNull ArrayList<MediaMeta> userPickedSet) {\n                callbackLambda.onPicked(userPickedSet);\n            }\n\n            @Override\n            public void onPickedFailed() {\n                callbackLambda.onPicked(null);\n            }\n        });\n    }\n\n    /**\n     * 发起请求\n     *\n     * @param pickerCallback 图片选中的回调\n     */\n    public void start(@NonNull final PickerCallback pickerCallback) {\n        Preconditions.checkNotNull(pickerCallback, \"Please ensure PickerCallback not null!\");\n        Preconditions.checkNotNull(mConfig, \"Please ensure U set PickerConfig correct!\");\n        PermissionsHelper.with(mActivity)\n                .request(sRequirePermissions)\n                .execute(new PermissionsCallback() {\n                    @Override\n                    public void onResult(boolean granted) {\n                        if (granted) {\n                            startActual(pickerCallback);\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 处理 PickerActivity 的启动\n     */\n    private void startActual(@NonNull final PickerCallback pickerCallback) {\n        // 1. 若开启了裁剪, 则只能选中一张图片\n        if (mConfig.isCropSupport()) {\n            mConfig.rebuild()\n                    .setThreshold(1)\n                    .setPickedPictures(null)\n                    .build();\n        }\n        // 2. 获取回调的 Fragment\n        CallbackFragment callbackFragment = CallbackFragment.getInstance(mActivity);\n        if (callbackFragment == null) {\n            pickerCallback.onPickedFailed();\n            return;\n        }\n        callbackFragment.setCallback(new CallbackFragment.Callback() {\n            @Override\n            public void onActivityResult(int requestCode, int resultCode, Intent data) {\n                ArrayList<MediaMeta> metas;\n                if (resultCode == RESULT_OK && requestCode == PickerActivity.REQUEST_CODE && null != data\n                        && (metas = data.getParcelableArrayListExtra(PickerActivity.RESULT_EXTRA_PICKED_PICTURES)) != null) {\n                    pickerCallback.onPickedComplete(metas);\n                } else {\n                    pickerCallback.onPickedFailed();\n                }\n            }\n        });\n        PickerActivity.launchActivityForResult(mActivity, callbackFragment, mConfig);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerModel.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.sharry.lib.album.Constants.MIME_TYPE_3GP;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_AIV;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_FLV;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_GIF;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_JPEG;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_MKV;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_MOV;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_MP4;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_MPG;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_PNG;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_RMVB;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_VOB;\nimport static com.sharry.lib.album.Constants.MIME_TYPE_WEBP;\nimport static com.sharry.lib.album.FileUtil.getLastFileName;\nimport static com.sharry.lib.album.FileUtil.getParentFolderPath;\n\n/**\n * MVP frame model associated with PicturePicker.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.2\n * @since 2018/8/30 20:00\n */\nclass PickerModel implements PickerContract.IModel {\n\n    private static final String TAG = PickerModel.class.getSimpleName();\n    private static final ThreadPoolExecutor FETCH_EXECUTOR;\n\n    static {\n        FETCH_EXECUTOR = new ThreadPoolExecutor(\n                // 4 个线程并发即可满足需求\n                // 使用 5 个, 是为了预防因其中一个线程意外阻塞而导致任务无法正常执行的问题\n                5, 5,\n                // 60 s 后自动销毁\n                60, TimeUnit.SECONDS,\n                new LinkedBlockingQueue<Runnable>(),\n                new ThreadFactory() {\n                    @Override\n                    public Thread newThread(@NonNull Runnable r) {\n                        Thread thread = new Thread(r, PickerModel.class.getSimpleName());\n                        thread.setDaemon(false);\n                        return thread;\n                    }\n                }\n        );\n        // 允许核心线程销毁, 相册为低频组件, 无需持有核心线程, 防止占用过多系统资源\n        FETCH_EXECUTOR.allowCoreThreadTimeOut(true);\n    }\n\n    private Future mFetchDataFuture;\n    private Future mFetchPictureFuture;\n    private Future mFetchGifFuture;\n    private Future mFetchVideoFuture;\n\n    PickerModel() {\n    }\n\n    @Override\n    public void fetchData(final Context context, final boolean supportPicture, final boolean supportGif,\n                          final boolean supportVideo, final Callback callback) {\n        mFetchDataFuture = FETCH_EXECUTOR.submit(new Runnable() {\n\n            @Override\n            public void run() {\n                // 用于存储遍历到的所有图片文件夹集合\n                ArrayList<FolderModel> folderModels = new ArrayList<>();\n                // 创建一个图片文件夹, 用于保存所有图片\n                FolderModel folderAll = new FolderModel(\n                        context.getString(R.string.lib_album_picker_all_picture)\n                );\n                folderModels.add(folderAll);\n                /*\n                   key 为图片所在文件夹的绝对路径\n                   values 为 FolderModel 的对象\n                 */\n                ConcurrentHashMap<String, FolderModel> folders = new ConcurrentHashMap<>(16);\n                // 等待执行结束\n                try {\n                    // 创建计数器\n                    int count = 0;\n                    if (supportPicture) count++;\n                    if (supportGif) count++;\n                    if (supportVideo) count++;\n                    CountDownLatch latch = new CountDownLatch(count);\n                    // 获取图片数据\n                    if (supportPicture) {\n                        mFetchPictureFuture = FETCH_EXECUTOR.submit(new PictureFetchRunnable(context, folders, folderAll, latch));\n                    }\n                    // 获取 GIF 数据\n                    if (supportGif) {\n                        mFetchGifFuture = FETCH_EXECUTOR.submit(new GifFetchRunnable(context, folders, folderAll, latch));\n                    }\n                    // 获取视频数据\n                    if (supportVideo) {\n                        mFetchVideoFuture = FETCH_EXECUTOR.submit(new VideoFetchRunnable(context, folders, folderAll, latch));\n                    }\n                    latch.await();\n                } catch (InterruptedException e) {\n                    // ignore.\n                } finally {\n                    // 注入数据\n                    folderModels.addAll(folders.values());\n                    // 回调完成\n                    callback.onFetched(folderModels);\n                }\n            }\n\n        });\n    }\n\n    @Override\n    public void stopIfFetching() {\n        if (mFetchPictureFuture != null) {\n            mFetchPictureFuture.cancel(true);\n        }\n        if (mFetchGifFuture != null) {\n            mFetchGifFuture.cancel(true);\n        }\n        if (mFetchVideoFuture != null) {\n            mFetchVideoFuture.cancel(true);\n        }\n        if (mFetchDataFuture != null) {\n            mFetchDataFuture.cancel(true);\n        }\n    }\n\n    /**\n     * The runnable for fetch picture resources.\n     */\n    private static class PictureFetchRunnable implements Runnable {\n\n        private final Context context;\n        private final ConcurrentHashMap<String, FolderModel> folders;\n        private final FolderModel folderAll;\n        private final CountDownLatch latch;\n\n        PictureFetchRunnable(Context context,\n                             ConcurrentHashMap<String, FolderModel> folders,\n                             FolderModel folderAll,\n                             CountDownLatch latch) {\n            this.context = context;\n            this.folderAll = folderAll;\n            this.folders = folders;\n            this.latch = latch;\n        }\n\n        @Override\n        public void run() {\n            Cursor cursor = createPictureCursor();\n            try {\n                while (cursor.moveToNext()) {\n                    // 验证路径是否有效\n                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));\n                    if (TextUtils.isEmpty(path)) {\n                        continue;\n                    }\n                    // 构建数据源\n                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));\n                    MediaMeta meta = MediaMeta.create(\n                            Uri.withAppendedPath(\n                                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,\n                                    String.valueOf(id)),\n                            path,\n                            true\n                    );\n                    meta.date = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));\n                    meta.mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));\n\n                    // 1. 添加到 <所有> 目录下\n                    folderAll.addMeta(meta);\n                    // 2. 添加到文件所在目录\n                    String folderPath = getParentFolderPath(path);\n                    if (TextUtils.isEmpty(folderPath)) {\n                        continue;\n                    }\n                    // 添加资源到缓存\n                    FolderModel folder = folders.get(folderPath);\n                    if (folder == null) {\n                        String folderName = getLastFileName(folderPath);\n                        if (TextUtils.isEmpty(folderName)) {\n                            folderName = context.getString(R.string.lib_album_picker_root_folder);\n                        }\n                        folder = new FolderModel(folderName);\n                        folders.put(folderPath, folder);\n                    }\n                    folder.addMeta(meta);\n                }\n                Log.i(TAG, \"Fetch picture resource completed.\");\n            } catch (Throwable throwable) {\n                // ignore.\n            } finally {\n                if (cursor != null) {\n                    cursor.close();\n                }\n                latch.countDown();\n            }\n        }\n\n        /**\n         * Create image cursor associated with this runnable.\n         */\n        private Cursor createPictureCursorWithGif() {\n            Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n            String[] projection = new String[]{\n                    MediaStore.Video.Media._ID,\n                    MediaStore.Images.Media.DATA,\n                    MediaStore.Images.Media.DATE_ADDED,\n                    MediaStore.Video.Media.MIME_TYPE\n            };\n            String selection = MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Images.Media.MIME_TYPE + \"=?\";\n            String[] selectionArgs = new String[]{\n                    MIME_TYPE_JPEG,\n                    MIME_TYPE_PNG,\n                    MIME_TYPE_WEBP,\n                    MIME_TYPE_GIF\n            };\n            String sortOrder = MediaStore.Images.Media.DATE_ADDED + \" DESC\";\n            return context.getContentResolver().query(uri, projection,\n                    selection, selectionArgs, sortOrder);\n        }\n\n        /**\n         * Create image cursor associated with this runnable.\n         */\n        private Cursor createPictureCursor() {\n            Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n            String[] projection = new String[]{\n                    MediaStore.Video.Media._ID,\n                    MediaStore.Images.Media.DATA,\n                    MediaStore.Images.Media.DATE_ADDED,\n                    MediaStore.Video.Media.MIME_TYPE\n            };\n            String selection = MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Images.Media.MIME_TYPE + \"=?\";\n            String[] selectionArgs = new String[]{\n                    MIME_TYPE_JPEG,\n                    MIME_TYPE_PNG,\n                    MIME_TYPE_WEBP\n            };\n            String sortOrder = MediaStore.Images.Media.DATE_ADDED + \" DESC\";\n            return context.getContentResolver().query(uri, projection,\n                    selection, selectionArgs, sortOrder);\n        }\n\n    }\n\n    /**\n     * The runnable for fetch gif resources.\n     */\n    private static class GifFetchRunnable implements Runnable {\n\n        private final Context context;\n        private final ConcurrentHashMap<String, FolderModel> folders;\n        private final FolderModel folderAll;\n        private final CountDownLatch latch;\n\n        GifFetchRunnable(Context context,\n                         ConcurrentHashMap<String, FolderModel> folders,\n                         FolderModel folderAll,\n                         CountDownLatch latch) {\n            this.context = context;\n            this.folderAll = folderAll;\n            this.folders = folders;\n            this.latch = latch;\n        }\n\n        @Override\n        public void run() {\n            Cursor cursor = createGifCursor();\n            try {\n                while (cursor.moveToNext()) {\n                    // 验证路径是否有效\n                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));\n                    if (TextUtils.isEmpty(path)) {\n                        continue;\n                    }\n                    // 构建数据源\n                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));\n                    MediaMeta meta = MediaMeta.create(\n                            Uri.withAppendedPath(\n                                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,\n                                    String.valueOf(id)),\n                            path,\n                            true\n                    );\n                    meta.date = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));\n                    meta.mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));\n                    // 1. 添加到 <所有> 目录下\n                    folderAll.addMeta(meta);\n                    // 2. 添加到文件所在目录\n                    String folderPath = getParentFolderPath(path);\n                    if (TextUtils.isEmpty(folderPath)) {\n                        continue;\n                    }\n                    // 添加资源到缓存\n                    FolderModel folder = folders.get(folderPath);\n                    if (folder == null) {\n                        String folderName = getLastFileName(folderPath);\n                        if (TextUtils.isEmpty(folderName)) {\n                            folderName = context.getString(R.string.lib_album_picker_root_folder);\n                        }\n                        folder = new FolderModel(folderName);\n                        folders.put(folderPath, folder);\n                    }\n                    folder.addMeta(meta);\n                }\n                Log.i(TAG, \"Fetch picture resource completed.\");\n            } catch (Throwable throwable) {\n                // ignore.\n            } finally {\n                if (cursor != null) {\n                    cursor.close();\n                }\n                latch.countDown();\n            }\n        }\n\n        /**\n         * Create image cursor associated with this runnable.\n         */\n        private Cursor createGifCursor() {\n            Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n            String[] projection = new String[]{\n                    MediaStore.Video.Media._ID,\n                    MediaStore.Images.Media.DATA,\n                    MediaStore.Images.Media.DATE_ADDED,\n                    MediaStore.Video.Media.MIME_TYPE\n            };\n            String selection = MediaStore.Images.Media.MIME_TYPE + \"=?\";\n            String[] selectionArgs = new String[]{\n                    MIME_TYPE_GIF\n            };\n            String sortOrder = MediaStore.Images.Media.DATE_ADDED + \" DESC\";\n            return context.getContentResolver().query(uri, projection,\n                    selection, selectionArgs, sortOrder);\n        }\n\n    }\n\n    /**\n     * The runnable for fetch video resources.\n     */\n    private static class VideoFetchRunnable implements Runnable {\n\n        private final Context context;\n        private final ConcurrentHashMap<String, FolderModel> folders;\n        private final FolderModel folderAll;\n        private final CountDownLatch latch;\n\n        VideoFetchRunnable(Context context,\n                           ConcurrentHashMap<String, FolderModel> folders,\n                           FolderModel folderAll,\n                           CountDownLatch latch) {\n            this.context = context;\n            this.folderAll = folderAll;\n            this.folders = folders;\n            this.latch = latch;\n        }\n\n        @Override\n        public void run() {\n            Cursor cursor = createVideoCursor();\n            try {\n                while (cursor.moveToNext()) {\n                    // 验证路径是否有效\n                    String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));\n                    if (TextUtils.isEmpty(path)) {\n                        continue;\n                    }\n                    long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));\n                    MediaMeta meta = MediaMeta.create(\n                            Uri.withAppendedPath(\n                                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,\n                                    String.valueOf(id)\n                            ),\n                            path,\n                            false\n                    );\n                    meta.duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));\n                    meta.date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED));\n                    meta.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE));\n                    meta.mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));\n                    // 获取缩略图\n                    meta.thumbnailPath = fetchVideoThumbNail(id, path, meta.date);\n                    // 添加到 <所有> 目录下\n                    folderAll.addMeta(meta);\n                    // 获取资源所在文件夹\n                    String folderPath = getParentFolderPath(path);\n                    if (TextUtils.isEmpty(folderPath)) {\n                        continue;\n                    }\n                    // 添加资源到缓存\n                    FolderModel folder = folders.get(folderPath);\n                    if (folder == null) {\n                        String folderName = getLastFileName(folderPath);\n                        if (TextUtils.isEmpty(folderName)) {\n                            folderName = context.getString(R.string.lib_album_picker_root_folder);\n                        }\n                        folder = new FolderModel(folderName);\n                        folders.put(folderPath, folder);\n                    }\n                    folder.addMeta(meta);\n                }\n                Log.i(TAG, \"Fetch video resource completed.\");\n            } catch (Throwable throwable) {\n                // ignore.\n            } finally {\n                if (cursor != null) {\n                    cursor.close();\n                }\n                latch.countDown();\n            }\n        }\n\n        /**\n         * Create video cursor associated with this runnable.\n         */\n        private Cursor createVideoCursor() {\n            Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;\n            String[] projection = new String[]{\n                    MediaStore.Video.Media._ID,\n                    MediaStore.Video.Media.DATA,\n                    MediaStore.Video.Media.DURATION,\n                    MediaStore.Video.Media.DATE_ADDED,\n                    MediaStore.Video.Media.SIZE,\n                    MediaStore.Video.Media.MIME_TYPE\n            };\n            String selection = MediaStore.Images.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=? or \" +\n                    MediaStore.Video.Media.MIME_TYPE + \"=?\";\n            String[] selectionArgs = new String[]{\n                    MIME_TYPE_MP4,\n                    MIME_TYPE_3GP,\n                    MIME_TYPE_AIV,\n                    MIME_TYPE_RMVB,\n                    MIME_TYPE_VOB,\n                    MIME_TYPE_FLV,\n                    MIME_TYPE_MKV,\n                    MIME_TYPE_MOV,\n                    MIME_TYPE_MPG,\n            };\n            String sortOrder = MediaStore.Images.Media.DATE_ADDED + \" DESC\";\n            return context.getContentResolver().query(uri, projection,\n                    selection, selectionArgs, sortOrder);\n        }\n\n        /**\n         * 获取视频缩略图地址\n         */\n        @Nullable\n        private String fetchVideoThumbNail(long id, String path, long date) {\n            String thumbNailPath = null;\n            Cursor cursor = createThumbnailCursor(id);\n            if (cursor != null) {\n                while (cursor.moveToNext()) {\n                    thumbNailPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Thumbnails.DATA));\n                }\n                cursor.close();\n            }\n            return thumbNailPath;\n        }\n\n        private Cursor createThumbnailCursor(long id) {\n            Uri uri = MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI;\n            String[] projection = new String[]{\n                    MediaStore.Video.Thumbnails.DATA,\n                    MediaStore.Video.Thumbnails.VIDEO_ID\n            };\n            String selection = MediaStore.Video.Thumbnails.VIDEO_ID + \"=?\";\n            String[] selectionArgs = new String[]{String.valueOf(id)};\n            return context.getContentResolver().query(uri, projection, selection,\n                    selectionArgs, null);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/PickerPresenter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.text.MessageFormat;\nimport java.util.ArrayList;\n\n\n/**\n * MVP frame presenter associated with PicturePicker.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.3\n * @since 2018/9/1 10:17\n */\nclass PickerPresenter implements PickerContract.IPresenter,\n        TakerCallbackLambda,\n        CropperCallbackLambda {\n\n    /**\n     * View associated with this presenter.\n     */\n    private final PickerContract.IView mView;\n\n    /**\n     * Model associated with this presenter.\n     */\n    private final PickerContract.IModel mModel;\n\n    /**\n     * Config associated with the PicturePicker.\n     */\n    private final PickerConfig mPickerConfig;\n\n    /**\n     * Config associated with the PictureWatcher.\n     */\n    private final WatcherConfig mWatcherConfig;\n\n    /**\n     * Data Source.\n     */\n    private ArrayList<FolderModel> mFolderModels;\n    private final ArrayList<MediaMeta> mPickedSet;\n\n    /**\n     * Current checked set.\n     */\n    private final ArrayList<MediaMeta> mDisplaySet = new ArrayList<>();\n    private FolderModel mCheckedFolder;\n\n    PickerPresenter(@NonNull PickerContract.IView view, @NonNull PickerConfig config) {\n        this.mView = view;\n        this.mPickerConfig = config;\n        this.mPickedSet = mPickerConfig.getUserPickedSet();\n        this.mWatcherConfig = WatcherConfig.Builder()\n                .setThreshold(mPickerConfig.getThreshold())\n                .setIndicatorTextColor(mPickerConfig.getIndicatorTextColor())\n                .setIndicatorSolidColor(mPickerConfig.getIndicatorSolidColor())\n                .setIndicatorBorderColor(\n                        mPickerConfig.getIndicatorBorderCheckedColor(),\n                        mPickerConfig.getIndicatorBorderUncheckedColor()\n                )\n                .setUserPickedSet(mPickedSet)\n                .build();\n        this.mModel = new PickerModel();\n        setupView();\n        fetchData((Context) mView);\n    }\n\n    //////////////////////////////////////////////PickerContract.IPresenter/////////////////////////////////////////////////\n\n    @Override\n    public boolean handlePictureChecked(MediaMeta checkedMeta) {\n        boolean result = isCanPickedPicture(true);\n        if (result && mPickedSet.add(checkedMeta)) {\n            mView.setToolbarEnsureText(buildEnsureText());\n            mView.setPreviewText(buildPreviewText());\n        }\n        return result;\n    }\n\n    @Override\n    public void handlePictureUnchecked(MediaMeta removedMeta) {\n        if (mPickedSet.remove(removedMeta)) {\n            mView.setToolbarEnsureText(buildEnsureText());\n            mView.setPreviewText(buildPreviewText());\n        }\n    }\n\n    @Override\n    public void handlePickedSetChanged(MediaMeta mediaMeta) {\n        if (mediaMeta == null) {\n            return;\n        }\n        int changedPos = mDisplaySet.indexOf(mediaMeta);\n        if (changedPos != -1) {\n            mView.setToolbarEnsureText(buildEnsureText());\n            mView.setPreviewText(buildPreviewText());\n            mView.notifyDisplaySetItemChanged(mPickerConfig.isCameraSupport() ?\n                    changedPos + 1 : changedPos);\n        }\n    }\n\n    @Override\n    public void handleCameraClicked() {\n        if (mPickerConfig.getTakerConfig() != null) {\n            TakerManager.with((Context) mView)\n                    .setConfig(\n                            mPickerConfig.getTakerConfig().rebuild()\n                                    // 取消相机拍摄后的裁剪动作, 由 Picker ensure 时触发\n                                    .setVideoRecord(mPickerConfig.isPickVideo())\n                                    .build()\n                    )\n                    .take(this);\n        }\n    }\n\n    @Override\n    public void handlePictureClicked(int position, View sharedElement) {\n        WatcherManager.with((Context) mView)\n                .setSharedElement(sharedElement)\n                .setLoaderEngine(Loader.getPictureLoader())\n                .setConfig(\n                        mWatcherConfig.rebuild()\n                                .setDisplayDataSet(mDisplaySet, position)\n                                .build()\n                )\n                .start();\n    }\n\n    @Override\n    public void handlePreviewClicked() {\n        if (!isCanPreview()) {\n            return;\n        }\n        WatcherManager.with((Context) mView)\n                .setLoaderEngine(Loader.getPictureLoader())\n                .setConfig(\n                        mWatcherConfig.rebuild()\n                                .setDisplayDataSet(mPickedSet, 0)\n                                .build()\n                )\n                .start();\n    }\n\n    @Override\n    public void handleFolderChecked(int position) {\n        performFolderChecked(position);\n    }\n\n    @Override\n    public void handleEnsureClicked() {\n        if (!isCanEnsure()) {\n            return;\n        }\n        // 不需要裁剪, 直接返回\n        if (mPickerConfig.isCropSupport() && mPickedSet.get(0).isPicture()) {\n            // 启动裁剪\n            assert mPickerConfig.getCropperConfig() != null;\n            CropperManager.with((Context) mView)\n                    .setConfig(\n                            mPickerConfig.getCropperConfig().rebuild()\n                                    .setOriginUri(mPickedSet.get(0).contentUri)\n                                    .build()\n                    )\n                    .crop(this);\n        } else {\n            mView.setResultAndFinish(mPickedSet);\n        }\n    }\n\n    @Override\n    public void handleRecycleViewDraw(RecyclerView parent) {\n        // Cache view bounds.\n        SharedElementHelper.CACHES.clear();\n        for (int i = 0; i < parent.getChildCount(); i++) {\n            View child = parent.getChildAt(i);\n            int adapterPosition = parent.getChildAdapterPosition(child) +\n                    (mPickerConfig.isCameraSupport() ? -1 : 0);\n            SharedElementHelper.CACHES.put(adapterPosition, SharedElementHelper.Bounds.parseFrom(\n                    child, adapterPosition));\n        }\n    }\n\n    @Override\n    public void handleViewDestroy() {\n        // 终止 mModel 获取数据\n        mModel.stopIfFetching();\n        // 清空共享元素缓存的数据\n        SharedElementHelper.CACHES.clear();\n    }\n\n    //////////////////////////////////////////////TakerCallback/////////////////////////////////////////////////\n\n    @Override\n    public void onCameraTake(@Nullable MediaMeta newMeta) {\n        if (newMeta == null) {\n            return;\n        }\n        // 1. 添加到 <当前展示> 的文件夹下\n        mCheckedFolder.addMeta(newMeta);\n        // 2. 添加到 <所有文件> 的文件夹下\n        FolderModel folderAll = mFolderModels.get(0);\n        if (folderAll != mCheckedFolder) {\n            folderAll.addMeta(newMeta);\n        }\n        // 3. 更新展示的集合\n        mDisplaySet.add(0, newMeta);\n        // 3.1 判断是否可以继续选择\n        if (isCanPickedPicture(false)) {\n            mPickedSet.add(newMeta);\n            mView.setToolbarEnsureText(buildEnsureText());\n            mView.setPreviewText(buildPreviewText());\n        }\n        // 3.2 通知 UI 更新视图\n        mView.notifyNewMetaInsertToFirst();\n        mView.notifyFolderDataSetChanged();\n    }\n\n    ////////////////////////////////////////////// CropperCallbackLambda /////////////////////////////////////////////////\n\n    @Override\n    public void onCropped(@Nullable MediaMeta mediaMeta) {\n        if (mediaMeta == null) {\n            return;\n        }\n        mPickedSet.clear();\n        mPickedSet.add(mediaMeta);\n        mView.setResultAndFinish(mPickedSet);\n    }\n\n    private void setupView() {\n        // 配置 UI 视图\n        mView.setToolbarScrollable(mPickerConfig.isToolbarBehavior());\n        mView.setFabVisible(mPickerConfig.isFabBehavior());\n        if (mPickerConfig.getToolbarBkgColor() != PickerConfig.INVALIDATE_VALUE) {\n            mView.setToolbarBackgroundColor(mPickerConfig.getToolbarBkgColor());\n            mView.setFabColor(mPickerConfig.getToolbarBkgColor());\n        }\n        if (mPickerConfig.getToolbarBkgDrawableResId() != PickerConfig.INVALIDATE_VALUE) {\n            mView.setToolbarBackgroundDrawable(mPickerConfig.getToolbarBkgDrawableResId());\n        }\n        if (mPickerConfig.getPickerBackgroundColor() != PickerConfig.INVALIDATE_VALUE) {\n            mView.setBackgroundColor(mPickerConfig.getPickerBackgroundColor());\n        }\n        // 设置图片的列数\n        mView.setSpanCount(mPickerConfig.getSpanCount());\n        // 设置 RecyclerView 的 Adapter\n        mView.setPickerAdapter(mPickerConfig, mDisplaySet, mPickedSet);\n    }\n\n    private void fetchData(Context context) {\n        mView.setProgressBarVisible(true);\n        mModel.fetchData(\n                context.getApplicationContext(),\n                mPickerConfig.isPickPicture(),\n                mPickerConfig.isPickGif(),\n                mPickerConfig.isPickVideo(),\n                new PickerContract.IModel.Callback() {\n\n                    private final Handler mainHandler = new Handler(Looper.getMainLooper());\n\n                    @Override\n                    public void onFetched(@NonNull final ArrayList<FolderModel> folderModels) {\n                        mFolderModels = folderModels;\n                        mainHandler.post(new Runnable() {\n                            @Override\n                            public void run() {\n                                mView.setProgressBarVisible(false);\n                                mView.setFolderAdapter(mFolderModels);\n                                performFolderChecked(0);\n                            }\n                        });\n                    }\n\n                }\n        );\n    }\n\n    /**\n     * 执行展示文件夹的操作\n     */\n    private void performFolderChecked(int position) {\n        // Upgrade checked folder.\n        mCheckedFolder = mFolderModels.get(position);\n        mDisplaySet.clear();\n        mDisplaySet.addAll(mCheckedFolder.getMetas());\n        // Notify view displays paths changed.\n        mView.notifyDisplaySetChanged();\n        // Set folder text associated with view.\n        mView.setPictureFolderText(mCheckedFolder.getName());\n        // Set ensure text associated with view toolbar.\n        mView.setToolbarEnsureText(buildEnsureText());\n        // Set preview text associated with view.\n        mView.setPreviewText(buildPreviewText());\n    }\n\n    /**\n     * 是否可以继续选择图片\n     *\n     * @param isShowFailedMsg 是否提示失败原因\n     * @return true is can picked, false is cannot picked.\n     */\n    private boolean isCanPickedPicture(boolean isShowFailedMsg) {\n        if (mPickedSet.size() == mPickerConfig.getThreshold()) {\n            if (isShowFailedMsg) {\n                mView.showMsg(mView.getString(R.string.lib_album_picker_tips_over_threshold_prefix)\n                        + mPickerConfig.getThreshold()\n                        + mView.getString(R.string.lib_album_picker_tips_over_threshold_suffix)\n                );\n            }\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * 是否可以启动图片预览\n     *\n     * @return true is can launch, false is cannot launch.\n     */\n    private boolean isCanPreview() {\n        if (mPickedSet.isEmpty()) {\n            mView.showMsg(mView.getString(R.string.lib_album_picker_tips_preview_failed));\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * 是否可以发起确认请求\n     *\n     * @return true is can ensure, false is cannot ensure.\n     */\n    private boolean isCanEnsure() {\n        if (mPickedSet.isEmpty()) {\n            mView.showMsg(mView.getString(R.string.lib_album_picker_tips_ensure_failed));\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * 构建标题确认文本\n     */\n    private CharSequence buildEnsureText() {\n        return MessageFormat.format(\n                \"{0} ({1}/{2})\",\n                mView.getString(R.string.lib_album_picker_ensure),\n                mPickedSet.size(),\n                mPickerConfig.getThreshold()\n        );\n    }\n\n    /**\n     * 构建预览文本\n     */\n    private CharSequence buildPreviewText() {\n        return MessageFormat.format(\n                \"{0} ({1})\",\n                mView.getString(R.string.lib_album_picker_preview),\n                mPickedSet.size()\n        );\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_bottom_indicator.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_camera_header.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"50dp\"\n    android:height=\"50dp\"\n    android:viewportHeight=\"1024\"\n    android:viewportWidth=\"1024\">\n    <path\n        android:fillColor=\"#888888\"\n        android:pathData=\"M908.98,224.73L743.75,224.73v-14.5c0,-64.14 -52.2,-116.34 -116.35,-116.34L396.57,93.89c-64.14,0 -116.32,52.19 -116.32,116.34v14.5L114.98,224.73C51.58,224.73 0,276.25 0,339.57v475.7c0,63.31 51.58,114.83 114.98,114.83h794c63.43,0 115.02,-51.52 115.02,-114.83L1024,339.57c0,-63.32 -51.58,-114.84 -115.02,-114.84zM963.32,815.27c0,29.86 -24.38,54.16 -54.34,54.16h-794c-29.95,0 -54.3,-24.3 -54.3,-54.16L60.68,339.57c0,-29.86 24.35,-54.16 54.3,-54.16h195.61a30.32,30.32 0,0 0,30.34 -30.34v-44.84a55.7,55.7 0,0 1,55.64 -55.66h230.83c30.7,0 55.67,24.96 55.67,55.66v44.84a30.32,30.32 0,0 0,30.34 30.34h195.57c29.95,0 54.34,24.3 54.34,54.16v475.7z\" />\n    <path\n        android:fillColor=\"#888888\"\n        android:pathData=\"M512,350.62c-106.84,0 -193.74,86.9 -193.74,193.7 0,106.82 86.9,193.71 193.74,193.71 106.83,0 193.74,-86.9 193.74,-193.71 0,-106.81 -86.9,-193.7 -193.74,-193.7zM512,677.35c-73.36,0 -133.06,-59.67 -133.06,-133.03 0,-73.35 59.7,-133.02 133.06,-133.02s133.06,59.67 133.06,133.02c0,73.36 -59.7,133.03 -133.06,133.03z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_fab.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_gif.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:pathData=\"M944,299H692c-4.4,0 -8,3.6 -8,8v406c0,4.4 3.6,8 8,8h59.2c4.4,0 8,-3.6 8,-8V549.9h168.2c4.4,0 8,-3.6 8,-8V495c0,-4.4 -3.6,-8 -8,-8H759.2V364.2H944c4.4,0 8,-3.6 8,-8V307c0,-4.4 -3.6,-8 -8,-8zM588,300h-56c-4.4,0 -8,3.6 -8,8v406c0,4.4 3.6,8 8,8h56c4.4,0 8,-3.6 8,-8V308c0,-4.4 -3.6,-8 -8,-8zM452,500.9H290.5c-4.4,0 -8,3.6 -8,8v43.7c0,4.4 3.6,8 8,8h94.9l-0.3,8.9c-1.2,58.8 -45.6,98.5 -110.9,98.5 -76.2,0 -123.9,-59.7 -123.9,-156.7 0,-95.8 46.8,-155.2 121.5,-155.2 54.8,0 93.1,26.9 108.5,75.4h76.2c-13.6,-87.2 -86,-143.4 -184.7,-143.4C150,288 72,375.2 72,511.9 72,650.2 149.1,736 273,736c114.1,0 187,-70.7 187,-181.6v-45.5c0,-4.4 -3.6,-8 -8,-8z\"\n      android:fillColor=\"#ffffff\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_right_arrow.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#ffffffff\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_video_default.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <solid android:color=\"#ff000000\" />\n\n</shape>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/drawable/ic_album_picker_video_play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/layout/lib_album_activity_picker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <!--标题栏-->\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.sharry.lib.album.toolbar.SToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:backIcon=\"@drawable/ic_album_picker_right_arrow\"\n            app:statusBarStyle=\"Transparent\"\n            app:subItemInterval=\"10dp\"\n            app:titleGravity=\"Left\"\n            app:titleTextSize=\"18dp\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <!--中心容器-->\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/rv_picker\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipChildren=\"false\"\n        android:clipToPadding=\"false\"\n        android:paddingBottom=\"60dp\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar\"\n        android:layout_width=\"50dp\"\n        android:layout_height=\"50dp\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"visible\" />\n\n    <!--底部的照片文件夹选择器-->\n    <LinearLayout\n        android:id=\"@+id/ll_bottom_menu\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"500dp\"\n        android:orientation=\"vertical\"\n        app:behavior_peekHeight=\"60dp\"\n        app:layout_behavior=\"@string/bottom_sheet_behavior\">\n\n        <RelativeLayout\n            android:id=\"@+id/rv_menu_nav_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"60dp\"\n            android:background=\"@color/lib_album_picker_bottom_menu_nav_bg_collapsed_color\">\n\n            <ImageView\n                android:id=\"@+id/iv_nav_indicator\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerVertical=\"true\"\n                android:paddingLeft=\"20dp\"\n                app:srcCompat=\"@drawable/ic_album_picker_bottom_indicator\" />\n\n            <TextView\n                android:id=\"@+id/tv_folder_name\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_toRightOf=\"@+id/iv_nav_indicator\"\n                android:drawablePadding=\"5dp\"\n                android:gravity=\"center_vertical\"\n                android:paddingStart=\"5dp\"\n                android:paddingLeft=\"5dp\"\n                android:text=\"@string/lib_album_picker_all_picture\"\n                android:textColor=\"@color/lib_album_picker_bottom_menu_nav_text_collapsed_color\"\n                android:textSize=\"15dp\" />\n\n            <TextView\n                android:id=\"@+id/tv_preview\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_alignParentRight=\"true\"\n                android:layout_gravity=\"center_vertical|right\"\n                android:gravity=\"center\"\n                android:paddingLeft=\"20dp\"\n                android:paddingRight=\"20dp\"\n                android:text=\"@string/lib_album_picker_preview\"\n                android:textColor=\"@color/lib_album_picker_bottom_menu_nav_text_collapsed_color\"\n                android:textSize=\"14dp\" />\n\n        </RelativeLayout>\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/recycle_folders\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/lib_album_picker_bottom_menu_content_folders_bg_color\" />\n\n    </LinearLayout>\n\n    <!--悬浮按钮-->\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginRight=\"20dp\"\n        android:layout_marginBottom=\"70dp\"\n        app:layout_anchor=\"@+id/rv_picker\"\n        app:layout_anchorGravity=\"bottom|right\"\n        app:layout_behavior=\"@string/lib_album_picker_fab_behavior\"\n        app:srcCompat=\"@drawable/ic_album_picker_fab\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/layout/lib_album_recycle_item_folder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:foreground=\"?attr/selectableItemBackgroundBorderless\"\n    android:orientation=\"horizontal\"\n    android:paddingLeft=\"20dp\"\n    android:paddingTop=\"5dp\"\n    android:paddingRight=\"20dp\"\n    android:paddingBottom=\"5dp\">\n\n    <ImageView\n        android:id=\"@+id/iv_preview\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"80dp\"\n        android:scaleType=\"centerCrop\" />\n\n    <TextView\n        android:id=\"@+id/tv_folder_name\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginLeft=\"15dp\"\n        android:gravity=\"center\" />\n\n</LinearLayout>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/layout/lib_album_recycle_item_header_camera.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ImageView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/iv_picture\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:foreground=\"?attr/selectableItemBackgroundBorderless\"\n    android:paddingLeft=\"30dp\"\n    android:paddingRight=\"30dp\"\n    android:scaleType=\"fitCenter\"\n    app:srcCompat=\"@drawable/ic_album_picker_camera_header\" />"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/layout/lib_album_recycle_item_picture.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"1dp\">\n\n    <ImageView\n        android:id=\"@+id/iv_picture\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:foreground=\"?attr/selectableItemBackgroundBorderless\" />\n\n    <com.sharry.lib.album.CheckedIndicatorView\n        android:id=\"@+id/check_indicator\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end|top\"\n        android:textColor=\"@android:color/white\" />\n\n    <ImageView\n        android:id=\"@+id/iv_gif_tag\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"left|bottom\"\n        android:layout_margin=\"5dp\"\n        app:srcCompat=\"@drawable/ic_album_picker_gif\" />\n\n</FrameLayout>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/layout/lib_album_recycle_item_video.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tool=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"1dp\">\n\n    <ImageView\n        android:id=\"@+id/iv_picture\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:src=\"@drawable/ic_album_picker_video_default\" />\n\n    <ImageView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/lib_album_picker_recycle_item_video_bg_color\"\n        android:foreground=\"?attr/selectableItemBackgroundBorderless\"\n        android:scaleType=\"centerInside\"\n        app:srcCompat=\"@drawable/ic_album_picker_video_play\" />\n\n    <com.sharry.lib.album.CheckedIndicatorView\n        android:id=\"@+id/check_indicator\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end|top\"\n        android:textColor=\"@android:color/white\" />\n\n    <TextView\n        android:id=\"@+id/tv_duration\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:padding=\"10dp\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"13dp\"\n        tool:text=\"15:00\" />\n\n</FrameLayout>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/values/picker_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <color name=\"lib_album_picker_theme_primary_color\">#ff00b0ff</color>\n    <color name=\"lib_album_picker_theme_primary_dark_color\">#ff00b0ff</color>\n    <color name=\"lib_album_picker_theme_accent_color\">#ff64b6f6</color>\n\n    <color name=\"lib_album_picker_bottom_menu_nav_bg_collapsed_color\">#a9000000</color>\n    <color name=\"lib_album_picker_bottom_menu_navi_bg_expand_color\">#ffffffff</color>\n    <color name=\"lib_album_picker_bottom_menu_nav_text_collapsed_color\">#ffffffff</color>\n    <color name=\"lib_album_picker_bottom_menu_navi_text_expand_color\">#ff333333</color>\n    <color name=\"lib_album_picker_bottom_menu_content_folders_bg_color\">#ffffffff</color>\n    <color name=\"lib_album_picker_recycle_item_video_bg_color\">#A9313131</color>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/values/picker_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"lib_album_picker_ensure\">Ensure</string>\n    <string name=\"lib_album_picker_preview\">Preview</string>\n    <string name=\"lib_album_picker_all_picture\">All</string>\n    <string name=\"lib_album_picker_root_folder\">Storage Card</string>\n    <string name=\"lib_album_picker_tips_over_threshold_prefix\">Pick a maximum of&#160;</string>\n    <string name=\"lib_album_picker_tips_over_threshold_suffix\">&#160;pictures.</string>\n    <string name=\"lib_album_picker_tips_fetch_album_failed\">Fetch album data failed.</string>\n    <string name=\"lib_album_picker_tips_ensure_failed\">Please pick at least one picture.</string>\n    <string name=\"lib_album_picker_tips_preview_failed\">@string/lib_album_picker_tips_ensure_failed</string>\n    <string name=\"lib_album_picker_fab_behavior\" translatable=\"false\">com.sharry.lib.album.PicturePickerFabBehavior</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/values/picker_themes.xml",
    "content": "<resources>\n\n    <style name=\"PickerTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/lib_album_picker_theme_primary_color</item>\n        <item name=\"colorPrimaryDark\">@color/lib_album_picker_theme_primary_dark_color</item>\n        <item name=\"colorAccent\">@color/lib_album_picker_theme_accent_color</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "lib-album/src/main/picker/com/sharry/lib/album/res/values-zh/picker_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!--PickerActivity 需要用到的字符串-->\n    <string name=\"lib_album_picker_ensure\">确认</string>\n    <string name=\"lib_album_picker_preview\">预览</string>\n    <string name=\"lib_album_picker_all_picture\">所有</string>\n    <string name=\"lib_album_picker_root_folder\">SD 卡根目录</string>\n    <string name=\"lib_album_picker_tips_over_threshold_prefix\">最多只可选择&#160;</string>\n    <string name=\"lib_album_picker_tips_over_threshold_suffix\">&#160;张图片</string>\n    <string name=\"lib_album_picker_tips_fetch_album_failed\">获取相册数据失败</string>\n    <string name=\"lib_album_picker_tips_ensure_failed\">至少选择一张图片</string>\n    <string name=\"lib_album_picker_tips_preview_failed\">@string/lib_album_picker_tips_ensure_failed</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/VideoPlayerActivity.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ObjectAnimator;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.media.MediaPlayer;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.util.Log;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\nimport android.widget.VideoView;\n\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.appcompat.widget.AppCompatSeekBar;\nimport androidx.constraintlayout.widget.ConstraintLayout;\n\n/**\n * 视频播放的 Activity\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-04 16:33\n */\npublic class VideoPlayerActivity extends AppCompatActivity implements MediaPlayer.OnPreparedListener,\n        MediaPlayer.OnCompletionListener,\n        MediaPlayer.OnErrorListener,\n        View.OnClickListener {\n\n    private static final String EXTRA_MEDIA_META = \"extra_media_meta\";\n\n    public static void launch(Context context, MediaMeta mediaMeta) {\n        Intent intent = new Intent(context, VideoPlayerActivity.class);\n        intent.putExtra(EXTRA_MEDIA_META, mediaMeta);\n        context.startActivity(intent);\n        if (context instanceof Activity) {\n            ((Activity) context).overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n        }\n    }\n\n    private static final String TAG = VideoPlayerActivity.class.getSimpleName();\n    private static final int MSG_WHAT_UPDATE_PROGRESS = 0;\n    private static final int MAXIMUM_TRY_AGAIN_THRESHOLD = 3;\n\n    private final Handler mHandler = new Handler(Looper.getMainLooper()) {\n        @Override\n        public void handleMessage(Message msg) {\n            if (msg.what != MSG_WHAT_UPDATE_PROGRESS) {\n                return;\n            }\n            // 更新进度\n            updateProgress();\n        }\n    };\n\n    /**\n     * 播放数据源\n     */\n    private MediaMeta mDataSource;\n\n    /**\n     * Widgets.\n     */\n    private VideoView mVideoView;\n    private TextView mTvCurrent;\n    private AppCompatSeekBar mSeekBar;\n    private TextView mTvTotal;\n    private ConstraintLayout mClControl;\n    private ImageView mIvControl;\n    private ObjectAnimator mPanelHindAnimator;\n    private ObjectAnimator mPanelShowAnimator;\n\n    /**\n     * 条件控制变量\n     */\n    private boolean mIsPrepared = false;\n    private boolean mIsPaused = false;\n    private int mCountTryAgain = 0;\n    private int mCurrentDuration;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.lib_album_activity_video_player);\n        parseIntent();\n        initViews();\n        prepare();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        showControlPanel();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        pause();\n    }\n\n    @Override\n    public void onBackPressed() {\n        stop();\n        super.onBackPressed();\n    }\n\n    @Override\n    public void finish() {\n        super.finish();\n        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n    }\n\n    ////////////////////////////////////MediaPlayer.OnCompletionListener///////////////////////////////////////\n\n    /**\n     * Callback when SetVideoPath / onResume\n     */\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        // 标记为准备完成\n        mIsPrepared = true;\n        // 清空重试次数\n        mCountTryAgain = 0;\n        // 为 View 注入数据\n        mTvTotal.setText(DateUtil.format(mp.getDuration()));\n        mSeekBar.setMax(mp.getDuration());\n        // 若之前是暂停, 则显示控制面板\n        if (mIsPaused) {\n            showControlPanel();\n        }\n        // 若之前非暂停, 则直接播放\n        else {\n            play();\n        }\n    }\n\n    ////////////////////////////////////MediaPlayer.OnErrorListener///////////////////////////////////////\n\n    @Override\n    public boolean onError(MediaPlayer mp, int what, int extra) {\n        if (mCountTryAgain++ < MAXIMUM_TRY_AGAIN_THRESHOLD) {\n            Log.w(TAG, \"Occurred an error, try again \" + mCountTryAgain + \" time\");\n            prepare();\n            return true;\n        } else {\n            // 重置视图\n            reset();\n            // 标记为准备失败\n            mIsPrepared = false;\n            return false;\n        }\n    }\n\n    ////////////////////////////////////MediaPlayer.OnCompletionListener///////////////////////////////////////\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n        reset();\n        mIsPrepared = false;\n    }\n\n    ////////////////////////////////////View.OnClickListener///////////////////////////////////////\n\n    @Override\n    public void onClick(View v) {\n        if (v.getId() == R.id.video_view || v.getId() == R.id.fl_container) {\n            showControlPanel();\n        } else if (v.getId() == R.id.cl_control) {\n            hindControlPanel();\n        } else if (v.getId() == R.id.iv_control) {\n            if (mIsPrepared) {\n                if (mVideoView.isPlaying()) {\n                    pause();\n                } else {\n                    play();\n                }\n            } else {\n                prepare();\n            }\n        } else {\n            // ignore.\n        }\n    }\n\n    //////////////////////////////////// Private methods ///////////////////////////////////////\n\n    private void parseIntent() {\n        mDataSource = getIntent().getParcelableExtra(EXTRA_MEDIA_META);\n    }\n\n    private void initViews() {\n        // 设置外层窗体, 让其可响应事件\n        findViewById(R.id.fl_container).setOnClickListener(this);\n        // 配置 Video View\n        mVideoView = findViewById(R.id.video_view);\n        mVideoView.setOnPreparedListener(this);\n        mVideoView.setOnCompletionListener(this);\n        mVideoView.setOnErrorListener(this);\n        mVideoView.setOnClickListener(this);\n        // 配置 SeekBar\n        mSeekBar = findViewById(R.id.seek_bar);\n        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n                mCurrentDuration = progress;\n                mTvCurrent.setText(DateUtil.format(mCurrentDuration));\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                mVideoView.seekTo(mCurrentDuration);\n            }\n        });\n        // 控制中心\n        mClControl = findViewById(R.id.cl_control);\n        mClControl.setOnClickListener(this);\n        // 控制按钮\n        mIvControl = findViewById(R.id.iv_control);\n        mIvControl.setOnClickListener(this);\n        // 播放进度\n        mTvCurrent = findViewById(R.id.tv_current);\n        mTvTotal = findViewById(R.id.tv_total);\n    }\n\n    private void updateProgress() {\n        // 更新进度\n        mTvCurrent.setText(DateUtil.format(mVideoView.getCurrentPosition()));\n        mSeekBar.setProgress(mVideoView.getCurrentPosition());\n        // 进行下一次更新\n        if (mVideoView != null && mVideoView.isPlaying()) {\n            mHandler.sendEmptyMessageDelayed(MSG_WHAT_UPDATE_PROGRESS, 1000);\n        }\n    }\n\n    private void showControlPanel() {\n        if (mPanelShowAnimator == null) {\n            mPanelShowAnimator = ObjectAnimator.ofFloat(mClControl,\n                    \"alpha\", 0f, 1f);\n            mPanelShowAnimator.setDuration(200);\n            mPanelShowAnimator.addListener(new AnimatorListenerAdapter() {\n\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    mClControl.setVisibility(View.VISIBLE);\n                }\n\n            });\n        }\n        if (mPanelShowAnimator.isRunning()) {\n            return;\n        }\n        mPanelShowAnimator.start();\n    }\n\n    private void hindControlPanel() {\n        if (mPanelHindAnimator == null) {\n            mPanelHindAnimator = ObjectAnimator.ofFloat(mClControl,\n                    \"alpha\", 1f, 0f);\n            mPanelHindAnimator.setDuration(200);\n            mPanelHindAnimator.addListener(new AnimatorListenerAdapter() {\n\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    mClControl.setVisibility(View.VISIBLE);\n                }\n\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    mClControl.setVisibility(View.INVISIBLE);\n                }\n            });\n        }\n        if (mPanelHindAnimator.isRunning()) {\n            return;\n        }\n        mPanelHindAnimator.start();\n    }\n\n    //////////////////////////////////// VideoPlay Control ///////////////////////////////////////\n\n    private void prepare() {\n        mVideoView.setVideoURI(mDataSource.contentUri);\n        reset();\n    }\n\n    private void play() {\n        mIvControl.setImageResource(R.drawable.ic_album_player_video_pasue);\n        mVideoView.start();\n        mVideoView.seekTo(mCurrentDuration);\n        updateProgress();\n        mIsPaused = false;\n    }\n\n    private void pause() {\n        mIvControl.setImageResource(R.drawable.ic_album_player_video_play);\n        mVideoView.pause();\n        mHandler.removeMessages(MSG_WHAT_UPDATE_PROGRESS);\n        mIsPaused = true;\n    }\n\n    private void stop() {\n        mVideoView.stopPlayback();\n    }\n\n    private void reset() {\n        mHandler.removeMessages(MSG_WHAT_UPDATE_PROGRESS);\n        mIvControl.setImageResource(R.drawable.ic_album_player_video_play);\n        mTvCurrent.setText(DateUtil.format(0));\n        mSeekBar.setProgress(0);\n        showControlPanel();\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/res/drawable/ic_album_player_video_pasue.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/res/drawable/ic_album_player_video_play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/res/layout/lib_album_activity_video_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/fl_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/lib_album_player_bg_color\"\n    android:orientation=\"vertical\">\n\n    <com.sharry.lib.album.toolbar.SToolbar\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"invisible\"\n        app:statusBarStyle=\"Transparent\" />\n\n    <VideoView\n        android:id=\"@+id/video_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/cl_control\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/lib_album_picker_recycle_item_video_bg_color\">\n\n        <ImageView\n            android:id=\"@+id/iv_control\"\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:srcCompat=\"@drawable/ic_album_player_video_play\" />\n\n        <TextView\n            android:id=\"@+id/tv_current\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingLeft=\"15dp\"\n            android:text=\"00:00\"\n            android:textColor=\"@android:color/white\"\n            app:layout_constraintBottom_toBottomOf=\"@id/seek_bar\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"@id/seek_bar\" />\n\n        <androidx.appcompat.widget.AppCompatSeekBar\n            android:id=\"@+id/seek_bar\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"80dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toRightOf=\"@id/tv_current\"\n            app:layout_constraintRight_toLeftOf=\"@id/tv_total\" />\n\n        <TextView\n            android:id=\"@+id/tv_total\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingRight=\"15dp\"\n            android:text=\"00:00\"\n            android:textColor=\"@android:color/white\"\n            app:layout_constraintBottom_toBottomOf=\"@id/seek_bar\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"@id/seek_bar\" />\n\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</FrameLayout>"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/res/layout-land/lib_album_activity_video_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/fl_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/lib_album_player_bg_color\"\n    android:orientation=\"vertical\">\n\n    <com.sharry.lib.album.toolbar.SToolbar\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"invisible\"\n        app:statusBarStyle=\"Transparent\" />\n\n    <VideoView\n        android:id=\"@+id/video_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/cl_control\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/lib_album_picker_recycle_item_video_bg_color\">\n\n        <ImageView\n            android:id=\"@+id/iv_control\"\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:srcCompat=\"@drawable/ic_album_player_video_play\" />\n\n        <TextView\n            android:id=\"@+id/tv_current\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingLeft=\"15dp\"\n            android:text=\"00:00\"\n            android:textColor=\"@android:color/white\"\n            app:layout_constraintBottom_toBottomOf=\"@id/seek_bar\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"@id/seek_bar\" />\n\n        <androidx.appcompat.widget.AppCompatSeekBar\n            android:id=\"@+id/seek_bar\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"80dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toRightOf=\"@id/tv_current\"\n            app:layout_constraintRight_toLeftOf=\"@id/tv_total\" />\n\n        <TextView\n            android:id=\"@+id/tv_total\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingRight=\"15dp\"\n            android:text=\"00:00\"\n            android:textColor=\"@android:color/white\"\n            app:layout_constraintBottom_toBottomOf=\"@id/seek_bar\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"@id/seek_bar\" />\n\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</FrameLayout>"
  },
  {
    "path": "lib-album/src/main/player/com/sharry/lib/album/res/values/player_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <color name=\"lib_album_player_bg_color\">#FF000000</color>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/AspectRatioFragment.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.sharry.lib.album;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.sharry.lib.camera.AspectRatio;\n\n\n/**\n * A simple dialog that allows user to pick an aspect ratio.\n */\npublic class AspectRatioFragment extends DialogFragment {\n\n    private static final String ARG_ASPECT_RATIOS = \"aspect_ratios\";\n    private static final String ARG_CURRENT_ASPECT_RATIO = \"current_aspect_ratio\";\n\n    private Listener mListener;\n\n    static AspectRatioFragment newInstance(AspectRatio[] ratios,\n                                           AspectRatio currentRatio) {\n        final AspectRatioFragment fragment = new AspectRatioFragment();\n        final Bundle args = new Bundle();\n        args.putParcelableArray(ARG_ASPECT_RATIOS, ratios);\n        args.putParcelable(ARG_CURRENT_ASPECT_RATIO, currentRatio);\n        fragment.setArguments(args);\n        return fragment;\n    }\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        mListener = (Listener) context;\n    }\n\n    @Override\n    public void onDetach() {\n        mListener = null;\n        super.onDetach();\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        final Bundle args = getArguments();\n        final AspectRatio[] ratios = (AspectRatio[]) args.getParcelableArray(ARG_ASPECT_RATIOS);\n        if (ratios == null) {\n            throw new RuntimeException(\"No ratios\");\n        }\n        final AspectRatio current = args.getParcelable(ARG_CURRENT_ASPECT_RATIO);\n        final AspectRatioAdapter adapter = new AspectRatioAdapter(ratios, current);\n        return new AlertDialog.Builder(getContext())\n                .setAdapter(adapter, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int position) {\n                        mListener.onAspectRatioSelected(ratios[position]);\n                    }\n                })\n                .create();\n    }\n\n    private static class AspectRatioAdapter extends BaseAdapter {\n\n        private final AspectRatio[] mRatios;\n        private final AspectRatio mCurrentRatio;\n\n        AspectRatioAdapter(AspectRatio[] ratios, AspectRatio current) {\n            mRatios = ratios;\n            mCurrentRatio = current;\n        }\n\n        @Override\n        public int getCount() {\n            return mRatios.length;\n        }\n\n        @Override\n        public AspectRatio getItem(int position) {\n            return mRatios[position];\n        }\n\n        @Override\n        public long getItemId(int position) {\n            return position;\n        }\n\n        @Override\n        public View getView(int position, View view, ViewGroup parent) {\n            AspectRatioAdapter.ViewHolder holder;\n            if (view == null) {\n                view = LayoutInflater.from(parent.getContext())\n                        .inflate(android.R.layout.simple_list_item_1, parent, false);\n                holder = new AspectRatioAdapter.ViewHolder();\n                holder.text = view.findViewById(android.R.id.text1);\n                view.setTag(holder);\n            } else {\n                holder = (AspectRatioAdapter.ViewHolder) view.getTag();\n            }\n            AspectRatio ratio = getItem(position);\n            StringBuilder sb = new StringBuilder(ratio.toString());\n            if (ratio.equals(mCurrentRatio)) {\n                sb.append(\" *\");\n            }\n            holder.text.setText(sb);\n            return view;\n        }\n\n        private static class ViewHolder {\n            TextView text;\n        }\n\n    }\n\n    public interface Listener {\n        void onAspectRatioSelected(@NonNull AspectRatio ratio);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/ITakerContract.java",
    "content": "package com.sharry.lib.album;\n\nimport android.graphics.Bitmap;\nimport android.net.Uri;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.StringRes;\n\nimport com.sharry.lib.camera.AspectRatio;\nimport com.sharry.lib.camera.SCameraView;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-02\n */\npublic interface ITakerContract {\n\n    interface IView {\n\n        void setPreviewAspect(@NonNull AspectRatio aspect);\n\n        void setPreviewFullScreen(boolean fullScreen);\n\n        void setPreviewRenderer(@NonNull String rendererClassName);\n\n        void setRecordButtonVisible(boolean visible);\n\n        void setSupportVideoRecord(boolean isVideoRecord);\n\n        void setMaxRecordDuration(long maxDuration);\n\n        void setRecordButtonProgress(long currentDuration);\n\n        void setProgressColor(int recordProgressColor);\n\n        void setPreviewSource(@NonNull Bitmap bitmap);\n\n        void startVideoPlayer(@NonNull Uri uri);\n\n        void stopVideoPlayer();\n\n        int STATUS_CAMERA_PREVIEW = 1;\n        int STATUS_PICTURE_PREVIEW = 2;\n        int STATUS_VIDEO_PLAY = 3;\n        int STATUS_PICKED = 4;\n\n        Bitmap getCameraBitmap();\n\n        @IntDef(value = {\n                STATUS_CAMERA_PREVIEW,\n                STATUS_PICTURE_PREVIEW,\n                STATUS_VIDEO_PLAY,\n                STATUS_PICKED\n        })\n        @interface Status {\n        }\n\n        void setStatus(@Status int status);\n\n        @Status\n        int getStatus();\n\n        void toast(@StringRes int resId);\n\n        void setResult(@NonNull MediaMeta mediaMeta);\n\n    }\n\n    interface IPresenter {\n\n        void handleGranted();\n\n        void handleDenied();\n\n        void handleTakePicture();\n\n        void handleRecordStart(SCameraView cameraView);\n\n        void handleRecordFinish(long duration);\n\n        void handleVideoPlayFailed();\n\n        void handleViewDestroy();\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerActivity.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.media.MediaPlayer;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.Toast;\nimport android.widget.VideoView;\n\nimport androidx.annotation.IntegerRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.constraintlayout.widget.ConstraintLayout;\nimport androidx.constraintlayout.widget.ConstraintSet;\n\nimport com.sharry.lib.album.toolbar.ImageViewOptions;\nimport com.sharry.lib.album.toolbar.SToolbar;\nimport com.sharry.lib.camera.AspectRatio;\nimport com.sharry.lib.camera.IPreviewer;\nimport com.sharry.lib.camera.SCameraView;\n\nimport java.lang.reflect.Constructor;\n\n/**\n * 图片/视频拍摄页面\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-02\n */\npublic class TakerActivity extends AppCompatActivity implements\n        ITakerContract.IView,\n        AspectRatioFragment.Listener,\n        RecorderButton.Interaction {\n\n    public static final int REQUEST_CODE = 286;\n    public static final String RESULT_EXTRA_MEDIA_META = \"RESULT_EXTRA_MEDIA_META\";\n    private static final String EXTRA_TAKER_CONFIG = \"extra_taker_config\";\n\n\n    public static void launchForResult(CallbackFragment fragment, TakerConfig config) {\n        Intent intent = new Intent(fragment.getActivity(), TakerActivity.class);\n        intent.putExtra(EXTRA_TAKER_CONFIG, config);\n        fragment.startActivityForResult(intent, REQUEST_CODE);\n    }\n\n    private static final AspectRatio[] ASPECT_RATIOS = {\n            AspectRatio.of(1, 1),\n            AspectRatio.of(4, 3),\n            AspectRatio.of(16, 9)\n    };\n\n    /**\n     * The presenter associated with this Activity.\n     */\n    private ITakerContract.IPresenter mPresenter;\n\n    /**\n     * Widgets\n     */\n    private SToolbar mToolbar;\n    private ImageView mIvAspect;\n    private ImageView mIvFullScreen;\n    private SCameraView mCameraView;\n    private RecorderButton mBtnRecord;\n    private ImageView mIvPicturePreview;\n    private VideoView mVideoPlayer;\n    private ConstraintLayout mClEnsurePanel;\n    private ImageView mIvDenied;\n    private ImageView mIvGranted;\n    private int mStatus;\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.lib_ablum_activity_taker);\n        initTitle();\n        initViews();\n        initPresenter();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        if (mVideoPlayer.getVisibility() == View.VISIBLE) {\n            mVideoPlayer.resume();\n        } else {\n            mVideoPlayer.stopPlayback();\n        }\n        if (mCameraView.getVisibility() == View.VISIBLE) {\n            mCameraView.startPreview();\n        } else {\n            mCameraView.stopPreview();\n        }\n    }\n\n    @Override\n    protected void onPause() {\n        mCameraView.stopPreview();\n        mVideoPlayer.pause();\n        super.onPause();\n    }\n\n    @Override\n    protected void onDestroy() {\n        mPresenter.handleViewDestroy();\n        super.onDestroy();\n    }\n\n    ////////////////////////////////////ITakerContract.IView///////////////////////////////////////\n\n\n    @Override\n    public void setPreviewAspect(@NonNull AspectRatio aspect) {\n        mCameraView.setAspectRatio(aspect);\n    }\n\n    @Override\n    public void setPreviewFullScreen(boolean fullScreen) {\n        mCameraView.setAdjustViewBounds(!fullScreen);\n        ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mCameraView.getLayoutParams();\n        params.topToTop = fullScreen ? ConstraintSet.PARENT_ID : ConstraintSet.UNSET;\n        params.topToBottom = fullScreen ? ConstraintSet.UNSET : R.id.toolbar;\n        mCameraView.setLayoutParams(params);\n    }\n\n    @Override\n    public void setPreviewRenderer(@NonNull String rendererClassName) {\n        try {\n            Class<? extends IPreviewer.Renderer> rendererClass = (Class<? extends IPreviewer.Renderer>)\n                    Class.forName(rendererClassName);\n            Constructor constructor = rendererClass.getDeclaredConstructor(Context.class);\n            constructor.setAccessible(true);\n            IPreviewer.Renderer renderer = (IPreviewer.Renderer) constructor.newInstance(this);\n            mCameraView.getPreviewer().setRenderer(renderer);\n        } catch (Throwable e) {\n            // ignore.\n        }\n    }\n\n    @Override\n    public void setRecordButtonVisible(boolean visible) {\n        mBtnRecord.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);\n    }\n\n    @Override\n    public void setSupportVideoRecord(boolean isVideoRecord) {\n        mBtnRecord.setLongClickEnable(isVideoRecord);\n    }\n\n    @Override\n    public void setMaxRecordDuration(long maxDuration) {\n        mBtnRecord.setMaxProgress(maxDuration);\n    }\n\n    @Override\n    public void setRecordButtonProgress(long currentDuration) {\n        mBtnRecord.setCurrentProgress(currentDuration);\n    }\n\n    @Override\n    public void setProgressColor(int recordProgressColor) {\n        mBtnRecord.setProgressColor(recordProgressColor);\n    }\n\n    @Override\n    public void setPreviewSource(@NonNull Bitmap bitmap) {\n        mIvPicturePreview.setImageBitmap(bitmap);\n    }\n\n    @Override\n    public void startVideoPlayer(@NonNull Uri uri) {\n        mVideoPlayer.setVideoURI(uri);\n    }\n\n    @Override\n    public void stopVideoPlayer() {\n        mVideoPlayer.stopPlayback();\n    }\n\n    @Override\n    public Bitmap getCameraBitmap() {\n        return mCameraView.takePicture();\n    }\n\n    @Override\n    public void setStatus(int status) {\n        mStatus = status;\n        switch (status) {\n            case STATUS_PICKED:\n                // Keeping.\n                break;\n            case STATUS_VIDEO_PLAY:\n                // 停止预览\n                mCameraView.stopPreview();\n                // 置为视频播放状态\n                mToolbar.setVisibility(View.INVISIBLE);\n                mCameraView.setVisibility(View.INVISIBLE);\n                mBtnRecord.setVisibility(View.INVISIBLE);\n                mIvPicturePreview.setVisibility(View.INVISIBLE);\n                mVideoPlayer.setVisibility(View.VISIBLE);\n                mClEnsurePanel.setVisibility(View.VISIBLE);\n                break;\n            case STATUS_PICTURE_PREVIEW:\n                // 停止预览\n                mCameraView.stopPreview();\n                // 置为照片预览状态\n                mToolbar.setVisibility(View.INVISIBLE);\n                mCameraView.setVisibility(View.INVISIBLE);\n                mBtnRecord.setVisibility(View.INVISIBLE);\n                mVideoPlayer.setVisibility(View.INVISIBLE);\n                mIvPicturePreview.setVisibility(View.VISIBLE);\n                mClEnsurePanel.setVisibility(View.VISIBLE);\n                break;\n            case STATUS_CAMERA_PREVIEW:\n            default:\n                // 置为预览状态\n                mToolbar.setVisibility(View.VISIBLE);\n                mIvFullScreen.setVisibility(View.VISIBLE);\n                mIvAspect.setVisibility(View.VISIBLE);\n                mCameraView.setVisibility(View.VISIBLE);\n                mBtnRecord.setVisibility(View.VISIBLE);\n                mVideoPlayer.setVisibility(View.INVISIBLE);\n                mIvPicturePreview.setVisibility(View.INVISIBLE);\n                mClEnsurePanel.setVisibility(View.INVISIBLE);\n                // 开始预览\n                mCameraView.startPreview();\n                break;\n        }\n    }\n\n    @Override\n    public int getStatus() {\n        return mStatus;\n    }\n\n    @Override\n    public void toast(@IntegerRes int resId) {\n        Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();\n    }\n\n    @Override\n    public void setResult(@NonNull MediaMeta mediaMeta) {\n        Intent intent = new Intent();\n        intent.putExtra(RESULT_EXTRA_MEDIA_META, mediaMeta);\n        setResult(RESULT_OK, intent);\n        setStatus(STATUS_PICKED);\n        finish();\n    }\n\n    ////////////////////////////////////AspectRatioFragment.Interaction///////////////////////////////////////\n\n    @Override\n    public void onAspectRatioSelected(@NonNull AspectRatio ratio) {\n        mCameraView.setAspectRatio(ratio);\n    }\n\n    ////////////////////////////////////RecordProgressButton.Interaction///////////////////////////////////////\n\n    @Override\n    public void onTakePicture() {\n        mPresenter.handleTakePicture();\n    }\n\n    @Override\n    public void onRecordStart() {\n        mIvAspect.setVisibility(View.INVISIBLE);\n        mIvFullScreen.setVisibility(View.INVISIBLE);\n        mPresenter.handleRecordStart(mCameraView);\n    }\n\n    @Override\n    public void onRecordFinish(long duration) {\n        mIvAspect.setVisibility(View.VISIBLE);\n        mIvFullScreen.setVisibility(View.VISIBLE);\n        mPresenter.handleRecordFinish(duration);\n    }\n\n    ////////////////////////////////////Private method///////////////////////////////////////\n\n    private void initTitle() {\n        mToolbar = findViewById(R.id.toolbar);\n        int paddingSize = DensityUtil.dp2px(this, 20f);\n        // switch\n        mToolbar.addLeftMenuImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(R.drawable.ic_album_taker_camera_switch)\n                        .setPaddingLeft(paddingSize)\n                        .setListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                int curFacing = mCameraView.getFacing();\n                                mCameraView.setFacing(curFacing == SCameraView.FACING_FRONT ?\n                                        SCameraView.FACING_BACK : SCameraView.FACING_FRONT);\n                            }\n                        })\n                        .build()\n        );\n        // aspect\n        mToolbar.addLeftMenuImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(R.drawable.ic_album_taker_aspect)\n                        .setPaddingLeft(paddingSize)\n                        .setListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                AspectRatioFragment.newInstance(ASPECT_RATIOS, mCameraView.getAspectRatio())\n                                        .show(getSupportFragmentManager(), AspectRatioFragment.class.getSimpleName());\n                            }\n                        })\n                        .build()\n        );\n        // 全面屏\n        mToolbar.addLeftMenuImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(R.drawable.ic_album_taker_full_screen)\n                        .setPaddingLeft(paddingSize)\n                        .setListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                setPreviewFullScreen(mCameraView.getAdjustViewBounds());\n                            }\n                        })\n                        .build()\n        );\n        // 获取 Toolbar 上的控件\n        mIvAspect = mToolbar.getLeftMenuView(1);\n        mIvFullScreen = mToolbar.getLeftMenuView(2);\n    }\n\n    private void initViews() {\n        // SCameraView\n        mCameraView = findViewById(R.id.camera_view);\n        mCameraView.setAutoFocus(true);\n\n        // RecordProgressButton\n        mBtnRecord = findViewById(R.id.btn_record);\n\n        // Taken picture preview\n        mIvPicturePreview = findViewById(R.id.iv_picture_preview);\n\n        // Video Player\n        mVideoPlayer = findViewById(R.id.video_view);\n        mVideoPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {\n            @Override\n            public void onPrepared(MediaPlayer mp) {\n                mp.start();\n                mp.setLooping(true);\n            }\n        });\n        mVideoPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {\n            @Override\n            public boolean onError(MediaPlayer mp, int what, int extra) {\n                mPresenter.handleVideoPlayFailed();\n                return true;\n            }\n        });\n\n        // Selector Panel\n        mClEnsurePanel = findViewById(R.id.cl_ensure_panel);\n\n        // Denied\n        mIvDenied = findViewById(R.id.iv_denied);\n        mIvDenied.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                mPresenter.handleDenied();\n            }\n        });\n\n        // Granted\n        mIvGranted = findViewById(R.id.iv_granted);\n        mIvGranted.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                mPresenter.handleGranted();\n            }\n        });\n    }\n\n    private void initPresenter() {\n        mPresenter = new TakerPresenter(\n                this,\n                (TakerConfig) getIntent().getParcelableExtra(EXTRA_TAKER_CONFIG)\n        );\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerCallback.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\n\n/**\n * 拍照回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-30 09:08\n */\npublic interface TakerCallback {\n\n    /**\n     * 拍照完成的回调\n     *\n     * @param newMeta 照片输出路径\n     */\n    void onCameraTakeComplete(@NonNull MediaMeta newMeta);\n\n    /**\n     * 未进行有效的拍摄\n     */\n    void onTakeFailed();\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerCallbackLambda.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.Nullable;\n\n/**\n * 拍照回调\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-30 09:08\n */\npublic interface TakerCallbackLambda {\n\n    /**\n     * 拍照完成的回调\n     *\n     * @param newMeta null is failed, nonnull is success.\n     */\n    void onCameraTake(@Nullable MediaMeta newMeta);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerConfig.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.sharry.lib.camera.IPreviewer;\nimport com.sharry.lib.media.recorder.Options;\n\n/**\n * 相机拍照的相关参数\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/6/21 09:07\n */\npublic class TakerConfig implements Parcelable {\n\n    protected TakerConfig(Parcel in) {\n        authority = in.readString();\n        quality = in.readInt();\n        relativePath = in.readString();\n        previewAspect = in.readInt();\n        isFullScreen = in.readByte() != 0;\n        isSupportVideoRecord = in.readByte() != 0;\n        isJustVideoRecord = in.readByte() != 0;\n        maximumDuration = in.readLong();\n        minimumDuration = in.readLong();\n        recordProgressColor = in.readInt();\n        recordResolution = in.readInt();\n        rendererClsName = in.readString();\n        cropConfig = in.readParcelable(CropperConfig.class.getClassLoader());\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeString(authority);\n        dest.writeInt(quality);\n        dest.writeString(relativePath);\n        dest.writeInt(previewAspect);\n        dest.writeByte((byte) (isFullScreen ? 1 : 0));\n        dest.writeByte((byte) (isSupportVideoRecord ? 1 : 0));\n        dest.writeByte((byte) (isJustVideoRecord ? 1 : 0));\n        dest.writeLong(maximumDuration);\n        dest.writeLong(minimumDuration);\n        dest.writeInt(recordProgressColor);\n        dest.writeInt(recordResolution);\n        dest.writeString(rendererClsName);\n        dest.writeParcelable(cropConfig, flags);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public static final Creator<TakerConfig> CREATOR = new Creator<TakerConfig>() {\n        @Override\n        public TakerConfig createFromParcel(Parcel in) {\n            return new TakerConfig(in);\n        }\n\n        @Override\n        public TakerConfig[] newArray(int size) {\n            return new TakerConfig[size];\n        }\n    };\n\n    /**\n     * Get instance of TakerConfig.Builder.\n     */\n    @NonNull\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    /**\n     * fileProvider 的 authority 属性, 用于 7.0 之后, 查找文件的 URI\n     */\n    private String authority;\n\n    /**\n     * 拍照后压缩的质量\n     */\n    private int quality = 80;\n\n    /**\n     * 文件输出路径\n     */\n    private String relativePath;\n\n    public static final int ASPECT_1_1 = 758;\n    public static final int ASPECT_4_3 = 917;\n    public static final int ASPECT_16_9 = 995;\n\n\n    @IntDef(value = {\n            ASPECT_1_1,\n            ASPECT_4_3,\n            ASPECT_16_9\n    })\n    @interface Aspect {\n\n    }\n\n    /**\n     * 初始时相机的预览比例\n     */\n    private int previewAspect = ASPECT_4_3;\n\n    /**\n     * 相机预览时是否缩放至全屏\n     */\n    private boolean isFullScreen;\n\n    /**\n     * 是否支持视频录制\n     * <p>\n     * 若支持视频录制, 则裁剪无效\n     */\n    private boolean isSupportVideoRecord;\n\n    /**\n     * 仅开启视频录制\n     * <p>\n     * 若仅开启视频录制，则拍照无效\n     */\n    private boolean isJustVideoRecord;\n\n    /**\n     * 视频录制最大时长\n     * <p>\n     * Unit is ms\n     */\n    private long maximumDuration = 15 * 1000;\n\n    /**\n     * 视频录制最短时长\n     * <p>\n     * Unit is ms\n     */\n    private long minimumDuration = 1000;\n\n    /**\n     * 录制时进度条的颜色\n     */\n    private int recordProgressColor = Color.parseColor(\"#ff00b0ff\");\n\n    /**\n     * 视频录制的最大分辨率\n     * <p>\n     * 默认为 720p\n     */\n    private int recordResolution = Options.Video.RESOLUTION_720P;\n\n    /**\n     * 用户自定义 Renderer 的类名\n     */\n    private String rendererClsName;\n\n    /**\n     * 提供图片裁剪的支持\n     */\n    private CropperConfig cropConfig;\n\n\n    private TakerConfig() {\n    }\n\n    public Builder rebuild() {\n        return new Builder(this);\n    }\n\n    public int getQuality() {\n        return quality;\n    }\n\n    public String getRelativePath() {\n        return relativePath;\n    }\n\n    public int getPreviewAspect() {\n        return previewAspect;\n    }\n\n    public boolean isFullScreen() {\n        return isFullScreen;\n    }\n\n    public boolean isSupportVideoRecord() {\n        return isSupportVideoRecord;\n    }\n\n    public long getMaximumDuration() {\n        return maximumDuration;\n    }\n\n    public long getMinimumDuration() {\n        return minimumDuration;\n    }\n\n    public int getRecordProgressColor() {\n        return recordProgressColor;\n    }\n\n    public String getRendererClassName() {\n        return rendererClsName;\n    }\n\n    public int getRecordResolution() {\n        return recordResolution;\n    }\n\n    public String getAuthority() {\n        return authority;\n    }\n\n    public CropperConfig getCropConfig() {\n        return cropConfig;\n    }\n\n    public String getRendererClsName() {\n        return rendererClsName;\n    }\n\n    public boolean isJustVideoRecord() {\n        return isJustVideoRecord;\n    }\n\n    public static class Builder {\n\n        private TakerConfig mConfig;\n\n        private Builder() {\n            mConfig = new TakerConfig();\n        }\n\n        private Builder(@NonNull TakerConfig config) {\n            this.mConfig = config;\n        }\n\n        /**\n         * 设置文件输出相对路径, 拍摄后的图片会生成在目录下\n         * <p>\n         * 绝对路径: \"/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum\"\n         * 相对路径: \"SAlbum\"\n         * <p>\n         * 注:\n         * Android 10 无法在外部存储卡随意创建文件, 因此会在对应的媒体目录下追加相对路径\n         * 如: \"/storage/emulated/0/\" + {@link android.os.Environment#DIRECTORY_PICTURES} + \"SAlbum\"\n         *\n         * @param relativePath 若是传 null, 则会在 \"/storage/emulated/0/\"\n         *                     + {@link android.os.Environment#DIRECTORY_PICTURES} 中创建\n         */\n        public Builder setRelativePath(@Nullable String relativePath) {\n            this.mConfig.relativePath = relativePath;\n            return this;\n        }\n\n        /**\n         * 设置文件输出的目录, 拍摄后的图片会生成在目录下\n         */\n        public Builder setAuthority(@NonNull String authority) {\n            Preconditions.checkNotEmpty(authority);\n            this.mConfig.authority = authority;\n            return this;\n        }\n\n        /**\n         * 设置拍照后的压缩质量\n         */\n        public Builder setPictureQuality(int quality) {\n            mConfig.quality = quality;\n            return this;\n        }\n\n        /**\n         * 设置裁剪的配置\n         */\n        public Builder setPreviewAspect(@Aspect int aspect) {\n            mConfig.previewAspect = aspect;\n            return this;\n        }\n\n        /**\n         * 设置裁剪的配置\n         */\n        public Builder setFullScreen(boolean isFullScreen) {\n            mConfig.isFullScreen = isFullScreen;\n            return this;\n        }\n\n        /**\n         * 是否支持录制是否\n         *\n         * @param isSupportVideoRecord true is support, false is just take picture.\n         */\n        public Builder setVideoRecord(boolean isSupportVideoRecord) {\n            mConfig.isSupportVideoRecord = isSupportVideoRecord;\n            return this;\n        }\n\n        /**\n         * 是否仅支持视频录制\n         *\n         * @param isJustVideoRecord true is just recordVideo, false is support take picture.\n         */\n        public Builder setJustVideoRecord(boolean isJustVideoRecord) {\n            mConfig.isJustVideoRecord = isJustVideoRecord;\n            return this;\n        }\n\n        /**\n         * 设置录制最大时长\n         *\n         * @param maxRecordDuration unit ms\n         */\n        public Builder setMaxRecordDuration(long maxRecordDuration) {\n            mConfig.maximumDuration = maxRecordDuration;\n            return this;\n        }\n\n        /**\n         * 设置录制最短时长\n         *\n         * @param minimumDuration unit ms\n         */\n        public Builder setMinRecordDuration(long minimumDuration) {\n            mConfig.minimumDuration = minimumDuration;\n            return this;\n        }\n\n        /**\n         * 设置录制进度条的颜色\n         */\n        public Builder setRecordProgressColor(@ColorInt int colorRecordProgress) {\n            mConfig.recordProgressColor = colorRecordProgress;\n            return this;\n        }\n\n        /**\n         * 设置用户的自定义 Renderer\n         */\n        public Builder setRenderer(@NonNull Class<? extends IPreviewer.Renderer> rendererClass) {\n            try {\n                rendererClass.getDeclaredConstructor(Context.class);\n            } catch (NoSuchMethodException e) {\n                throw new UnsupportedOperationException(\"Please ensure \" + rendererClass.getSimpleName()\n                        + \" have a constructor like: \" + rendererClass.getSimpleName() + \"(Context context)\");\n            }\n            mConfig.rendererClsName = rendererClass.getName();\n            return this;\n        }\n\n        /**\n         * 设置视频录制时的分辨率\n         */\n        public Builder setRecordResolution(@Options.Video.Resolution int recordResolution) {\n            mConfig.recordResolution = recordResolution;\n            return this;\n        }\n\n        /**\n         * 设置裁剪的配置\n         */\n        public Builder setCropConfig(@Nullable CropperConfig cropConfig) {\n            mConfig.cropConfig = cropConfig;\n            return this;\n        }\n\n        public TakerConfig build() {\n            if (TextUtils.isEmpty(mConfig.authority)) {\n                throw new UnsupportedOperationException(\"Please invoke setAuthority correct\");\n            }\n            return mConfig;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerManager.java",
    "content": "package com.sharry.lib.album;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\n\nimport androidx.annotation.NonNull;\n\nimport static android.app.Activity.RESULT_OK;\n\n/**\n * 从相机拍照获取图片的入口\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 5:02 PM\n */\npublic class TakerManager {\n\n    private static String[] sRequirePermissions = {\n            Manifest.permission.CAMERA,\n            Manifest.permission.RECORD_AUDIO,\n            Manifest.permission.READ_EXTERNAL_STORAGE,\n            Manifest.permission.WRITE_EXTERNAL_STORAGE\n    };\n\n    public static TakerManager with(@NonNull Context context) {\n        if (context instanceof Activity) {\n            Activity activity = (Activity) context;\n            return new TakerManager(activity);\n        } else {\n            throw new IllegalArgumentException(\"TakerManager.with -> Context can not cast to Activity\");\n        }\n    }\n\n    private Activity mBind;\n    private TakerConfig mConfig;\n\n    private TakerManager(Activity activity) {\n        this.mBind = activity;\n    }\n\n    /**\n     * 设置配置属性\n     */\n    public TakerManager setConfig(@NonNull TakerConfig config) {\n        this.mConfig = Preconditions.checkNotNull(config, \"Please ensure TakerConfig not null!\");\n        return this;\n    }\n\n    /**\n     * 获取拍摄照片\n     */\n    public void take(@NonNull final TakerCallbackLambda callbackLambda) {\n        take(new TakerCallback() {\n            @Override\n            public void onCameraTakeComplete(@NonNull MediaMeta newMeta) {\n                callbackLambda.onCameraTake(newMeta);\n            }\n\n            @Override\n            public void onTakeFailed() {\n                callbackLambda.onCameraTake(null);\n            }\n        });\n    }\n\n    /**\n     * 获取拍摄照片\n     */\n    public void take(@NonNull final TakerCallback callback) {\n        Preconditions.checkNotNull(callback, \"Please ensure callback not null!\");\n        Preconditions.checkNotNull(mConfig, \"Please ensure U set TakerConfig correct!\");\n        PermissionsHelper.with(mBind)\n                .request(sRequirePermissions)\n                .execute(new PermissionsCallback() {\n                    @Override\n                    public void onResult(boolean granted) {\n                        if (granted) {\n                            takeActual(callback);\n                        }\n                    }\n                });\n    }\n\n    private void takeActual(final TakerCallback callback) {\n        // 获取回调的 Fragment\n        CallbackFragment callbackFragment = CallbackFragment.getInstance(mBind);\n        if (callbackFragment == null) {\n            callback.onTakeFailed();\n            return;\n        }\n        callbackFragment.setCallback(new CallbackFragment.Callback() {\n            @Override\n            public void onActivityResult(int requestCode, int resultCode, Intent data) {\n                MediaMeta mediaMeta;\n                if (resultCode == RESULT_OK && requestCode == TakerActivity.REQUEST_CODE && data != null\n                        && (mediaMeta = data.getParcelableExtra(TakerActivity.RESULT_EXTRA_MEDIA_META)) != null) {\n                    // 处理图片裁剪\n                    if (mConfig.getCropConfig() != null && mediaMeta.isPicture) {\n                        performCropPicture(mediaMeta, callback);\n                    } else {\n                        callback.onCameraTakeComplete(mediaMeta);\n                    }\n                } else {\n                    callback.onTakeFailed();\n                }\n            }\n        });\n        // 启动拍照录像的页面\n        TakerActivity.launchForResult(callbackFragment, mConfig);\n    }\n\n    /**\n     * 处理裁剪\n     */\n    private void performCropPicture(MediaMeta mediaMeta, final TakerCallback callback) {\n        CropperManager.with(mBind)\n                .setConfig(\n                        mConfig.getCropConfig().rebuild()\n                                // 需要裁剪的文件路径\n                                .setOriginUri(mediaMeta.getContentUri())\n                                .build()\n                )\n                .crop(new CropperCallback() {\n                    @Override\n                    public void onCropComplete(@NonNull MediaMeta meta) {\n                        callback.onCameraTakeComplete(meta);\n                    }\n\n                    @Override\n                    public void onCropFailed() {\n                        callback.onTakeFailed();\n                    }\n                });\n    }\n\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/TakerPresenter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.ParcelFileDescriptor;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.sharry.lib.camera.AspectRatio;\nimport com.sharry.lib.camera.SCameraView;\nimport com.sharry.lib.media.recorder.EncodeType;\nimport com.sharry.lib.media.recorder.IRecorderCallback;\nimport com.sharry.lib.media.recorder.MuxerType;\nimport com.sharry.lib.media.recorder.Options;\nimport com.sharry.lib.media.recorder.SMediaRecorder;\n\nimport java.io.File;\n\nimport static com.sharry.lib.album.TakerConfig.ASPECT_16_9;\nimport static com.sharry.lib.album.TakerConfig.ASPECT_1_1;\nimport static com.sharry.lib.album.TakerConfig.ASPECT_4_3;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-02\n */\nclass TakerPresenter implements ITakerContract.IPresenter {\n\n    private static final String TAG = TakerPresenter.class.getSimpleName();\n    private static final int MAXIMUM_TRY_AGAIN_THRESHOLD = 3;\n\n    private final Context mContext;\n    private final ITakerContract.IView mView;\n    private final TakerConfig mConfig;\n    private final SMediaRecorder mRecorder;\n    private final Options.Video mRecordOptions;\n    private Bitmap mFetchedBitmap;\n    private long mRecordDuration;\n    private int mCountTryAgain = 0;\n    private Uri mVideoUri;\n    private File mVideoFile;\n\n    TakerPresenter(TakerActivity view, TakerConfig config) {\n        this.mContext = view;\n        this.mView = view;\n        this.mConfig = config;\n        this.mRecorder = SMediaRecorder.with(view);\n        this.mRecorder.addRecordCallback(new IRecorderCallback.Adapter() {\n\n            @Override\n            public void onProgress(long time) {\n                performProgressChanged(time);\n            }\n\n            @Override\n            public void onComplete(@NonNull Uri uri, File file) {\n                performRecordComplete(uri, file);\n            }\n\n            @Override\n            public void onFailed(int errorCode, @NonNull Throwable e) {\n                performRecordFiled();\n            }\n\n        });\n        this.mRecordOptions = new Options.Video.Builder()\n                .setRelativePath(mConfig.getRelativePath())\n                .setAuthority(mConfig.getAuthority())\n                .setEncodeType(EncodeType.Video.H264)\n                .setMuxerType(MuxerType.MP4)\n                .setResolution(Options.Video.RESOLUTION_720P)\n                .setAudioOptions(Options.Audio.DEFAULT)\n                .build();\n        // 配置视图\n        setupViews();\n    }\n\n    @Override\n    public void handleVideoPlayFailed() {\n        if (mCountTryAgain++ < MAXIMUM_TRY_AGAIN_THRESHOLD) {\n            Log.w(TAG, \"Occurred an error, try again \" + mCountTryAgain + \" time\");\n            mView.startVideoPlayer(mVideoUri);\n        } else {\n            // 重新尝试了 3 次仍然没有播放成功, 说明录制的视频有问题, 当做录制失败处理\n            performRecordFiled();\n        }\n    }\n\n    @Override\n    public void handleTakePicture() {\n        if (mConfig.isJustVideoRecord()) {\n            return;\n        }\n        Bitmap bitmap = mView.getCameraBitmap();\n        if (bitmap == null) {\n            mView.toast(R.string.lib_album_taker_take_picture_failed);\n            return;\n        }\n        // 保存 bitmap\n        mFetchedBitmap = bitmap;\n        mView.setStatus(ITakerContract.IView.STATUS_PICTURE_PREVIEW);\n        mView.setPreviewSource(mFetchedBitmap);\n    }\n\n    @Override\n    @SuppressLint(\"MissingPermission\")\n    public void handleRecordStart(SCameraView cameraView) {\n        mRecorder.start(cameraView, mRecordOptions);\n    }\n\n    @Override\n    public void handleRecordFinish(long duration) {\n        if (duration < mConfig.getMinimumDuration()) {\n            mRecorder.cancel();\n            mView.toast(R.string.lib_album_taker_record_time_too_short);\n        } else {\n            // Recorder 的 Complete 是异步操作, 这里先将录制按钮异常, 防止用户误触\n            mView.setRecordButtonVisible(false);\n            mRecorder.complete();\n        }\n    }\n\n    @Override\n    public void handleGranted() {\n        if (mVideoUri != null || mVideoFile != null) {\n            performVideoEnsure();\n        } else {\n            performPictureEnsure();\n        }\n    }\n\n    @Override\n    public void handleDenied() {\n        // 重置为预览状态\n        mView.setStatus(ITakerContract.IView.STATUS_CAMERA_PREVIEW);\n        recycle();\n    }\n\n    @Override\n    public void handleViewDestroy() {\n        mRecorder.cancel();\n        // 若非选中状态, 则重置数据\n        if (mView.getStatus() != ITakerContract.IView.STATUS_PICKED) {\n            recycle();\n        }\n    }\n\n    private void setupViews() {\n        // 配置 CameraView\n        switch (mConfig.getPreviewAspect()) {\n            case ASPECT_1_1:\n                mView.setPreviewAspect(AspectRatio.of(1, 1));\n                break;\n            case ASPECT_16_9:\n                mView.setPreviewAspect(AspectRatio.of(16, 9));\n                break;\n            case ASPECT_4_3:\n            default:\n                mView.setPreviewAspect(AspectRatio.of(4, 3));\n                break;\n        }\n        mView.setPreviewFullScreen(mConfig.isFullScreen());\n        if (!TextUtils.isEmpty(mConfig.getRendererClassName())) {\n            mView.setPreviewRenderer(mConfig.getRendererClassName());\n        }\n        // 配置 RecorderView\n        mView.setMaxRecordDuration(mConfig.getMaximumDuration());\n        mView.setSupportVideoRecord(mConfig.isSupportVideoRecord());\n        mView.setProgressColor(mConfig.getRecordProgressColor());\n        // 设置 View 为预览状态\n        mView.setStatus(ITakerContract.IView.STATUS_CAMERA_PREVIEW);\n    }\n\n    /**\n     * 处理录制进度变更\n     */\n    private void performProgressChanged(long time) {\n        mRecordDuration = time;\n        mView.setRecordButtonProgress(time);\n    }\n\n    /**\n     * 处理录制失败\n     */\n    private void performRecordFiled() {\n        recycle();\n        mView.toast(R.string.lib_album_taker_record_failed);\n        mView.setStatus(ITakerContract.IView.STATUS_CAMERA_PREVIEW);\n    }\n\n    /**\n     * 处理录制成功\n     */\n    private void performRecordComplete(Uri uri, File file) {\n        mVideoUri = uri;\n        mVideoFile = file;\n        mView.setStatus(ITakerContract.IView.STATUS_VIDEO_PLAY);\n        mView.startVideoPlayer(mVideoUri);\n    }\n\n    /**\n     * 处理图像确认\n     */\n    private void performPictureEnsure() {\n        try {\n            if (VersionUtil.isQ()) {\n                Uri uri = FileUtil.createJpegPendingItem(mContext, mConfig.getRelativePath());\n                ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri, \"w\");\n                CompressUtil.doCompress(mFetchedBitmap, pfd.getFileDescriptor(), mConfig.getQuality(),\n                        mFetchedBitmap.getWidth(), mFetchedBitmap.getHeight());\n                FileUtil.publishPendingItem(mContext, uri);\n                String path = FileUtil.getImagePath(mContext, uri);\n                MediaMeta mediaMeta = MediaMeta.create(uri, path, true);\n                mediaMeta.date = System.currentTimeMillis();\n                mView.setResult(mediaMeta);\n            } else {\n                File file = FileUtil.createJpegFile(mContext, mConfig.getRelativePath());\n                Uri uri = FileUtil.getUriFromFile(mContext, mConfig.getAuthority(), file);\n                ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri, \"w\");\n                CompressUtil.doCompress(mFetchedBitmap, pfd.getFileDescriptor(), mConfig.getQuality(),\n                        mFetchedBitmap.getWidth(), mFetchedBitmap.getHeight());\n                FileUtil.notifyMediaStore(mContext, file.getAbsolutePath());\n                MediaMeta mediaMeta = MediaMeta.create(uri, file.getAbsolutePath(), true);\n                mediaMeta.date = System.currentTimeMillis();\n                mView.setResult(mediaMeta);\n            }\n        } catch (Throwable e) {\n            mView.toast(R.string.lib_album_taker_picture_saved_failed);\n            mView.setStatus(ITakerContract.IView.STATUS_CAMERA_PREVIEW);\n        }\n    }\n\n    /**\n     * 处理视频确认\n     */\n    private void performVideoEnsure() {\n        long currentTime = System.currentTimeMillis();\n        MediaMeta mediaMeta = MediaMeta.create(mVideoUri, mVideoFile.getAbsolutePath(), false);\n        mediaMeta.date = currentTime;\n        mediaMeta.duration = mRecordDuration;\n        mView.setResult(mediaMeta);\n    }\n\n    /**\n     * 重置资源\n     */\n    private void recycle() {\n        mFetchedBitmap = null;\n        mCountTryAgain = 0;\n        if (VersionUtil.isQ()) {\n            FileUtil.delete(mContext, mVideoUri);\n        } else {\n            FileUtil.delete(mContext, mVideoFile);\n        }\n        mVideoUri = null;\n        mVideoFile = null;\n        mView.stopVideoPlayer();\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/drawable/ic_album_taker_aspect.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/drawable/ic_album_taker_camera_switch.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,4h-3.17L15,2L9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM15,15.5L15,13L9,13v2.5L5.5,12 9,8.5L9,11h6L15,8.5l3.5,3.5 -3.5,3.5z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/drawable/ic_album_taker_denied.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"256dp\"\n    android:height=\"256dp\"\n    android:viewportWidth=\"256\"\n    android:viewportHeight=\"256\">\n  <path\n      android:pathData=\"M128,128m-128,0a128,128 0,1 1,256 0a128,128 0,1 1,-256 0\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillAlpha=\"0.6\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M147.418,103.659L81.63,103.659L101.196,87.056C103.152,85.396 103.152,82.905 101.196,81.245C99.239,79.585 96.304,79.585 94.348,81.245L66.467,104.696C65.489,105.526 65,106.564 65,107.602C65,108.639 65.489,109.677 66.467,110.507L94.103,133.958C96.06,135.618 98.995,135.618 100.951,133.958C102.908,132.298 102.908,129.808 100.951,128.147L81.875,111.96L147.418,111.96C165.761,111.96 180.435,124.412 180.435,139.977C180.435,155.541 165.761,167.993 147.418,167.993L97.283,167.993C94.592,167.993 92.391,169.861 92.391,172.144C92.391,174.427 94.592,176.295 97.283,176.295L147.418,176.295C171.141,176.295 190.217,160.107 190.217,139.977C190.217,119.846 171.141,103.659 147.418,103.659Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#353330\"\n      android:fillType=\"nonZero\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/drawable/ic_album_taker_full_screen.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/drawable/ic_album_taker_granted.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"256dp\"\n    android:height=\"256dp\"\n    android:viewportWidth=\"256\"\n    android:viewportHeight=\"256\">\n  <path\n      android:pathData=\"M128,128m-128,0a128,128 0,1 1,256 0a128,128 0,1 1,-256 0\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillAlpha=\"0.92\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M119.8,170.029L196.975,93.727C199.225,91.48 199.225,87.884 196.975,85.749C194.725,83.501 191.125,83.501 188.875,85.749L114.063,159.691L74.012,119.573C71.762,117.326 68.05,117.326 65.8,119.573C63.55,121.821 63.55,125.529 65.8,127.777L107.988,169.917C111.813,173.737 115.75,173.962 119.8,170.029Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#1BAC1A\"\n      android:fillType=\"nonZero\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/layout/lib_ablum_activity_taker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tool=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"#ff000000\"\n    android:orientation=\"vertical\">\n\n    <com.sharry.lib.camera.SCameraView\n        android:id=\"@+id/camera_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        app:autoFocus=\"true\"\n        app:layout_constraintTop_toBottomOf=\"@id/toolbar\" />\n\n    <com.sharry.lib.album.toolbar.SToolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:statusBarStyle=\"Transparent\" />\n\n    <com.sharry.lib.album.RecorderButton\n        android:id=\"@+id/btn_record\"\n        android:layout_width=\"100dp\"\n        android:layout_height=\"100dp\"\n        android:layout_marginBottom=\"30dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n\n    <ImageView\n        android:id=\"@+id/iv_picture_preview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:scaleType=\"fitCenter\" />\n\n    <VideoView\n        android:id=\"@+id/video_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"invisible\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tool:visibility=\"visible\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/cl_ensure_panel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"150dp\"\n        android:background=\"@color/lib_album_taker_ensure_panel_bg_color\"\n        android:visibility=\"invisible\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        tool:visibility=\"visible\">\n\n        <ImageView\n            android:id=\"@+id/iv_denied\"\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            android:layout_marginRight=\"30dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toLeftOf=\"@id/iv_granted\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:srcCompat=\"@drawable/ic_album_taker_denied\" />\n\n        <ImageView\n            android:id=\"@+id/iv_granted\"\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            android:layout_marginLeft=\"30dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toRightOf=\"@+id/iv_denied\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:srcCompat=\"@drawable/ic_album_taker_granted\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/values/taker_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <color name=\"lib_album_taker_ensure_panel_bg_color\">#5E000000</color>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/values/taker_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"lib_album_taker_take_picture_failed\">Take picture failed</string>\n    <string name=\"lib_album_taker_record_time_too_short\">Record time too short</string>\n    <string name=\"lib_album_taker_record_failed\">Record failed, please try again</string>\n    <string name=\"lib_album_taker_picture_saved_failed\">Picture save failed</string>\n    <string name=\"lib_album_taker_video_saved_failed\">Video save failed</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/taker/com/sharry/lib/album/res/values-zh/taker_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"lib_album_taker_take_picture_failed\">获取照片失败</string>\n    <string name=\"lib_album_taker_record_time_too_short\">录制时间过短</string>\n    <string name=\"lib_album_taker_record_failed\">录制失败, 请稍后重试</string>\n    <string name=\"lib_album_taker_picture_saved_failed\">图片保存失败</string>\n    <string name=\"lib_album_taker_video_saved_failed\">视频保存失败</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/ActivityStateUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport android.app.Activity;\nimport android.content.pm.ActivityInfo;\nimport android.os.Build;\nimport android.util.Log;\n\nimport java.lang.reflect.Field;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 2:43 PM\n */\nclass ActivityStateUtil {\n\n    private static final String TAG = ActivityStateUtil.class.getSimpleName();\n\n    static boolean isIllegalState(Activity activity) {\n        if (activity.isFinishing() || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed())) {\n            Log.e(TAG, \"Activity in error state.\");\n            return true;\n        }\n        return false;\n    }\n\n    static void fixRequestOrientation(Activity activity) {\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {\n            try {\n                Field field = Activity.class.getDeclaredField(\"mActivityInfo\");\n                field.setAccessible(true);\n                ActivityInfo o = (ActivityInfo) field.get(activity);\n                o.screenOrientation = -1;\n                field.setAccessible(false);\n            } catch (Throwable e) {\n                // ignore.\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/CallbackFragment.java",
    "content": "package com.sharry.lib.album;\n\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.app.FragmentManager;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * 用于回调 launchActivityForResult 的过度 Fragment\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/11/30 15:46\n */\npublic class CallbackFragment extends Fragment {\n\n    public static final String TAG = CallbackFragment.class.getSimpleName();\n\n    /**\n     * 获取一个添加到 Activity 中的 Fragment 的实例\n     *\n     * @param bind The activity associated with this fragment.\n     * @return an instance of CallbackFragment.\n     */\n    @Nullable\n    public static CallbackFragment getInstance(@NonNull Activity bind) {\n        if (ActivityStateUtil.isIllegalState(bind)) {\n            return null;\n        }\n        CallbackFragment callbackFragment = findFragmentFromActivity(bind);\n        if (callbackFragment == null) {\n            callbackFragment = CallbackFragment.newInstance();\n            FragmentManager fragmentManager = bind.getFragmentManager();\n            fragmentManager.beginTransaction()\n                    .add(callbackFragment, TAG)\n                    .commitAllowingStateLoss();\n            fragmentManager.executePendingTransactions();\n        }\n        return callbackFragment;\n    }\n\n    /**\n     * 在 Activity 中通过 TAG 去寻找我们添加的 Fragment\n     */\n    private static CallbackFragment findFragmentFromActivity(@NonNull Activity activity) {\n        return (CallbackFragment) activity.getFragmentManager().findFragmentByTag(TAG);\n    }\n\n    private static CallbackFragment newInstance() {\n        return new CallbackFragment();\n    }\n\n    private Callback mCallback;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setRetainInstance(true);\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (null != mCallback) {\n            mCallback.onActivityResult(requestCode, resultCode, data);\n        }\n    }\n\n    /**\n     * 设置图片选择回调\n     */\n    public void setCallback(Callback callback) {\n        this.mCallback = callback;\n    }\n\n    /**\n     * The callback associated with this Fragment.\n     */\n    public interface Callback {\n\n        void onActivityResult(int requestCode, int resultCode, Intent data);\n\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/ColorUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.FloatRange;\n\n/**\n * 处理颜色相关的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 17:39\n */\nclass ColorUtil {\n\n    /**\n     * Get ARGB color range in [primitiveColor, destColor]\n     *\n     * @param fraction   range[0, 1]\n     * @param colorStart color associated with primitive changed\n     * @param colorDest  color associated with dest changed\n     */\n    static int gradualChanged(float fraction, @ColorInt int colorStart, @ColorInt int colorDest) {\n        // split start color.\n        float startChannelA = ((colorStart >> 24) & 0xff) / 255.0f;\n        float startChannelR = ((colorStart >> 16) & 0xff) / 255.0f;\n        float startChannelG = ((colorStart >> 8) & 0xff) / 255.0f;\n        float startChannelB = (colorStart & 0xff) / 255.0f;\n        // convert from sRGB to linear\n        startChannelR = (float) Math.pow(startChannelR, 2.2);\n        startChannelG = (float) Math.pow(startChannelG, 2.2);\n        startChannelB = (float) Math.pow(startChannelB, 2.2);\n\n        // split dest color.\n        float destChannelA = ((colorDest >> 24) & 0xff) / 255.0f;\n        float destChannelR = ((colorDest >> 16) & 0xff) / 255.0f;\n        float destChannelG = ((colorDest >> 8) & 0xff) / 255.0f;\n        float destChannelB = (colorDest & 0xff) / 255.0f;\n        destChannelR = (float) Math.pow(destChannelR, 2.2);\n        destChannelG = (float) Math.pow(destChannelG, 2.2);\n        destChannelB = (float) Math.pow(destChannelB, 2.2);\n\n        // compute the interpolated color in linear space\n        float a = startChannelA + fraction * (destChannelA - startChannelA);\n        float r = startChannelR + fraction * (destChannelR - startChannelR);\n        float g = startChannelG + fraction * (destChannelG - startChannelG);\n        float b = startChannelB + fraction * (destChannelB - startChannelB);\n        // convert back to sRGB in the [0..255] range\n        a = a * 255.0f;\n        r = (float) Math.pow(r, 1.0 / 2.2) * 255.0f;\n        g = (float) Math.pow(g, 1.0 / 2.2) * 255.0f;\n        b = (float) Math.pow(b, 1.0 / 2.2) * 255.0f;\n\n        return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);\n    }\n\n    /**\n     * 颜色透明化\n     *\n     * @param baseColor     需要更改的颜色\n     * @param alphaPercent: 0 代表全透明, 1 代表不透明\n     */\n    static int alphaColor(int baseColor, @FloatRange(from = 0f, to = 1f) float alphaPercent) {\n        if (alphaPercent > 1) {\n            alphaPercent = 1;\n        }\n        if (alphaPercent < 0) {\n            alphaPercent = 0;\n        }\n        int baseAlpha = (baseColor & 0xff000000) >>> 24;\n        int alpha = (int) (baseAlpha * alphaPercent);\n        return alpha << 24 | (baseColor & 0xffffff);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/CompressUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\nimport android.media.ExifInterface;\nimport android.text.TextUtils;\n\nimport java.io.FileDescriptor;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\n/**\n * 处理图片相关的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/18 16:23\n */\nclass CompressUtil {\n\n    /**\n     * 图片压缩\n     */\n    static void doCompress(String originPath, FileDescriptor fd, int quality) throws IOException {\n        if (TextUtils.isEmpty(originPath)) {\n            throw new IllegalArgumentException(\"CompressUtil.doCompress -> parameter originFilePath must not be null!\");\n        }\n        // 1. 邻近采样压缩尺寸(Nearest Neighbour Resampling Compress)\n        BitmapFactory.Options options = getBitmapOptions(originPath);\n        Bitmap bitmap = BitmapFactory.decodeFile(originPath, options);\n        if (bitmap == null) {\n            return;\n        }\n        // 2. 旋转一下 Bitmap\n        bitmap = rotateBitmap(bitmap, readPictureAngle(originPath));\n        // 3. 质量压缩(Quality Compress)\n        qualityCompress(bitmap, quality, fd);\n    }\n\n    /**\n     * 图片压缩\n     */\n    static void doCompress(Bitmap originBitmap, FileDescriptor fd, int quality, int desireWidth, int desireHeight) throws IOException {\n        int width = originBitmap.getWidth();\n        int height = originBitmap.getHeight();\n        float scale = Math.max(desireWidth, desireHeight) / (float) Math.max(width, height);\n        int w = Math.round(scale * width);\n        int h = Math.round(scale * height);\n        Bitmap bitmap = Bitmap.createScaledBitmap(originBitmap, w, h, true);\n        qualityCompress(bitmap, quality, fd);\n    }\n\n    /**\n     * 解析图片文件的宽高与目标宽高, 获取 Bitmap.Options\n     *\n     * @param filePath 文件路径\n     * @return 获取 Bitmap.Options\n     */\n    private static BitmapFactory.Options getBitmapOptions(String filePath) {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeFile(filePath, options);\n        options.inSampleSize = calculateSampleSize(options.outWidth, options.outHeight);\n        options.inJustDecodeBounds = false;\n        return options;\n    }\n\n    /**\n     * 根据主流屏幕自适应计算采样率\n     *\n     * @param srcWidth  原始宽度\n     * @param srcHeight 原始高度\n     * @return 采样率\n     */\n    private static int calculateSampleSize(int srcWidth, int srcHeight) {\n        //将 srcWidth 和 srcHeight 设置为偶数，方便除法计算\n        srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;\n        srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;\n\n        int longSide = Math.max(srcWidth, srcHeight);\n        int shortSide = Math.min(srcWidth, srcHeight);\n\n        float scale = ((float) shortSide / longSide);\n        if (scale <= 1 && scale > 0.5625) {\n            if (longSide < 1664) {\n                return 1;\n            } else if (longSide >= 1664 && longSide < 4990) {\n                return 2;\n            } else if (longSide > 4990 && longSide < 10240) {\n                return 4;\n            } else {\n                return longSide / 1280 == 0 ? 1 : longSide / 1280;\n            }\n        } else if (scale <= 0.5625 && scale > 0.5) {\n            return longSide / 1280 == 0 ? 1 : longSide / 1280;\n        } else {\n            return (int) Math.ceil(longSide / (1280.0 / scale));\n        }\n    }\n\n    /**\n     * Bitmap 质量压缩\n     *\n     * @param srcBitmap 原始 Bitmap\n     * @param quality   压缩质量\n     * @param fd        压缩目标的文件描述符\n     */\n    static void qualityCompress(Bitmap srcBitmap, int quality, FileDescriptor fd) throws IOException {\n        // 进行质量压缩\n        FileOutputStream out = new FileOutputStream(fd);\n        // 采用有损的 jpeg 图片压缩\n        srcBitmap.compress(Bitmap.CompressFormat.JPEG, quality, out);\n        out.flush();\n        out.close();\n    }\n\n    /**\n     * 旋转 Bitmap\n     *\n     * @param bitmap 原始 bitmap\n     * @param angle  旋转的角度\n     */\n    private static Bitmap rotateBitmap(Bitmap bitmap, int angle) {\n        if (angle == 0) {\n            return bitmap;\n        }\n        //旋转图片 动作\n        Matrix matrix = new Matrix();\n        matrix.postRotate(angle);\n        // 旋转后的 Bitmap\n        return Bitmap.createBitmap(bitmap, 0, 0,\n                bitmap.getWidth(), bitmap.getHeight(), matrix, true);\n    }\n\n    /**\n     * 读取图片文件旋转的角度\n     *\n     * @param path 文件路径\n     */\n    private static int readPictureAngle(String path) throws IOException {\n        int degree = 0;\n        ExifInterface exifInterface = new ExifInterface(path);\n        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);\n        switch (orientation) {\n            case ExifInterface.ORIENTATION_ROTATE_90:\n                degree = 90;\n                break;\n            case ExifInterface.ORIENTATION_ROTATE_180:\n                degree = 180;\n                break;\n            case ExifInterface.ORIENTATION_ROTATE_270:\n                degree = 270;\n                break;\n            default:\n                break;\n        }\n        return degree;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/Constants.java",
    "content": "package com.sharry.lib.album;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-03 17:24\n */\ninterface Constants {\n\n\n    /**\n     * Picture\n     */\n    String MIME_TYPE_JPEG = \"image/jpeg\";\n    String MIME_TYPE_PNG = \"image/png\";\n    String MIME_TYPE_WEBP = \"image/webp\";\n    String MIME_TYPE_GIF = \"image/gif\";\n\n    /**\n     * Video\n     */\n    String MIME_TYPE_MP4 = \"video/mp4\";\n    String MIME_TYPE_3GP = \"video/3gp\";\n    String MIME_TYPE_AIV = \"video/aiv\";\n    String MIME_TYPE_RMVB = \"video/rmvb\";\n    String MIME_TYPE_VOB = \"video/vob\";\n    String MIME_TYPE_FLV = \"video/flv\";\n    String MIME_TYPE_MKV = \"video/mkv\";\n    String MIME_TYPE_MOV = \"video/mov\";\n    String MIME_TYPE_MPG = \"video/mpg\";\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/DateUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-02 17:09\n */\nclass DateUtil {\n\n    private static final SimpleDateFormat HMS_FORMAT = new SimpleDateFormat(\"HH:mm:ss\", Locale.CHINA);\n    private static final SimpleDateFormat MS_FORMAT = new SimpleDateFormat(\"mm:ss\", Locale.CHINA);\n\n    static {\n        HMS_FORMAT.setTimeZone(TimeZone.getTimeZone(\"GMT+00:00\"));\n        MS_FORMAT.setTimeZone(TimeZone.getTimeZone(\"GMT+00:00\"));\n    }\n\n    static String format(long duration) {\n        if (duration < 3600000) {\n            return MS_FORMAT.format(duration);\n        } else {\n            return HMS_FORMAT.format(duration);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/DensityUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport android.content.Context;\nimport android.util.TypedValue;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-05 09:57\n */\nclass DensityUtil {\n\n    /**\n     * Dip convert 2 pixel\n     */\n    static int dp2px(Context context, float dp) {\n        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,\n                context.getResources().getDisplayMetrics());\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/FileUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport android.annotation.TargetApi;\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.media.MediaScannerConnection;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.text.format.DateFormat;\nimport android.util.Log;\n\nimport androidx.core.content.FileProvider;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Calendar;\nimport java.util.Locale;\n\n/**\n * 处理文件相关的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 17:39\n */\nclass FileUtil {\n\n    private static final String TAG = FileUtil.class.getSimpleName();\n\n    /**\n     * Get parent folder associated with this file.\n     */\n    static String getParentFolderPath(String filePath) {\n        String parentFolderPath = new File(filePath).getParentFile().getAbsolutePath();\n        if (TextUtils.isEmpty(parentFolderPath)) {\n            int end = filePath.lastIndexOf(File.separator);\n            if (end != -1) {\n                parentFolderPath = filePath.substring(0, end);\n            }\n        }\n        return parentFolderPath;\n    }\n\n    /**\n     * Get last file name associated with this path.\n     */\n    static String getLastFileName(String filePath) {\n        return filePath.substring(filePath.lastIndexOf(File.separator) + 1);\n    }\n\n    /**\n     * 获取 URI\n     */\n    static Uri getUriFromFile(Context context, String authority, File file) {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?\n                FileProvider.getUriForFile(context, authority, file) : Uri.fromFile(file);\n    }\n\n    /**\n     * 创建临时文件\n     *\n     * @return 创建的文件\n     */\n    static File createTempJpegFile(Context context) {\n        // 获取临时文件目录\n        File tempDirectory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);\n        // 创建临时文件\n        String tempFileName = \"temp_file_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + \".jpg\";\n        File tempFile = new File(tempDirectory, tempFileName);\n        try {\n            if (tempFile.exists()) {\n                tempFile.delete();\n            }\n            tempFile.createNewFile();\n            Log.i(TAG, \"create temp file directory success -> \" + tempFile.getAbsolutePath());\n        } catch (IOException e) {\n            Log.e(TAG, \"create temp file directory failed ->\" + tempFile.getAbsolutePath(), e);\n        }\n        return tempFile;\n    }\n\n    /**\n     * 创建图片路径的 URI\n     *\n     * @param relativePath 文件目录路径\n     */\n    @TargetApi(29)\n    static Uri createJpegPendingItem(Context context, String relativePath) {\n        // 创建拍照目标文件\n        String fileName = \"camera_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + \".jpg\";\n        ContentValues values = new ContentValues();\n        // 创建相对路径\n        if (TextUtils.isEmpty(relativePath)) {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);\n        } else {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + \"/\" + relativePath);\n        }\n        values.put(MediaStore.Images.Media.MIME_TYPE, \"image/jpg\");\n        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);\n        values.put(MediaStore.Images.Media.IS_PENDING, 1);\n        ContentResolver resolver = context.getContentResolver();\n        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {\n            return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);\n        } else {\n            return resolver.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);\n        }\n    }\n\n    /**\n     * 通知 MediaStore 文件发布\n     */\n    @TargetApi(29)\n    static void publishPendingItem(Context context, Uri item) {\n        if (context == null || item == null) {\n            return;\n        }\n        ContentValues values = new ContentValues();\n        values.put(MediaStore.Images.Media.IS_PENDING, 0);\n        context.getContentResolver().update(item, values, null, null);\n    }\n\n    /**\n     * 删除 Uri\n     */\n    @TargetApi(29)\n    static void delete(Context context, Uri uri) {\n        if (context != null && uri != null) {\n            context.getContentResolver().delete(uri, null, null);\n        }\n    }\n\n    /**\n     * 创建 JPEG 图片文件\n     */\n    static File createJpegFile(Context context, String relativePath) {\n        // 创建拍照目标文件\n        String fileName = \"camera_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + \".jpg\";\n        File dir = TextUtils.isEmpty(relativePath) ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)\n                : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), relativePath);\n        try {\n            // 获取默认路径\n            if (!dir.exists()) {\n                dir.mkdirs();\n            }\n            File file = new File(dir, fileName);\n            if (file.exists()) {\n                file.delete();\n            }\n            file.createNewFile();\n            Log.i(TAG, \"create jpeg file success -> \" + file.getAbsolutePath());\n            return file;\n        } catch (Throwable e) {\n            throw new UnsupportedOperationException(\"Cannot create file at:  \" + dir);\n        }\n    }\n\n    /**\n     * 通知 MediaStore 文件更替\n     */\n    static void notifyMediaStore(Context context, String filePath) {\n        if (context == null || TextUtils.isEmpty(filePath)) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            MediaScannerConnection.scanFile(context.getApplicationContext(), new String[]{filePath}, null, null);\n        } else {\n            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(\"file://\" + Environment.getExternalStorageDirectory())));\n        }\n    }\n\n    /**\n     * 删除文件\n     */\n    static void delete(Context context, File file) {\n        if (context != null && file != null && file.exists() && file.isFile()) {\n            if (file.delete()) {\n                notifyMediaStore(context, file.getAbsolutePath());\n            }\n        }\n    }\n\n    /**\n     * 根据图片 URI 获取图片路径\n     */\n    static String getImagePath(final Context context, final Uri uri) {\n        if (null == uri) {\n            return null;\n        }\n        final String scheme = uri.getScheme();\n        String data = null;\n\n        if (scheme == null) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {\n            Cursor cursor = context.getContentResolver().query(uri,\n                    new String[]{MediaStore.Images.ImageColumns.DATA}, null,\n                    null, null\n            );\n            if (null != cursor) {\n                if (cursor.moveToFirst()) {\n                    int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);\n                    if (index > -1) {\n                        data = cursor.getString(index);\n                    }\n                }\n                cursor.close();\n            }\n        }\n        return data;\n    }\n\n    /**\n     * 根据视频 URI 获取视频路径\n     */\n    static String getVideoPath(final Context context, final Uri uri) {\n        if (null == uri) {\n            return null;\n        }\n        final String scheme = uri.getScheme();\n        String data = null;\n\n        if (scheme == null) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {\n            Cursor cursor = context.getContentResolver().query(uri,\n                    new String[]{MediaStore.Video.VideoColumns.DATA}, null,\n                    null, null\n            );\n            if (null != cursor) {\n                if (cursor.moveToFirst()) {\n                    int index = cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA);\n                    if (index > -1) {\n                        data = cursor.getString(index);\n                    }\n                }\n                cursor.close();\n            }\n        }\n        return data;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/PermissionsCallback.java",
    "content": "package com.sharry.lib.album;\n\n/**\n * 权限请求的回调\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/1/5 16:22\n */\ninterface PermissionsCallback {\n\n    void onResult(boolean granted);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/PermissionsFragment.java",
    "content": "package com.sharry.lib.album;\n\nimport android.annotation.TargetApi;\nimport android.app.Fragment;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.provider.Settings;\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport android.util.Log;\n\n/**\n * 执行权限请求的 Fragment\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/1/5 16:22\n */\npublic class PermissionsFragment extends Fragment {\n\n    private static final int REQUEST_CODE_PERMISSIONS = 0x001234;\n    private static final int REQUEST_CODE_SETTING = 0x0012345;\n    private PermissionsCallback mCallback;\n    private String[] mPermissions;\n\n    public static PermissionsFragment getInstance() {\n        return new PermissionsFragment();\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setRetainInstance(true);\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    void requestPermissions(@NonNull String[] permissions, PermissionsCallback callback) {\n        mPermissions = permissions;\n        mCallback = callback;\n        requestPermissions(mPermissions, REQUEST_CODE_PERMISSIONS);\n    }\n\n    @Override\n    @TargetApi(Build.VERSION_CODES.M)\n    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\n        if (requestCode != REQUEST_CODE_PERMISSIONS) return;\n        boolean isAllGranted = true;\n        for (int i = 0, size = permissions.length; i < size; i++) {\n            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {\n                log(\"onRequestPermissionsResult: \" + permissions[i] + \" is Granted\");\n            } else {\n                log(\"onRequestPermissionsResult: \" + permissions[i] + \" is Denied\");\n                isAllGranted = false;\n            }\n        }\n        if (isAllGranted) {\n            mCallback.onResult(true);\n        } else {\n            showPermissionDeniedDialog();\n        }\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    boolean isGranted(String permission) {\n        return getActivity().checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    boolean isRevoked(String permission) {\n        return getActivity().getPackageManager().isPermissionRevokedByPolicy(permission, getActivity().getPackageName());\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    private void showPermissionDeniedDialog() {\n        //启动当前App的系统设置界面\n        AlertDialog dialog = new AlertDialog.Builder(getContext())\n                .setTitle(\"帮助\")\n                .setMessage(\"当前应用缺少必要权限\")\n                .setNegativeButton(\"取消\", new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        mCallback.onResult(false);\n                        dialog.dismiss();\n                    }\n                })\n                .setPositiveButton(\"设置\", new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        // 启动当前App的系统设置界面\n                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);\n                        intent.setData(Uri.parse(\"package:\" + getContext().getPackageName()));\n                        startActivityForResult(intent, REQUEST_CODE_SETTING);\n                    }\n                }).create();\n        dialog.setCancelable(false);\n        dialog.show();\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (requestCode != REQUEST_CODE_SETTING) return;\n        // 从设置页面回来时, 重新检测一次申请的权限是否都已经授权\n        boolean isAllGranted = true;\n        for (String permission : mPermissions) {\n            if (!isGranted(permission)) {\n                isAllGranted = false;\n                break;\n            }\n        }\n        mCallback.onResult(isAllGranted);\n    }\n\n    void log(String message) {\n        Log.i(PermissionsHelper.TAG, message);\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/PermissionsHelper.java",
    "content": "package com.sharry.lib.album;\n\nimport android.app.Activity;\nimport android.app.FragmentManager;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.Log;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 权限请求管理类\n * Thanks RxPermissions\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/1/5 16:23\n */\nclass PermissionsHelper {\n\n    public static final String TAG = PermissionsHelper.class.getSimpleName();\n    private PermissionsFragment mPermissionsFragment;\n    private String[] mPermissions;\n\n    public static PermissionsHelper with(Context context) {\n        if (context instanceof Activity) {\n            Activity activity = (Activity) context;\n            return new PermissionsHelper(activity);\n        } else {\n            throw new IllegalArgumentException(\"PermissionsUtil.with -> Context can not cast to Activity\");\n        }\n    }\n\n    private PermissionsHelper(Activity activity) {\n        mPermissionsFragment = getPermissionsFragment(activity);\n    }\n\n    /**\n     * 添加需要请求的权限\n     */\n    PermissionsHelper request(String... permissions) {\n        return requestArray(permissions);\n    }\n\n    /**\n     * 添加需要请求的权限(Kotlin 不支持从不定长参数转为 Array)\n     */\n    PermissionsHelper requestArray(String[] permissions) {\n        ensure(permissions);\n        mPermissions = permissions;\n        return this;\n    }\n\n    /**\n     * 执行权限请求\n     */\n    void execute(PermissionsCallback permissionsCallback) {\n        if (permissionsCallback == null) {\n            throw new IllegalArgumentException(\"PermissionsUtil.execute -> PermissionsCallback must not be null\");\n        }\n        executeActual(mPermissions, permissionsCallback);\n    }\n\n    /**\n     * 判断权限是否被授权\n     */\n    boolean isGranted(String permission) {\n        return !isMarshmallow() || (mPermissionsFragment != null && mPermissionsFragment.isGranted(permission));\n    }\n\n    /**\n     * 判断权限是否被撤回\n     * <p>\n     * Always false if SDK &lt; 23.\n     */\n    @SuppressWarnings(\"WeakerAccess\")\n    boolean isRevoked(String permission) {\n        return isMarshmallow() && (mPermissionsFragment != null && mPermissionsFragment.isRevoked(permission));\n    }\n\n    /**\n     * 获取 PermissionsFragment\n     */\n    private PermissionsFragment getPermissionsFragment(Activity activity) {\n        if (ActivityStateUtil.isIllegalState(activity)) {\n            return null;\n        }\n        PermissionsFragment permissionsFragment = findPermissionsFragment(activity);\n        if (permissionsFragment == null) {\n            permissionsFragment = PermissionsFragment.getInstance();\n            FragmentManager fragmentManager = activity.getFragmentManager();\n            fragmentManager.beginTransaction().add(permissionsFragment, TAG).commitAllowingStateLoss();\n            fragmentManager.executePendingTransactions();\n        }\n        return permissionsFragment;\n    }\n\n    /**\n     * 在 Activity 中通过 TAG 去寻找我们添加的 Fragment\n     */\n    private PermissionsFragment findPermissionsFragment(Activity activity) {\n        return (PermissionsFragment) activity.getFragmentManager().findFragmentByTag(TAG);\n    }\n\n\n    /**\n     * 验证发起请求的权限是否有效\n     */\n    private void ensure(String[] permissions) {\n        if (permissions == null || permissions.length == 0) {\n            throw new IllegalArgumentException(\"PermissionsUtil.request -> requestEach requires at least one input permission\");\n        }\n    }\n\n    /**\n     * 执行权限请求\n     */\n    private void executeActual(String[] permissions, PermissionsCallback callback) {\n        if (mPermissionsFragment == null) {\n            Log.e(TAG, \"Request failed.\");\n            callback.onResult(false);\n            return;\n        }\n        List<String> unrequestedPermissions = new ArrayList<>();\n        for (String permission : permissions) {\n            Log.i(TAG, \"Requesting permission -> \" + permission);\n            if (isGranted(permission)) {\n                // Already granted, or not Android M\n                // Return a granted Permission object.\n                continue;\n            }\n            if (isRevoked(permission)) {\n                // Revoked by a policy, return a denied Permission object.\n                continue;\n            }\n            unrequestedPermissions.add(permission);\n        }\n        if (!unrequestedPermissions.isEmpty()) {\n            // 细节, toArray的时候指定了数组的长度\n            String[] unrequestedPermissionsArray = unrequestedPermissions.toArray(\n                    new String[unrequestedPermissions.size()]);\n            mPermissionsFragment.requestPermissions(unrequestedPermissionsArray, callback);\n        } else {\n            callback.onResult(true);\n        }\n    }\n\n    private boolean isMarshmallow() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/Preconditions.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\n\nimport java.util.Collection;\n\n/**\n * Contains common assertions.\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 3/29/2019 2:14 PM\n */\nfinal class Preconditions {\n\n    private Preconditions() {\n        // Utility class.\n    }\n\n    public static void checkArgument(boolean expression, @NonNull String message) {\n        if (!expression) {\n            throw new IllegalArgumentException(message);\n        }\n    }\n\n    @NonNull\n    static <T> T checkNotNull(@Nullable T arg) {\n        return checkNotNull(arg, \"Argument must not be null\");\n    }\n\n    @NonNull\n    static <T> T checkNotNull(@Nullable T arg, @NonNull String message) {\n        if (arg == null) {\n            throw new NullPointerException(message);\n        }\n        return arg;\n    }\n\n    @NonNull\n    static String checkNotEmpty(@Nullable String string) {\n        if (TextUtils.isEmpty(string)) {\n            throw new IllegalArgumentException(\"Must not be null or empty\");\n        }\n        return string;\n    }\n\n    @NonNull\n    static <T extends Collection<Y>, Y> T checkNotEmpty(@NonNull T collection) {\n        if (collection.isEmpty()) {\n            throw new IllegalArgumentException(\"Must not be empty.\");\n        }\n        return collection;\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/SharedElementHelper.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.animation.TypeEvaluator;\nimport android.graphics.Matrix;\nimport android.graphics.Path;\nimport android.graphics.PathMeasure;\nimport android.graphics.PointF;\nimport android.graphics.drawable.Drawable;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.Property;\nimport android.view.View;\nimport android.view.animation.DecelerateInterpolator;\nimport android.view.animation.OvershootInterpolator;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.util.HashMap;\n\n/**\n * Picture watcher shared elements jump helper.\n * <p>\n * Thanks for google framework sources, {@link android.transition.ChangeBounds}\n * and {@link android.transition.ChangeImageTransform}\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 3/18/2019 11:13 AM\n */\nclass SharedElementHelper {\n\n    /**\n     * Key is position in PictureLists\n     * <p>\n     * Value is view bounds.\n     */\n    static HashMap<Integer, Bounds> CACHES = new HashMap<>();\n\n    private static Property<ImageView, Matrix> ANIMATED_IMAGE_MATRIX_PROPERTY\n            = new Property<ImageView, Matrix>(Matrix.class, \"setImageMatrix\") {\n        @Override\n        public void set(ImageView imageView, Matrix matrix) {\n            imageView.setImageMatrix(matrix);\n        }\n\n        @Override\n        public Matrix get(ImageView object) {\n            return null;\n        }\n    };\n\n    private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =\n            new Property<ViewBounds, PointF>(PointF.class, \"topLeft\") {\n                @Override\n                public void set(ViewBounds viewBounds, PointF topLeft) {\n                    viewBounds.setTopLeft(topLeft);\n                }\n\n                @Override\n                public PointF get(ViewBounds viewBounds) {\n                    return null;\n                }\n            };\n\n    private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =\n            new Property<ViewBounds, PointF>(PointF.class, \"bottomRight\") {\n                @Override\n                public void set(ViewBounds viewBounds, PointF bottomRight) {\n                    viewBounds.setBottomRight(bottomRight);\n                }\n\n                @Override\n                public PointF get(ViewBounds viewBounds) {\n                    return null;\n                }\n            };\n\n    /**\n     * Create enter animator for PictureWatcher.\n     *\n     * @param target exchange target.\n     * @param data   origin data.\n     */\n    static Animator createSharedElementEnterAnimator(View target, Bounds data) {\n        int[] locations = new int[2];\n        target.getLocationOnScreen(locations);\n        target.setPivotX(0);\n        target.setPivotY(0);\n        AnimatorSet enterAnimators = new AnimatorSet();\n        enterAnimators.playTogether(\n                ObjectAnimator.ofFloat(target, \"scaleX\", data.width / (float) target.getWidth(), 1f),\n                ObjectAnimator.ofFloat(target, \"scaleY\", data.height / (float) target.getHeight(), 1f),\n                ObjectAnimator.ofFloat(target, \"translationX\", data.startX - locations[0], 0),\n                ObjectAnimator.ofFloat(target, \"translationY\", data.startY - locations[1], 0)\n        );\n        enterAnimators.setInterpolator(new OvershootInterpolator(1f));\n        enterAnimators.setDuration(300);\n        return enterAnimators;\n    }\n\n    /**\n     * Create exit animator for PictureWatcher.\n     *\n     * @param target exchange target.\n     * @param data   origin data.\n     */\n    static Animator createSharedElementExitAnimator(@Nullable ImageView target, Bounds data) {\n        if (target == null) {\n            return null;\n        }\n        Drawable drawable = target.getDrawable();\n        if (drawable == null) {\n            return null;\n        }\n        AnimatorSet exitAnimators = new AnimatorSet();\n        // 设置尺寸动画 ChangeBounds\n        AnimatorSet boundsAnim = getBoundsChangedAnim(target, data);\n        // 设置缩放动画\n        final ObjectAnimator matrixAnim = ObjectAnimator.ofObject(target, ANIMATED_IMAGE_MATRIX_PROPERTY,\n                new MatrixEvaluator(),\n                target.getImageMatrix(),\n                centerCropMatrix(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),\n                        data.width, data.height)\n        );\n        exitAnimators.playTogether(boundsAnim, matrixAnim);\n        exitAnimators.setDuration(400);\n        exitAnimators.setInterpolator(new DecelerateInterpolator());\n        return exitAnimators;\n    }\n\n    /**\n     * Create bounds changed animator for dismissOtherView animator.\n     *\n     * @param target exchange target.\n     * @param data   origin data.\n     */\n    private static AnimatorSet getBoundsChangedAnim(View target, Bounds data) {\n        final ViewBounds viewBounds = new ViewBounds(target);\n        int[] locations = new int[2];\n        target.getLocationOnScreen(locations);\n        target.setPivotX(0);\n        // 构建左上的位移动画\n        Path topLeftPath = new Path();\n        topLeftPath.moveTo(target.getLeft(), target.getTop());\n        topLeftPath.lineTo(data.startX - locations[0], data.startY - locations[1]);\n        ObjectAnimator topLeftAnimator = ObjectAnimator.ofFloat(viewBounds,\n                new PathProperty<>(TOP_LEFT_PROPERTY, topLeftPath), 0f, 1f);\n        // 构建右下的位移动画\n        Path bottomRightPath = new Path();\n        bottomRightPath.moveTo(target.getBottom(), target.getHeight());\n        bottomRightPath.lineTo(data.startX + data.width - locations[0],\n                data.startY + data.height - locations[1]);\n        ObjectAnimator bottomRightAnimator = ObjectAnimator.ofFloat(viewBounds,\n                new PathProperty<>(BOTTOM_RIGHT_PROPERTY, bottomRightPath), 0f, 1f);\n        // 构建动画集合\n        AnimatorSet set = new AnimatorSet();\n        set.playTogether(topLeftAnimator, bottomRightAnimator);\n        return set;\n    }\n\n    private static class ViewBounds {\n\n        private int mLeft;\n        private int mTop;\n        private int mRight;\n        private int mBottom;\n        private View mView;\n        private int mTopLeftCalls;\n        private int mBottomRightCalls;\n\n        ViewBounds(View view) {\n            mView = view;\n        }\n\n        void setTopLeft(PointF topLeft) {\n            mLeft = Math.round(topLeft.x);\n            mTop = Math.round(topLeft.y);\n            mTopLeftCalls++;\n            if (mTopLeftCalls == mBottomRightCalls) {\n                setLeftTopRightBottom();\n            }\n        }\n\n        void setBottomRight(PointF bottomRight) {\n            mRight = Math.round(bottomRight.x);\n            mBottom = Math.round(bottomRight.y);\n            mBottomRightCalls++;\n            if (mTopLeftCalls == mBottomRightCalls) {\n                setLeftTopRightBottom();\n            }\n        }\n\n        private void setLeftTopRightBottom() {\n            mView.setLeft(mLeft);\n            mView.setTop(mTop);\n            mView.setRight(mRight);\n            mView.setBottom(mBottom);\n            mTopLeftCalls = 0;\n            mBottomRightCalls = 0;\n        }\n\n    }\n\n    private static class PathProperty<T> extends Property<T, Float> {\n\n        private final Property<T, PointF> mProperty;\n        private final PathMeasure mPathMeasure;\n        private final float mPathLength;\n        private final float[] mPosition = new float[2];\n        private final PointF mPointF = new PointF();\n        private float mCurrentFraction;\n\n        PathProperty(Property<T, PointF> property, Path path) {\n            super(Float.class, property.getName());\n            mProperty = property;\n            mPathMeasure = new PathMeasure(path, false);\n            mPathLength = mPathMeasure.getLength();\n        }\n\n        @Override\n        public Float get(T object) {\n            return mCurrentFraction;\n        }\n\n        @Override\n        public void set(T target, Float fraction) {\n            mCurrentFraction = fraction;\n            mPathMeasure.getPosTan(mPathLength * fraction, mPosition, null);\n            mPointF.x = mPosition[0];\n            mPointF.y = mPosition[1];\n            mProperty.set(target, mPointF);\n        }\n\n    }\n\n    /**\n     * Calculates the image transformation matrix for an ImageView with ScaleType CENTER_CROP. This\n     * needs to be manually calculated for consistent behavior across all the API levels.\n     */\n    private static Matrix centerCropMatrix(int startWidth, int startHeight, int destWidth, int destHeight) {\n        final float scaleX = ((float) destWidth) / startWidth;\n        final float scaleY = ((float) destHeight) / startHeight;\n\n        final float maxScale = Math.max(scaleX, scaleY);\n        final float width = startWidth * maxScale;\n        final float height = startHeight * maxScale;\n        final int tx = Math.round((destWidth - width) / 2f);\n        final int ty = Math.round((destHeight - height) / 2f);\n\n        final Matrix matrix = new Matrix();\n        matrix.postScale(maxScale, maxScale);\n        matrix.postTranslate(tx, ty);\n        return matrix;\n    }\n\n    /**\n     * The evaluator associated with matrix animator.\n     */\n    public static class MatrixEvaluator implements TypeEvaluator<Matrix> {\n\n        float[] mTempStartValues = new float[9];\n\n        float[] mTempEndValues = new float[9];\n\n        Matrix mTempMatrix = new Matrix();\n\n        @Override\n        public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {\n            startValue.getValues(mTempStartValues);\n            endValue.getValues(mTempEndValues);\n            for (int i = 0; i < 9; i++) {\n                float diff = mTempEndValues[i] - mTempStartValues[i];\n                mTempEndValues[i] = mTempStartValues[i] + (fraction * diff);\n            }\n            mTempMatrix.setValues(mTempEndValues);\n            return mTempMatrix;\n        }\n    }\n\n    /**\n     * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n     * @version 1.0\n     * @since 3/15/2019 3:27 PM\n     */\n    static class Bounds implements Parcelable {\n\n        static Bounds parseFrom(@NonNull View sharedElement, int positionInPictures) {\n            Bounds result = new Bounds();\n            int[] locations = new int[2];\n            sharedElement.getLocationOnScreen(locations);\n            result.startX = locations[0];\n            result.startY = locations[1];\n            result.width = sharedElement.getWidth();\n            result.height = sharedElement.getHeight();\n            result.position = positionInPictures;\n            return result;\n        }\n\n        int startX;\n        int startY;\n        int width;\n        int height;\n        int position;\n\n        private Bounds() {\n\n        }\n\n        Bounds(Parcel in) {\n            startX = in.readInt();\n            startY = in.readInt();\n            width = in.readInt();\n            height = in.readInt();\n            position = in.readInt();\n        }\n\n        @Override\n        public void writeToParcel(Parcel dest, int flags) {\n            dest.writeInt(startX);\n            dest.writeInt(startY);\n            dest.writeInt(width);\n            dest.writeInt(height);\n            dest.writeInt(position);\n        }\n\n        @Override\n        public int describeContents() {\n            return 0;\n        }\n\n        @Override\n        public String toString() {\n            return \"SharedElementModel{\" +\n                    \"startX=\" + startX +\n                    \", startY=\" + startY +\n                    \", width=\" + width +\n                    \", height=\" + height +\n                    \", sharedPosition='\" + position + '\\'' +\n                    '}';\n        }\n\n        public static final Creator<Bounds> CREATOR = new Creator<Bounds>() {\n            @Override\n            public Bounds createFromParcel(Parcel in) {\n                return new Bounds(in);\n            }\n\n            @Override\n            public Bounds[] newArray(int size) {\n                return new Bounds[size];\n            }\n        };\n\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/utils/com/sharry/lib/album/VersionUtil.java",
    "content": "package com.sharry.lib.album;\n\nimport android.os.Build;\n\n/**\n * 版本控制相关的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 17:46\n */\nclass VersionUtil {\n\n    static boolean isJellyBeanMr1() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;\n    }\n\n    static boolean isLollipop() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;\n    }\n\n    static boolean isQ() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/DisplayAdapter.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentStatePagerAdapter;\n\nimport java.util.List;\n\n/**\n * ViewPager 嵌套 Fragment 组合的 Adapter\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2017/2/20 9:10\n */\nclass DisplayAdapter extends FragmentStatePagerAdapter {\n\n    private final List<? extends MediaMeta> mDataSet;\n\n    DisplayAdapter(FragmentManager fragmentManager, List<? extends MediaMeta> dataSet) {\n        super(fragmentManager);\n        this.mDataSet = dataSet;\n    }\n\n    @Override\n    public WatcherFragment getItem(int position) {\n        WatcherFragment watcherFragment = WatcherFragment.getInstance(position);\n        watcherFragment.setDataSource(mDataSet.get(position));\n        return WatcherFragment.getInstance(position);\n    }\n\n    @Override\n    public int getItemPosition(@NonNull Object object) {\n        return POSITION_NONE;\n    }\n\n    @Override\n    public int getCount() {\n        return mDataSet.size();\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/PickedPanelAdapter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.ArrayList;\n\n/**\n * 选中视图预览页面的 Adapter\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 23:23\n */\nclass PickedPanelAdapter extends RecyclerView.Adapter<PickedPanelAdapter.ViewHolder> {\n\n    private final ArrayList<MediaMeta> userPickedSet;\n    private final Interaction interaction;\n\n    PickedPanelAdapter(ArrayList<MediaMeta> userPickedSet, Interaction interaction) {\n        this.userPickedSet = userPickedSet;\n        this.interaction = interaction;\n    }\n\n    @NonNull\n    @Override\n    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        ImageView iv = new ImageView(parent.getContext());\n        int size = parent.getHeight();\n        iv.setScaleType(ImageView.ScaleType.CENTER_CROP);\n        iv.setLayoutParams(new ViewGroup.LayoutParams(size, size));\n        iv.setPadding(size / 20, size / 20, size / 20, size / 20);\n        return new ViewHolder(iv);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {\n        Loader.loadPicture(holder.ivPicture.getContext(), userPickedSet.get(position), holder.ivPicture);\n    }\n\n    @Override\n    public int getItemCount() {\n        return userPickedSet.size();\n    }\n\n    public interface Interaction {\n\n        void onPreviewItemClicked(ImageView imageView, MediaMeta meta, int position);\n\n    }\n\n    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {\n\n        final ImageView ivPicture;\n\n        ViewHolder(View itemView) {\n            super(itemView);\n            this.ivPicture = (ImageView) itemView;\n            ivPicture.setOnClickListener(this);\n        }\n\n        @Override\n        public void onClick(View v) {\n            int position = getAdapterPosition();\n            interaction.onPreviewItemClicked((ImageView) v, userPickedSet.get(position), position);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherActivity.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ObjectAnimator;\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.content.Intent;\nimport android.graphics.Color;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.ViewTreeObserver;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.sharry.lib.album.photoview.PhotoView;\nimport com.sharry.lib.album.toolbar.SToolbar;\nimport com.sharry.lib.album.toolbar.ViewOptions;\n\nimport java.util.ArrayList;\n\nimport static com.sharry.lib.album.ActivityStateUtil.fixRequestOrientation;\n\n/**\n * 图片查看器的 Activity, 主题设置为背景透明效果更佳\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.3\n * @since 2018/9/22 23:24\n */\npublic class WatcherActivity extends AppCompatActivity implements\n        WatcherContract.IView,\n        DraggableViewPager.Callback,\n        PickedPanelAdapter.Interaction {\n\n    private static final String EXTRA_SHARED_ELEMENT = \"start_intent_extra_shared_element\";\n    static final int REQUEST_CODE = 508;\n\n    static final String BROADCAST_PICKED_SET_CHANGED = \"com.sharry.lib.album.watcheractivity.broadcast.picked.set.changed\";\n    static final String BROADCAST_PICKED_SET_ENSURE = \"com.sharry.lib.album.watcheractivity.broadcast.picked.set.ensure\";\n    static final String BROADCAST_EXTRA_DATA = \"BROADCAST_EXTRA_DATA\";\n\n    /**\n     * 图片浏览器的配置, 使用静态变量暂存数据\n     * <p>\n     * 防止图片资源超过 1MB - 8k, 导致 Binder 驱动无法传值\n     */\n    private static WatcherConfig sConfig;\n\n    /**\n     * U can launch this activity from here.\n     *\n     * @param request       请求的 Activity\n     * @param resultTo      WatcherActivity 返回值的去向\n     * @param config        WatcherActivity 的配置\n     * @param sharedElement 共享元素\n     */\n    static void launchActivityForResult(@NonNull Activity request, @NonNull Fragment resultTo,\n                                        @NonNull WatcherConfig config, @Nullable View sharedElement) {\n        // 暂存 Config\n        sConfig = config;\n        Intent intent = new Intent(request, WatcherActivity.class);\n        if (sharedElement != null) {\n            intent.putExtra(\n                    WatcherActivity.EXTRA_SHARED_ELEMENT,\n                    SharedElementHelper.Bounds.parseFrom(sharedElement, config.getPosition())\n            );\n        }\n        resultTo.startActivityForResult(intent, REQUEST_CODE);\n        if (sharedElement != null) {\n            request.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n        }\n    }\n\n    /**\n     * The presenter for the view.\n     */\n    private WatcherContract.IPresenter mPresenter;\n\n    /**\n     * Widgets for this Activity.\n     */\n    private TextView mTvTitle;\n    private PhotoView mIvPlaceHolder;\n    private CheckedIndicatorView mCheckIndicator;\n    private DraggableViewPager mDisplayPager;\n    private DisplayAdapter mDisplayAdapter;\n    private LinearLayout mLlPickedPanelContainer;\n    private RecyclerView mRvPickedPanel;\n    private TextView mTvEnsure;\n\n    /**\n     * The animator for bottom preview.\n     */\n    private ObjectAnimator mBottomPreviewShowAnimator;\n    private ObjectAnimator mBottomPreviewDismissAnimator;\n\n    ////////////////////////////////////////// Lifecycle. /////////////////////////////////////////////\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        fixRequestOrientation(this);\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.lib_album_activity_watcher);\n        initTitle();\n        initViews();\n        initPresenter();\n    }\n\n    private void initTitle() {\n        SToolbar toolbar = findViewById(R.id.toolbar);\n        mTvTitle = toolbar.getTitleText();\n        // 添加右部的索引\n        mCheckIndicator = new CheckedIndicatorView(this);\n        toolbar.addRightMenuView(mCheckIndicator, new ViewOptions.Builder()\n                .setVisibility(View.INVISIBLE)\n                .setWidthExcludePadding(DensityUtil.dp2px(this, 25))\n                .setHeightExcludePadding(DensityUtil.dp2px(this, 25))\n                .setPaddingRight(DensityUtil.dp2px(this, 10))\n                .setListener(new View.OnClickListener() {\n                    @Override\n                    public void onClick(View v) {\n                        mPresenter.handleIndicatorClick(mCheckIndicator.isChecked());\n                    }\n                })\n                .build());\n    }\n\n    private void initViews() {\n        // 占位图\n        mIvPlaceHolder = findViewById(R.id.iv_se_place_holder);\n        // 1. 初始化 ViewPager\n        mDisplayPager = findViewById(R.id.view_pager);\n        mDisplayPager.setCallback(this);\n        mDisplayPager.setBackgroundColorRes(R.color.lib_album_watcher_bg_color);\n        // 2. 初始化底部菜单\n        mLlPickedPanelContainer = findViewById(R.id.ll_picked_panel_container);\n        mRvPickedPanel = findViewById(R.id.rv_picked_panel);\n        mRvPickedPanel.setLayoutManager(new LinearLayoutManager(this,\n                LinearLayoutManager.HORIZONTAL, false));\n        mTvEnsure = findViewById(R.id.tv_ensure);\n        mTvEnsure.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                mPresenter.handleEnsureClicked();\n            }\n        });\n    }\n\n    private void initPresenter() {\n        mPresenter = new WatcherPresenter(\n                this,\n                sConfig,\n                ((SharedElementHelper.Bounds) getIntent().getParcelableExtra(EXTRA_SHARED_ELEMENT))\n        );\n    }\n\n    @Override\n    public void onBackPressed() {\n        SharedElementHelper.Bounds exitData = mPresenter.getExitSharedElement();\n        // 使用动画退出\n        if (exitData != null) {\n            showSharedElementExitAndFinish(exitData);\n            dismissPickedPanel();\n        }\n        // 直接退出\n        else {\n            super.onBackPressed();\n        }\n    }\n\n    @Override\n    public void finish() {\n        setResult(RESULT_OK);\n        super.finish();\n        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        sConfig = null;\n        WatcherFragment.ACTIVES.clear();\n        WatcherFragment.IDLES.clear();\n    }\n\n    //////////////////////////////////////////////WatcherContract.IView/////////////////////////////////////////////////\n\n    @Override\n    public void showSharedElementEnter(@NonNull MediaMeta mediaMeta, @NonNull final SharedElementHelper.Bounds data) {\n        // 加载共享元素占位图\n        mIvPlaceHolder.setVisibility(View.VISIBLE);\n        Loader.loadPicture(this, mediaMeta, mIvPlaceHolder);\n        // 执行共享元素\n        mIvPlaceHolder.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {\n            @Override\n            public boolean onPreDraw() {\n                mIvPlaceHolder.getViewTreeObserver().removeOnPreDrawListener(this);\n                // Execute enter animator.\n                Animator startAnim = SharedElementHelper.createSharedElementEnterAnimator(mIvPlaceHolder, data);\n                startAnim.addListener(new AnimatorListenerAdapter() {\n                    @Override\n                    public void onAnimationEnd(Animator animation) {\n                        mIvPlaceHolder.setVisibility(View.GONE);\n                    }\n                });\n                startAnim.start();\n                return true;\n            }\n        });\n    }\n\n    @Override\n    public void showSharedElementExitAndFinish(@NonNull SharedElementHelper.Bounds data) {\n        final WatcherFragment watcherFragment = mDisplayAdapter.getItem(data.position);\n        final PhotoView target = watcherFragment.getPhotoView();\n        Animator exitAnim = SharedElementHelper.createSharedElementExitAnimator(target, data);\n        if (exitAnim == null) {\n            this.finish();\n            return;\n        }\n        exitAnim.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationStart(Animator animation) {\n                watcherFragment.dismissOtherView();\n                mDisplayPager.setBackgroundColor(Color.TRANSPARENT);\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                WatcherActivity.this.finish();\n            }\n        });\n        exitAnim.start();\n    }\n\n    @Override\n    public void setLeftTitleText(@NonNull CharSequence content) {\n        mTvTitle.setText(content);\n    }\n\n    @Override\n    public void setIndicatorText(@NonNull CharSequence indicatorText) {\n        mCheckIndicator.setText(indicatorText);\n    }\n\n    @Override\n    public void setIndicatorColors(int indicatorBorderCheckedColor, int indicatorBorderUncheckedColor,\n                                   int indicatorSolidColor, int indicatorTextColor) {\n        mCheckIndicator.setBorderColor(indicatorBorderCheckedColor, indicatorBorderUncheckedColor);\n        mCheckIndicator.setSolidColor(indicatorSolidColor);\n        mCheckIndicator.setTextColor(indicatorTextColor);\n    }\n\n    @Override\n    public void setIndicatorVisible(boolean isShowCheckedIndicator) {\n        mCheckIndicator.setVisibility(isShowCheckedIndicator ? View.VISIBLE : View.GONE);\n    }\n\n    @Override\n    public void setIndicatorChecked(boolean isChecked) {\n        mCheckIndicator.setChecked(isChecked);\n    }\n\n    @Override\n    public void setEnsureText(@NonNull CharSequence content) {\n        mTvEnsure.setText(content);\n    }\n\n    @Override\n    public void setDisplayAdapter(@NonNull ArrayList<MediaMeta> items) {\n        mDisplayAdapter = new DisplayAdapter(getSupportFragmentManager(), items);\n        mDisplayPager.setAdapter(mDisplayAdapter);\n    }\n\n    @Override\n    public void displayAt(int position) {\n        mDisplayPager.setCurrentItem(position);\n    }\n\n    @Override\n    public void setPickedAdapter(@NonNull ArrayList<MediaMeta> pickedSet) {\n        mRvPickedPanel.setAdapter(new PickedPanelAdapter(pickedSet, this));\n    }\n\n    @Override\n    public void notifyItemRemoved(@NonNull MediaMeta removedMeta, int removedIndex) {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvPickedPanel.getAdapter()) != null) {\n            adapter.notifyItemRemoved(removedIndex);\n        }\n        Intent data = new Intent(BROADCAST_PICKED_SET_CHANGED);\n        data.putExtra(BROADCAST_EXTRA_DATA, removedMeta);\n        LocalBroadcastManager.getInstance(this).sendBroadcast(data);\n    }\n\n    @Override\n    public void notifyItemPicked(@NonNull MediaMeta addedMeta, int addedIndex) {\n        RecyclerView.Adapter adapter;\n        if ((adapter = mRvPickedPanel.getAdapter()) != null) {\n            adapter.notifyItemInserted(addedIndex);\n        }\n        Intent data = new Intent(BROADCAST_PICKED_SET_CHANGED);\n        data.putExtra(BROADCAST_EXTRA_DATA, addedMeta);\n        LocalBroadcastManager.getInstance(this).sendBroadcast(data);\n    }\n\n    @Override\n    public void showPickedPanel() {\n        if (mLlPickedPanelContainer.getVisibility() == View.VISIBLE) {\n            return;\n        }\n        if (mBottomPreviewShowAnimator == null) {\n            mBottomPreviewShowAnimator = ObjectAnimator.ofFloat(mLlPickedPanelContainer,\n                    \"translationY\", mLlPickedPanelContainer.getHeight(), 0);\n            mBottomPreviewShowAnimator.setDuration(200);\n            mBottomPreviewShowAnimator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    mLlPickedPanelContainer.setVisibility(View.VISIBLE);\n                }\n            });\n        }\n        mBottomPreviewShowAnimator.start();\n    }\n\n    @Override\n    public void dismissPickedPanel() {\n        if (mLlPickedPanelContainer.getVisibility() == View.INVISIBLE) {\n            return;\n        }\n        if (mBottomPreviewDismissAnimator == null) {\n            mBottomPreviewDismissAnimator = ObjectAnimator.ofFloat(mLlPickedPanelContainer,\n                    \"translationY\", 0, mLlPickedPanelContainer.getHeight());\n            mBottomPreviewDismissAnimator.setDuration(200);\n            mBottomPreviewDismissAnimator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    mLlPickedPanelContainer.setVisibility(View.INVISIBLE);\n                }\n            });\n        }\n        mBottomPreviewDismissAnimator.start();\n    }\n\n    @Override\n    public void pickedPanelSmoothScrollToPosition(int position) {\n        mRvPickedPanel.smoothScrollToPosition(position);\n    }\n\n    @Override\n    public void showMsg(@NonNull String msg) {\n        Snackbar.make(mRvPickedPanel, msg, Snackbar.LENGTH_LONG).show();\n    }\n\n    @Override\n    public void sendEnsureBroadcast() {\n        Intent data = new Intent(BROADCAST_PICKED_SET_ENSURE);\n        LocalBroadcastManager.getInstance(this).sendBroadcast(data);\n    }\n\n    ////////////////////////////////////////// DraggableViewPager.Callback /////////////////////////////////////////////\n\n    @Override\n    public void onPagerChanged(int position) {\n        if (mPresenter != null) {\n            mPresenter.handlePagerChanged(position);\n        }\n    }\n\n    @Override\n    public boolean handleDismissAction() {\n        return mPresenter.handleDisplayPagerDismiss();\n    }\n\n    @Override\n    public void onDismissed() {\n        finish();\n    }\n\n    ////////////////////////////////////////// PickedAdapter.Interaction /////////////////////////////////////////////\n\n    @Override\n    public void onPreviewItemClicked(ImageView imageView, MediaMeta meta, int position) {\n        mPresenter.handlePickedItemClicked(meta);\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherCallback.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Sharry on 2018/6/13.\n * Email: SharryChooCHN@Gmail.com\n * Version: 1.0\n * Description: 图片选择器的回调\n */\npublic interface WatcherCallback {\n\n    WatcherCallback DEFAULT = new WatcherCallback() {\n\n        @Override\n        public void onWatcherPickedComplete(@NonNull ArrayList<MediaMeta> pickedSet) {\n\n        }\n\n        @Override\n        public void onWatcherPickedFailed() {\n\n        }\n    };\n\n    /**\n     * The callback method will call when pick picture from watcher complete.\n     */\n    void onWatcherPickedComplete(@NonNull ArrayList<MediaMeta> pickedSet);\n\n    /**\n     * DO nothing at Watcher page.\n     */\n    void onWatcherPickedFailed();\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherCallbackLambda.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n/**\n * Watcher Picker callback.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2020-01-06 16:58\n */\npublic interface WatcherCallbackLambda {\n\n    void onWatcherPicked(@Nullable ArrayList<MediaMeta> pickedSet);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherConfig.java",
    "content": "package com.sharry.lib.album;\n\nimport android.graphics.Color;\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n/**\n * 图片查看器相关的配置\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 17:56\n */\npublic class WatcherConfig {\n\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    private static final int INVALIDATE = -1;\n\n    /**\n     * 需要展示的集合\n     */\n    private ArrayList<MediaMeta> mediaMetas;\n\n    /**\n     * 阈值\n     * <p>\n     * 若为 {@link #INVALIDATE}, 则不提供图片选取功能\n     */\n    private int threshold = INVALIDATE;\n\n    /**\n     * 用户已图片选中的集合\n     * <p>\n     * 在 {@code threshold != INVALIDATE} 时生效\n     */\n    private ArrayList<MediaMeta> userPickedSet;\n\n    /**\n     * 指示器背景色\n     */\n    private int indicatorTextColor = Color.WHITE;\n\n    /**\n     * 指示器选中的填充色\n     */\n    private int indicatorSolidColor = Color.parseColor(\"#ff64b6f6\");\n\n    /**\n     * 指示器边框选中的颜色\n     */\n    private int indicatorBorderCheckedColor = indicatorSolidColor;\n\n    /**\n     * 指示器边框未被选中的颜色\n     */\n    private int indicatorBorderUncheckedColor = Color.WHITE;\n\n    /**\n     * 定位展示的位置\n     */\n    private int position;\n\n    public WatcherConfig() {\n    }\n\n    @NonNull\n    public ArrayList<MediaMeta> getPictureUris() {\n        return mediaMetas;\n    }\n\n    @Nullable\n    public ArrayList<MediaMeta> getUserPickedSet() {\n        return userPickedSet;\n    }\n\n    public int getThreshold() {\n        return threshold;\n    }\n\n    public int getIndicatorTextColor() {\n        return indicatorTextColor;\n    }\n\n    public int getIndicatorSolidColor() {\n        return indicatorSolidColor;\n    }\n\n    public int getIndicatorBorderCheckedColor() {\n        return indicatorBorderCheckedColor;\n    }\n\n    public int getIndicatorBorderUncheckedColor() {\n        return indicatorBorderUncheckedColor;\n    }\n\n    public int getPosition() {\n        return position;\n    }\n\n    public boolean isPickerSupport() {\n        return threshold != INVALIDATE;\n    }\n\n    public Builder rebuild() {\n        return new Builder(this);\n    }\n\n    public static class Builder {\n\n        private WatcherConfig mConfig;\n\n        private Builder() {\n            mConfig = new WatcherConfig();\n        }\n\n        private Builder(@NonNull WatcherConfig config) {\n            this.mConfig = config;\n        }\n\n        /**\n         * 选择的最大阈值\n         */\n        public Builder setThreshold(int threshold) {\n            mConfig.threshold = threshold;\n            return this;\n        }\n\n        /**\n         * 需要展示的 URI 集合\n         *\n         * @param metas    数据集合\n         * @param position 展示的位置\n         */\n        public Builder setDisplayDataSet(@NonNull ArrayList<MediaMeta> metas, int position) {\n            Preconditions.checkNotNull(metas);\n            mConfig.mediaMetas = metas;\n            mConfig.position = position;\n            return this;\n        }\n\n        /**\n         * 设置用户已经选中的图片, 会与 {@link #mediaMetas} 比较, 在右上角打钩\n         * 若为 null, 则不提供图片选择的功能\n         *\n         * @param pickedPictures 已选中的图片\n         */\n        public Builder setUserPickedSet(@Nullable ArrayList<MediaMeta> pickedPictures) {\n            mConfig.userPickedSet = pickedPictures;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param textColor 边框的颜色\n         */\n        public Builder setIndicatorTextColor(@ColorInt int textColor) {\n            mConfig.indicatorTextColor = textColor;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param solidColor 边框的颜色\n         */\n        public Builder setIndicatorSolidColor(@ColorInt int solidColor) {\n            mConfig.indicatorSolidColor = solidColor;\n            return this;\n        }\n\n        /**\n         * 设置选择索引的边框颜色\n         *\n         * @param checkedColor   选中的边框颜色的 Res Id\n         * @param uncheckedColor 未选中的边框颜色的Res Id\n         */\n        public Builder setIndicatorBorderColor(@ColorInt int checkedColor, @ColorInt int uncheckedColor) {\n            mConfig.indicatorBorderCheckedColor = checkedColor;\n            mConfig.indicatorBorderUncheckedColor = uncheckedColor;\n            return this;\n        }\n\n        public WatcherConfig build() {\n            if (mConfig.threshold > 0 && mConfig.userPickedSet == null) {\n                mConfig.userPickedSet = new ArrayList<>(mConfig.threshold);\n            }\n            return mConfig;\n        }\n\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherContract.java",
    "content": "package com.sharry.lib.album;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.StringRes;\n\nimport java.util.ArrayList;\n\n/**\n * PicturePicture MVP 的约束\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/6/13.\n */\ninterface WatcherContract {\n\n    interface IView {\n\n        void showSharedElementEnter(@NonNull MediaMeta mediaMeta, @NonNull SharedElementHelper.Bounds elementData);\n\n        void showSharedElementExitAndFinish(@NonNull SharedElementHelper.Bounds elementData);\n\n        void setLeftTitleText(@NonNull CharSequence content);\n\n        void setIndicatorVisible(boolean isShowCheckedIndicator);\n\n        void setIndicatorColors(int indicatorBorderCheckedColor, int indicatorBorderUncheckedColor,\n                                int indicatorSolidColor, int indicatorTextColor);\n\n        void setIndicatorChecked(boolean isChecked);\n\n        void setIndicatorText(@NonNull CharSequence indicatorText);\n\n        void setEnsureText(@NonNull CharSequence content);\n\n        void setDisplayAdapter(@NonNull ArrayList<MediaMeta> mediaMetas);\n\n        void displayAt(int position);\n\n        void setPickedAdapter(@NonNull ArrayList<MediaMeta> pickedSet);\n\n        void pickedPanelSmoothScrollToPosition(int position);\n\n        void showPickedPanel();\n\n        void dismissPickedPanel();\n\n        void notifyItemRemoved(@NonNull MediaMeta removedMeta, int removedIndex);\n\n        void notifyItemPicked(@NonNull MediaMeta addedMeta, int addedIndex);\n\n        String getString(@StringRes int resId);\n\n        void showMsg(@NonNull String msg);\n\n        void sendEnsureBroadcast();\n\n        void finish();\n    }\n\n    interface IPresenter {\n\n        void handlePagerChanged(int position);\n\n        void handleEnsureClicked();\n\n        void handleIndicatorClick(boolean isChecked);\n\n        void handlePickedItemClicked(MediaMeta pickedUri);\n\n        boolean handleDisplayPagerDismiss();\n\n        SharedElementHelper.Bounds getExitSharedElement();\n\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherFragment.java",
    "content": "package com.sharry.lib.album;\n\nimport android.os.Bundle;\nimport android.util.SparseArray;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\n\nimport com.sharry.lib.album.photoview.PhotoView;\n\nimport java.util.ArrayDeque;\nimport java.util.Queue;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-03 15:50\n */\npublic class WatcherFragment extends Fragment implements View.OnClickListener {\n\n    static final SparseArray<WatcherFragment> ACTIVES = new SparseArray<>();\n    static final Queue<WatcherFragment> IDLES = new ArrayDeque<>();\n\n    @NonNull\n    public static WatcherFragment getInstance(int position) {\n        WatcherFragment instance = ACTIVES.get(position);\n        if (instance == null) {\n            instance = IDLES.poll();\n            if (instance == null) {\n                instance = new WatcherFragment();\n            }\n            ACTIVES.put(position, instance);\n        }\n        return instance;\n    }\n\n    /**\n     * Widget.\n     */\n    private PhotoView mIvPicture;\n    private ImageView mIvPlayIcon;\n\n    private boolean mViewInitialized = false;\n\n    /**\n     * Display data source.\n     */\n    private MediaMeta mDataSource;\n\n    @Nullable\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return inflater.inflate(R.layout.lib_album_fragment_watcher_pager, container, false);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        initView(view);\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        mIvPicture = null;\n        mViewInitialized = false;\n        // Recycle Instance\n        int indexOfValue = ACTIVES.indexOfValue(this);\n        if (indexOfValue != -1) {\n            ACTIVES.removeAt(indexOfValue);\n        }\n        // 添加到空闲队列中\n        IDLES.offer(this);\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v.getId() == R.id.iv_play_icon) {\n            VideoPlayerActivity.launch(v.getContext(), mDataSource);\n        }\n    }\n\n    void setDataSource(@Nullable MediaMeta mediaMeta) {\n        mDataSource = mediaMeta;\n        performShowDataSource();\n    }\n\n    private void initView(View view) {\n        mIvPicture = view.findViewById(R.id.iv_picture);\n        mIvPlayIcon = view.findViewById(R.id.iv_play_icon);\n        mIvPlayIcon.setOnClickListener(this);\n        mViewInitialized = true;\n        performShowDataSource();\n    }\n\n    private void performShowDataSource() {\n        if (mDataSource == null || !mViewInitialized) {\n            return;\n        }\n        mIvPicture.setVisibility(View.VISIBLE);\n        if (mDataSource.isPicture) {\n            mIvPlayIcon.setVisibility(View.GONE);\n            if (Constants.MIME_TYPE_GIF.equals(mDataSource.mimeType)) {\n                Loader.loadGif(mIvPicture.getContext(), mDataSource, mIvPicture);\n            } else {\n                Loader.loadPicture(mIvPicture.getContext(), mDataSource, mIvPicture);\n            }\n        } else {\n            mIvPlayIcon.setVisibility(View.VISIBLE);\n            Loader.loadVideo(mIvPicture.getContext(), mDataSource, mIvPicture);\n        }\n    }\n\n    /**\n     * 获取 PhotoView\n     */\n    PhotoView getPhotoView() {\n        return mIvPicture;\n    }\n\n    /**\n     * 执行退出前的准备\n     */\n    void dismissOtherView() {\n        if (mViewInitialized) {\n            mIvPlayIcon.setVisibility(View.GONE);\n            mIvPicture.setVisibility(View.VISIBLE);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherManager.java",
    "content": "package com.sharry.lib.album;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\n\n/**\n * 图片查看器的管理类\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 4/28/2019 4:34 PM\n */\npublic class WatcherManager {\n\n    public static final String TAG = WatcherManager.class.getSimpleName();\n    private static String[] sPermissions = {\n            Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            Manifest.permission.READ_EXTERNAL_STORAGE\n    };\n\n    public static WatcherManager with(@NonNull Context context) {\n        if (context instanceof Activity) {\n            Activity activity = (Activity) context;\n            return new WatcherManager(activity);\n        } else {\n            throw new IllegalArgumentException(\"WatcherManager.with -> Context can not cast to Activity\");\n        }\n    }\n\n    private Activity mBind;\n    private WatcherConfig mConfig;\n    private View mTransitionView;\n\n    private WatcherManager(Activity activity) {\n        this.mBind = activity;\n    }\n\n    /**\n     * 设置共享元素\n     */\n    public WatcherManager setSharedElement(@NonNull View transitionView) {\n        mTransitionView = Preconditions.checkNotNull(transitionView, \"Please ensure View not null!\");\n        return this;\n    }\n\n    /**\n     * 设置图片预览的配置\n     */\n    public WatcherManager setConfig(@NonNull WatcherConfig config) {\n        this.mConfig = Preconditions.checkNotNull(config, \"Please ensure WatcherConfig not null!\");\n        return this;\n    }\n\n    /**\n     * 设置图片加载方案\n     */\n    public WatcherManager setLoaderEngine(@NonNull ILoaderEngine loader) {\n        Loader.setLoaderEngine(loader);\n        return this;\n    }\n\n    /**\n     * 调用图片查看器的方法\n     */\n    public void start() {\n        startForResult(WatcherCallback.DEFAULT);\n    }\n\n    /**\n     * 调用图片查看器, 一般用于相册\n     */\n    public void startForResult(@NonNull final WatcherCallbackLambda callbackLambda) {\n        Preconditions.checkNotNull(callbackLambda, \"Please ensure U set WatcherCallbackLambda correct.\");\n        startForResult(new WatcherCallback() {\n            @Override\n            public void onWatcherPickedComplete(@NonNull ArrayList<MediaMeta> pickedSet) {\n                callbackLambda.onWatcherPicked(pickedSet);\n            }\n\n            @Override\n            public void onWatcherPickedFailed() {\n                callbackLambda.onWatcherPicked(null);\n            }\n        });\n    }\n\n    /**\n     * 调用图片查看器, 一般用于相册\n     */\n    public void startForResult(@NonNull final WatcherCallback callback) {\n        Preconditions.checkNotNull(callback, \"Please ensure U set WatcherCallback correct.\");\n        Preconditions.checkNotNull(mConfig, \"Please ensure U set WatcherConfig correct.\");\n        PermissionsHelper.with(mBind)\n                .request(sPermissions)\n                .execute(new PermissionsCallback() {\n                    @Override\n                    public void onResult(boolean granted) {\n                        if (granted) {\n                            startForResultActual(callback);\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 真正执行 Activity 的启动\n     */\n    private void startForResultActual(final WatcherCallback callback) {\n        CallbackFragment callbackFragment = CallbackFragment.getInstance(mBind);\n        if (callbackFragment == null) {\n            callback.onWatcherPickedFailed();\n            return;\n        }\n        callbackFragment.setCallback(new CallbackFragment.Callback() {\n            @Override\n            public void onActivityResult(int requestCode, int resultCode, Intent data) {\n                if (resultCode == Activity.RESULT_OK && requestCode == WatcherActivity.REQUEST_CODE\n                        && mConfig.getUserPickedSet() != null) {\n                    callback.onWatcherPickedComplete(mConfig.getUserPickedSet());\n                } else {\n                    callback.onWatcherPickedFailed();\n                }\n            }\n        });\n        WatcherActivity.launchActivityForResult(mBind, callbackFragment, mConfig, mTransitionView);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/WatcherPresenter.java",
    "content": "package com.sharry.lib.album;\n\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport java.text.MessageFormat;\nimport java.util.ArrayList;\n\n/**\n * The presenter associated with PictureWatcher.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019/3/15 21:56\n */\nclass WatcherPresenter implements WatcherContract.IPresenter {\n\n    /**\n     * Final fields.\n     */\n    private final WatcherContract.IView mView;\n    private final WatcherConfig mConfig;\n    private final ArrayList<MediaMeta> mDisplayMetas;\n    private final ArrayList<MediaMeta> mPickedSet;\n    private final SharedElementHelper.Bounds mSharedElementEnterData;\n    private int mCurPosition;\n    private MediaMeta mCurDisplay;\n\n    WatcherPresenter(WatcherContract.IView view, WatcherConfig config, SharedElementHelper.Bounds sharedElementModel) {\n        this.mView = view;\n        this.mConfig = config;\n        this.mSharedElementEnterData = sharedElementModel;\n        // 获取需要展示图片的 URI 集合\n        this.mDisplayMetas = config.getPictureUris();\n        // 获取已经选中的图片\n        this.mPickedSet = config.getUserPickedSet();\n        // 获取当前需要展示的 Position 和 URI\n        this.mCurPosition = config.getPosition();\n        this.mCurDisplay = mDisplayMetas.get(mCurPosition);\n        // 配置视图\n        setupViews();\n    }\n\n    private void setupViews() {\n        // 1. 设置 Toolbar 数据\n        mView.setLeftTitleText(buildToolbarLeftText());\n        mView.setIndicatorVisible(mConfig.isPickerSupport());\n\n        // 2. 设置 Pictures 数据\n        mView.setDisplayAdapter(mDisplayMetas);\n        mView.displayAt(mCurPosition);\n\n        // 3. 设置底部菜单和按钮选中的状态\n        if (mConfig.isPickerSupport()) {\n            mView.setIndicatorColors(\n                    mConfig.getIndicatorBorderCheckedColor(),\n                    mConfig.getIndicatorBorderUncheckedColor(),\n                    mConfig.getIndicatorSolidColor(),\n                    mConfig.getIndicatorTextColor()\n            );\n            mView.setIndicatorChecked(mPickedSet.indexOf(mCurDisplay) != -1);\n            mView.setIndicatorText(buildToolbarCheckedIndicatorText());\n            // 底部菜单\n            mView.setPickedAdapter(mPickedSet);\n            mView.setEnsureText(buildEnsureText());\n            // 底部菜单延时弹出\n            if (!mPickedSet.isEmpty()) {\n                new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {\n                    @Override\n                    public void run() {\n                        mView.showPickedPanel();\n                    }\n                }, mSharedElementEnterData != null ? 500 : 0);\n            }\n        }\n\n        // 4. 执行共享元素入场动画\n        if (mSharedElementEnterData != null) {\n            mView.showSharedElementEnter(mDisplayMetas.get(mCurPosition), mSharedElementEnterData);\n        }\n    }\n\n    @Override\n    public void handlePagerChanged(int position) {\n        // 更新数据\n        mCurPosition = position;\n        mCurDisplay = mDisplayMetas.get(position);\n        // 展示 Toolbar 左边的指示文本\n        mView.setLeftTitleText(buildToolbarLeftText());\n        // 展示图片\n        mView.displayAt(mCurPosition);\n        if (mConfig.isPickerSupport()) {\n            mView.setIndicatorChecked(mPickedSet.indexOf(mCurDisplay) != -1);\n            mView.setIndicatorText(buildToolbarCheckedIndicatorText());\n            mView.setEnsureText(buildEnsureText());\n        }\n    }\n\n    @Override\n    public void handleIndicatorClick(boolean isChecked) {\n        if (isChecked) {\n            // 移除选中数据与状态\n            int removedIndex = mPickedSet.indexOf(mCurDisplay);\n            if (removedIndex < 0) {\n                return;\n            }\n            mPickedSet.remove(removedIndex);\n            // 通知 RecyclerView 数据变更\n            mView.notifyItemRemoved(mCurDisplay, removedIndex);\n        } else {\n            // 判断是否达到选择上限\n            if (mPickedSet.size() < mConfig.getThreshold()) {\n                mPickedSet.add(mCurDisplay);\n                int addedIndex = mPickedSet.indexOf(mCurDisplay);\n                // 通知 RecyclerView 数据变更\n                mView.notifyItemPicked(mCurDisplay, addedIndex);\n                mView.pickedPanelSmoothScrollToPosition(addedIndex);\n            } else {\n                mView.showMsg(\n                        mView.getString(R.string.lib_album_watcher_tips_over_threshold_prefix) +\n                                mConfig.getThreshold() +\n                                mView.getString(R.string.lib_album_watcher_tips_over_threshold_suffix)\n                );\n            }\n        }\n        mView.setIndicatorChecked(mPickedSet.indexOf(mCurDisplay) != -1);\n        mView.setIndicatorText(buildToolbarCheckedIndicatorText());\n        mView.setEnsureText(buildEnsureText());\n        // 控制底部导航栏的展示\n        if (mPickedSet.isEmpty()) {\n            mView.dismissPickedPanel();\n        } else {\n            mView.showPickedPanel();\n        }\n    }\n\n    @Override\n    public void handlePickedItemClicked(MediaMeta meta) {\n        int index = mDisplayMetas.indexOf(meta);\n        if (index >= 0) {\n            handlePagerChanged(index);\n        }\n    }\n\n    @Override\n    public void handleEnsureClicked() {\n        if (mPickedSet.isEmpty()) {\n            mView.showMsg(mView.getString(R.string.lib_album_watcher_tips_ensure_failed));\n            return;\n        }\n        mView.sendEnsureBroadcast();\n        mView.finish();\n    }\n\n    @Override\n    public boolean handleDisplayPagerDismiss() {\n        // 尝试获取退出时共享元素的数据\n        SharedElementHelper.Bounds exitData = getExitSharedElement();\n        // 若存在则消费这个 dismiss 事件\n        if (exitData != null) {\n            mView.showSharedElementExitAndFinish(exitData);\n            mView.dismissPickedPanel();\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public SharedElementHelper.Bounds getExitSharedElement() {\n        if (mSharedElementEnterData == null) {\n            return null;\n        }\n        return mSharedElementEnterData.position == mCurPosition ?\n                mSharedElementEnterData : SharedElementHelper.CACHES.get(mCurPosition);\n    }\n\n    /**\n     * 构建 Toolbar 左边的文本\n     */\n    private CharSequence buildToolbarLeftText() {\n        return MessageFormat.format(\"{0}/{1}\", mCurPosition + 1, mDisplayMetas.size());\n    }\n\n    /**\n     * 构建 Toolbar checked Indicator 的文本\n     */\n    private CharSequence buildToolbarCheckedIndicatorText() {\n        return String.valueOf(mPickedSet.indexOf(mCurDisplay) + 1);\n    }\n\n    /**\n     * 构建确认按钮文本\n     */\n    private CharSequence buildEnsureText() {\n        return MessageFormat.format(\"{0}({1}/{2})\",\n                mView.getString(R.string.lib_album_watcher_ensure), mPickedSet.size(), mConfig.getThreshold());\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/drawable/ic_album_watcher_right_arrow.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#ffffffff\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/drawable/ic_album_watcher_video_play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n</vector>\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/layout/lib_album_activity_watcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.sharry.lib.album.DraggableViewPager\n        android:id=\"@+id/view_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\" />\n\n    <com.sharry.lib.album.toolbar.SToolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:backIcon=\"@drawable/ic_album_watcher_right_arrow\"\n        app:statusBarStyle=\"Transparent\"\n        app:subItemInterval=\"10dp\"\n        app:titleGravity=\"Left\"\n        app:titleTextSize=\"18dp\" />\n\n    <LinearLayout\n        android:id=\"@+id/ll_picked_panel_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:background=\"@color/lib_album_watcher_bottom_preview_bg_color\"\n        android:gravity=\"right\"\n        android:orientation=\"vertical\"\n        android:visibility=\"invisible\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_picked_panel\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"100dp\" />\n\n        <TextView\n            android:id=\"@+id/tv_ensure\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"60dp\"\n            android:gravity=\"center\"\n            android:paddingLeft=\"20dp\"\n            android:paddingRight=\"20dp\"\n            android:text=\"@string/lib_album_watcher_ensure\"\n            android:textColor=\"@color/lib_album_watcher_bottom_preview_text_color\"\n            android:textSize=\"14dp\" />\n\n    </LinearLayout>\n\n    <com.sharry.lib.album.photoview.PhotoView\n        android:id=\"@+id/iv_se_place_holder\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"gone\" />\n\n</merge>"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/layout/lib_album_fragment_watcher_pager.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.sharry.lib.album.photoview.PhotoView\n        android:id=\"@+id/iv_picture\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <ImageView\n        android:id=\"@+id/iv_play_icon\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"80dp\"\n        android:layout_gravity=\"center\"\n        app:srcCompat=\"@drawable/ic_album_watcher_video_play\" />\n\n</FrameLayout>"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/values/watcher_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!--PictureWatcher-->\n    <color name=\"lib_album_watcher_bg_color\">#ff000000</color>\n    <color name=\"lib_album_watcher_bottom_preview_bg_color\">#a9000000</color>\n    <color name=\"lib_album_watcher_bottom_preview_text_color\">#ffffffff</color>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/values/watcher_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!--WatcherActivity 需要用到的字符串-->\n    <string name=\"lib_album_watcher_tips_over_threshold_prefix\">@string/lib_album_picker_tips_over_threshold_prefix</string>\n    <string name=\"lib_album_watcher_tips_over_threshold_suffix\">@string/lib_album_picker_tips_over_threshold_suffix</string>\n    <string name=\"lib_album_watcher_ensure\">@string/lib_album_picker_ensure</string>\n    <string name=\"lib_album_watcher_tips_ensure_failed\">@string/lib_album_picker_tips_ensure_failed</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/values/watcher_themes.xml",
    "content": "<resources>\n\n    <style name=\"WatcherTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "lib-album/src/main/watcher/com/sharry/lib/album/res/values-zh/watcher_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!--WatcherActivity 需要用到的字符串-->\n    <string name=\"lib_album_watcher_tips_over_threshold_prefix\">@string/lib_album_picker_tips_over_threshold_prefix</string>\n    <string name=\"lib_album_watcher_tips_over_threshold_suffix\">@string/lib_album_picker_tips_over_threshold_suffix</string>\n    <string name=\"lib_album_watcher_ensure\">@string/lib_album_picker_ensure</string>\n    <string name=\"lib_album_watcher_tips_ensure_failed\">@string/lib_album_picker_tips_ensure_failed</string>\n\n</resources>"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/CheckedIndicatorView.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Point;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.animation.AnticipateInterpolator;\nimport android.view.animation.OvershootInterpolator;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.AppCompatTextView;\n\n/**\n * 用于展示图片被选中的索引 View\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/6/14 16:24\n */\npublic class CheckedIndicatorView extends AppCompatTextView {\n\n    // Dimension\n    private int mBorderWidth;// 边框的宽度\n    private int mBorderMargin;// 边框与填充部分的间距\n    private int mRadius;// 绘制的半径\n    private float mAnimPercent = 0f;\n\n    // Color\n    private final int INVALIDATE_VALUE = -1;\n    private int mUncheckedBorderColor = Color.WHITE;// 未选中时边框的颜色\n    private int mCheckedBorderColor = mUncheckedBorderColor;// 选中时的边框颜色\n    private int mSolidColor = Color.BLUE;// 选中时内部填充的颜色\n\n    // Paint\n    private Paint mBorderPaint;\n    private Paint mSolidPaint;\n    private Point mCenterPoint;\n\n    // 用于控制的变量\n    private boolean mIsChecked = false;\n    private boolean mIsAnimatorStarted = false;\n\n    public CheckedIndicatorView(Context context) {\n        this(context, null);\n    }\n\n    public CheckedIndicatorView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public CheckedIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        setGravity(Gravity.CENTER);\n        init();\n    }\n\n    @Override\n    public void setOnClickListener(@Nullable final OnClickListener l) {\n        super.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                if (!mIsAnimatorStarted && l != null) {\n                    l.onClick(view);\n                }\n            }\n        });\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        // 可用的宽度\n        int validateWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();\n        // 可用的高度\n        int validateHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();\n        // 绘制的中心点\n        mCenterPoint.x = getPaddingLeft() + validateWidth / 2;\n        mCenterPoint.y = getPaddingTop() + validateHeight / 2;\n        // 内圆的半径\n        mRadius = Math.min(validateWidth, validateHeight) / 2;\n        // 外部边框的宽度\n        mBorderWidth = mRadius / 8;\n        // 外部边框距离内圆的距离\n        mBorderMargin = mBorderWidth;\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        // 绘制外环\n        mBorderPaint.setColor(mIsChecked ? mCheckedBorderColor : mUncheckedBorderColor);\n        mBorderPaint.setStrokeWidth(mBorderWidth);\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius - mBorderWidth / 2, mBorderPaint);\n        // 绘制内环\n        mSolidPaint.setColor(mIsChecked ? mSolidColor : mUncheckedBorderColor);\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mAnimPercent\n                * (mRadius - mBorderWidth - mBorderMargin), mSolidPaint);\n        // 绘制文本\n        if (mIsChecked) {\n            super.onDraw(canvas);\n        }\n    }\n\n    public void setChecked(boolean isChecked) {\n        if (isChecked != mIsChecked) {\n            if (mIsChecked) {\n                executeAnimator(false);\n            } else {\n                executeAnimator(true);\n            }\n        }\n    }\n\n    public void setCheckedWithoutAnimator(boolean isChecked) {\n        mIsChecked = isChecked;\n        mAnimPercent = mIsChecked ? 1 : 0;\n        invalidate();\n    }\n\n    public boolean isChecked() {\n        return mIsChecked;\n    }\n\n    /**\n     * 设置边框的颜色\n     */\n    public void setBorderColor(@ColorInt int checkedColor, @ColorInt int uncheckedColor) {\n        if (checkedColor != INVALIDATE_VALUE) {\n            mCheckedBorderColor = checkedColor;\n        }\n        if (uncheckedColor != INVALIDATE_VALUE) {\n            mUncheckedBorderColor = uncheckedColor;\n        }\n    }\n\n    /**\n     * 设置填充的颜色\n     */\n    public void setSolidColor(@ColorInt int solidColor) {\n        if (solidColor != INVALIDATE_VALUE) {\n            mSolidColor = solidColor;\n        }\n    }\n\n    /**\n     * 动态配置字体的尺寸\n     */\n    public void setTextSize(int dip) {\n        setTextSize(TypedValue.COMPLEX_UNIT_DIP, dip);\n    }\n\n    private void init() {\n        mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);\n        mBorderPaint.setStyle(Paint.Style.STROKE);\n        mBorderPaint.setColor(mUncheckedBorderColor);\n        mSolidPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);\n        mSolidPaint.setColor(mSolidColor);\n        mCenterPoint = new Point();\n        // 设置一个默认的点击事件\n        setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                setChecked(!mIsChecked);\n            }\n        });\n    }\n\n    /**\n     * 执行动画效果\n     *\n     * @param destIsChecked 最终选中的状态\n     */\n    private void executeAnimator(final boolean destIsChecked) {\n        if (mIsAnimatorStarted) {\n            return;\n        }\n        int start = destIsChecked ? 0 : 1;\n        int end = destIsChecked ? 1 : 0;\n        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end).setDuration(destIsChecked ? 300 : 200);\n        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mAnimPercent = (float) animation.getAnimatedValue();\n                invalidate();\n            }\n        });\n        valueAnimator.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationStart(Animator animation) {\n                mIsChecked = destIsChecked;\n                mIsAnimatorStarted = true;\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mIsAnimatorStarted = false;\n            }\n        });\n        if (destIsChecked) {\n            valueAnimator.setInterpolator(new OvershootInterpolator(2f));\n        } else {\n            valueAnimator.setInterpolator(new AnticipateInterpolator(2f));\n        }\n        valueAnimator.start();\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/DraggableViewPager.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.content.Context;\nimport android.database.DataSetObserver;\nimport android.graphics.Color;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.VelocityTracker;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.view.animation.AnticipateInterpolator;\nimport android.view.animation.OvershootInterpolator;\n\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.viewpager.widget.PagerAdapter;\nimport androidx.viewpager.widget.ViewPager;\n\n/**\n * 可拖拽返回的 ViewPager, 这里用作图片查看器\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.1\n * @since 2017/12/28 16:25\n */\npublic class DraggableViewPager extends ViewPager {\n\n    private static final int INVALIDATE_VALUE = -1;\n\n    private int mBaseColor = Color.BLACK;\n\n    private float mDownX = 0f;\n    private float mDownY = 0f;\n    private float mCapturedOriginY = 0f;\n    private float mDragThresholdHeight = 0f;\n    private float mVerticalVelocityThreshold = 1000f;\n    private float mFingerUpBackgroundAlpha = 1f;\n\n    private boolean mIsDragging = false;\n    private boolean mIsAnimRunning = false;\n\n    private VelocityTracker mVelocityTracker;\n    private Callback mCallback;\n\n    public DraggableViewPager(Context context) {\n        this(context, null);\n    }\n\n    public DraggableViewPager(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    private void init() {\n        // 速度捕获器\n        mVelocityTracker = VelocityTracker.obtain();\n        // 规定拖拽到消失的阈值\n        mDragThresholdHeight = getResources().getDisplayMetrics().heightPixels / 4;\n        // set page change listener\n        addOnPageChangeListener(new OnPageChangeListener() {\n            @Override\n            public void onPageSelected(int position) {\n                if (mCallback != null) {\n                    mCallback.onPagerChanged(position);\n                }\n            }\n\n            @Override\n            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\n\n            }\n\n            @Override\n            public void onPageScrollStateChanged(int state) {\n\n            }\n        });\n    }\n\n    /**\n     * 设置拖拽消失监听\n     */\n    public void setCallback(@Nullable Callback listener) {\n        this.mCallback = listener;\n    }\n\n    /**\n     * 设置背景色\n     *\n     * @param colorResId 颜色 ID\n     */\n    public void setBackgroundColorRes(@ColorRes int colorResId) {\n        setBackgroundColor(ContextCompat.getColor(getContext(), colorResId));\n    }\n\n    @Override\n    public void setBackgroundColor(int color) {\n        super.setBackgroundColor(color);\n        mBaseColor = color;\n    }\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        if (mIsAnimRunning) {\n            return false;\n        }\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mDownX = ev.getRawX();\n                mDownY = ev.getRawY();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                float deltaX = ev.getRawX() - mDownX;\n                float deltaY = ev.getRawY() - mDownY;\n                float maxMoved = Math.max(Math.abs(deltaX), Math.abs(deltaY));\n                if (maxMoved > ViewConfiguration.get(getContext()).getScaledTouchSlop()\n                        && Math.abs(deltaX) < Math.abs(deltaY)) {\n                    mIsDragging = true;\n                    return true;\n                } else {\n                    mIsDragging = false;\n                }\n            default:\n                break;\n        }\n        try {\n            return super.onInterceptTouchEvent(ev);\n        } catch (IllegalArgumentException e) {\n            e.printStackTrace();\n        }\n        return false;\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        if (!mIsDragging || getCurrentView() == null || mIsAnimRunning) {\n            return super.onTouchEvent(ev);\n        }\n        mVelocityTracker.addMovement(ev);\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mCapturedOriginY = getCurrentView().getY();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                float deltaY = (ev.getRawY() - mDownY) / 5;\n                getCurrentView().setY(mCapturedOriginY + deltaY);\n                mFingerUpBackgroundAlpha = 1 - (Math.abs(deltaY) / mDragThresholdHeight);\n                super.setBackgroundColor(ColorUtil.alphaColor(getBackgroundColor(), mFingerUpBackgroundAlpha));\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                mVelocityTracker.computeCurrentVelocity(1000);\n                // 这里处理拖拽后释放的操作\n                if (Math.abs(mVelocityTracker.getYVelocity()) > mVerticalVelocityThreshold\n                        || Math.abs(ev.getRawY() - mDownY) > mDragThresholdHeight) {\n                    dismiss();\n                } else {\n                    recover();\n                }\n                try {\n                    mVelocityTracker.recycle();\n                } catch (Throwable e) {\n                    // ......\n                }\n                mIsDragging = false;\n                break;\n            default:\n                break;\n        }\n        return true;\n    }\n\n    @Override\n    public void setAdapter(@Nullable PagerAdapter adapter) {\n        super.setAdapter(new PagerAdapterProxy(adapter));\n    }\n\n    @Nullable\n    private View getCurrentView() {\n        PagerAdapter adapter = getAdapter();\n        if (adapter instanceof PagerAdapterProxy) {\n            return ((PagerAdapterProxy) adapter).getCurrentView();\n        }\n        return null;\n    }\n\n    private void recover() {\n        if (mIsAnimRunning || getCurrentView() == null) {\n            return;\n        }\n        ValueAnimator animRecover = ValueAnimator.ofFloat(getCurrentView().getY(), mCapturedOriginY).setDuration(300);\n        animRecover.setInterpolator(new OvershootInterpolator(3f));\n        animRecover.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                getCurrentView().setY((float) animation.getAnimatedValue());\n                DraggableViewPager.super.setBackgroundColor(\n                        ColorUtil.alphaColor(\n                                getBackgroundColor(),\n                                mFingerUpBackgroundAlpha + (1 - mFingerUpBackgroundAlpha) * animation.getAnimatedFraction()\n                        )\n                );\n            }\n        });\n        animRecover.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationStart(Animator animation) {\n                mIsAnimRunning = true;\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mIsAnimRunning = false;\n            }\n        });\n        animRecover.start();\n    }\n\n    private void dismiss() {\n        if (mCallback != null && mCallback.handleDismissAction()) {\n            return;\n        }\n        if (mIsAnimRunning || getCurrentView() == null) {\n            return;\n        }\n        float destY = (getCurrentView().getY() - mCapturedOriginY > 0 ? 1 : -1)\n                * getResources().getDisplayMetrics().heightPixels;\n        ValueAnimator animDismiss = ValueAnimator.ofFloat(getCurrentView().getY(), destY).setDuration(400);\n        animDismiss.setInterpolator(new AnticipateInterpolator(1f));\n        animDismiss.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                getCurrentView().setY((float) animation.getAnimatedValue());\n                DraggableViewPager.super.setBackgroundColor(\n                        ColorUtil.alphaColor(\n                                getBackgroundColor(),\n                                mFingerUpBackgroundAlpha * (1 - animation.getAnimatedFraction())\n                        )\n                );\n            }\n        });\n        animDismiss.addListener(new AnimatorListenerAdapter() {\n\n            @Override\n            public void onAnimationStart(Animator animation) {\n                mIsAnimRunning = true;\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mIsAnimRunning = false;\n                if (mCallback != null) {\n                    mCallback.onDismissed();\n                }\n            }\n\n        });\n        animDismiss.start();\n    }\n\n    private int getBackgroundColor() {\n        return mBaseColor;\n    }\n\n    public interface Callback {\n\n        void onPagerChanged(int position);\n\n        /**\n         * @return if true mean dismiss action has been consumed, false will consume by self.\n         */\n        boolean handleDismissAction();\n\n        void onDismissed();\n\n    }\n\n    private static final class PagerAdapterProxy extends PagerAdapter {\n\n        private View mCurrentView;\n        private PagerAdapter mOriginAdapter;\n\n        PagerAdapterProxy(PagerAdapter originAdapter) {\n            mOriginAdapter = originAdapter;\n        }\n\n        public View getCurrentView() {\n            return mCurrentView;\n        }\n\n        @Override\n        public int getCount() {\n            return mOriginAdapter.getCount();\n        }\n\n        @Override\n        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {\n            return mOriginAdapter.isViewFromObject(view, object);\n        }\n\n        @Override\n        public Object instantiateItem(@NonNull ViewGroup container, int position) {\n            return mOriginAdapter.instantiateItem(container, position);\n        }\n\n        @Override\n        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n            mOriginAdapter.destroyItem(container, position, object);\n        }\n\n        @Override\n        public int getItemPosition(@NonNull Object object) {\n            return mOriginAdapter.getItemPosition(object);\n        }\n\n        @Override\n        public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n            if (object instanceof View) {\n                mCurrentView = (View) object;\n            } else if (object instanceof Fragment) {\n                mCurrentView = ((Fragment) object).getView();\n            }\n            mOriginAdapter.setPrimaryItem(container, position, object);\n        }\n\n        @Override\n        public void setPrimaryItem(@NonNull View container, int position, @NonNull Object object) {\n            if (object instanceof View) {\n                mCurrentView = (View) object;\n            } else if (object instanceof Fragment) {\n                mCurrentView = ((Fragment) object).getView();\n            }\n            mOriginAdapter.setPrimaryItem(container, position, object);\n        }\n\n        @Override\n        public void startUpdate(@NonNull ViewGroup container) {\n            mOriginAdapter.startUpdate(container);\n        }\n\n        @Override\n        public void finishUpdate(@NonNull ViewGroup container) {\n            mOriginAdapter.finishUpdate(container);\n        }\n\n        @Override\n        public void startUpdate(@NonNull View container) {\n            mOriginAdapter.startUpdate(container);\n        }\n\n        @NonNull\n        @Override\n        public Object instantiateItem(@NonNull View container, int position) {\n            return mOriginAdapter.instantiateItem(container, position);\n        }\n\n        @Override\n        public void destroyItem(@NonNull View container, int position, @NonNull Object object) {\n            mOriginAdapter.destroyItem(container, position, object);\n        }\n\n        @Override\n        public void finishUpdate(@NonNull View container) {\n            mOriginAdapter.finishUpdate(container);\n        }\n\n        @Nullable\n        @Override\n        public Parcelable saveState() {\n            return mOriginAdapter.saveState();\n        }\n\n        @Override\n        public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {\n            mOriginAdapter.restoreState(state, loader);\n        }\n\n        @Nullable\n        @Override\n        public CharSequence getPageTitle(int position) {\n            return mOriginAdapter.getPageTitle(position);\n        }\n\n        @Override\n        public float getPageWidth(int position) {\n            return mOriginAdapter.getPageWidth(position);\n        }\n\n        @Override\n        public void notifyDataSetChanged() {\n            mOriginAdapter.notifyDataSetChanged();\n        }\n\n        @Override\n        public void registerDataSetObserver(@NonNull DataSetObserver observer) {\n            mOriginAdapter.registerDataSetObserver(observer);\n        }\n\n        @Override\n        public void unregisterDataSetObserver(@NonNull DataSetObserver observer) {\n            mOriginAdapter.unregisterDataSetObserver(observer);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/PicturePickerFabBehavior.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.content.Context;\nimport androidx.annotation.NonNull;\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport com.google.android.material.floatingactionbutton.FloatingActionButton;\nimport androidx.core.view.ViewCompat;\nimport androidx.recyclerview.widget.RecyclerView;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\n/**\n * FloatingActionButton 的 CoordinateLayout 的 Behavior 动画\n * Used in xml {@code #R.layout.lib_album_activity_picker}\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.1\n * @since 2018/9/18 16:25\n */\npublic class PicturePickerFabBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {\n\n    public static PicturePickerFabBehavior from(View view) {\n        ViewGroup.LayoutParams params = view.getLayoutParams();\n        if (!(params instanceof CoordinatorLayout.LayoutParams)) {\n            throw new IllegalArgumentException(\"The view is not a child of CoordinatorLayout\");\n        } else {\n            CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();\n            if (!(behavior instanceof PicturePickerFabBehavior)) {\n                throw new IllegalArgumentException(\"The view is not associated with PicturePickerFabBehavior\");\n            } else {\n                return (PicturePickerFabBehavior) behavior;\n            }\n        }\n    }\n\n    private AnimatorSet mAppearAnimatorSet;\n    private AnimatorSet mDismissAnimatorSet;\n    private boolean mIsValid = true;\n\n    public PicturePickerFabBehavior() {\n    }\n\n    public PicturePickerFabBehavior(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    /**\n     * 设置这个 behavior 是否可用\n     *\n     * @param isValid if true the behavior is valid, if false the behavior is invalided.\n     */\n    public void setBehaviorValid(boolean isValid) {\n        mIsValid = isValid;\n    }\n\n    /**\n     * 设置依赖的控件\n     */\n    @Override\n    public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) {\n        return dependency instanceof RecyclerView;\n    }\n\n    @Override\n    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,\n                                       @NonNull FloatingActionButton child,\n                                       @NonNull View directTargetChild,\n                                       @NonNull View target, int axes, int type) {\n        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;\n    }\n\n    @Override\n    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,\n                               View target, int dxConsumed, int dyConsumed, int dxUnconsumed,\n                               int dyUnconsumed, int type) {\n        if (!mIsValid) return;\n        // 上滑\n        if (dyConsumed > 0 && dyUnconsumed == 0) {\n            setAnimator(child, true);\n        }\n\n        // 到了边界还在上滑\n        if (dyConsumed == 0 && dyUnconsumed > 0) {\n            setAnimator(child, true);\n        }\n\n        // 下滑\n        if (dyConsumed < 0 && dyUnconsumed == 0) {\n            setAnimator(child, false);\n        }\n\n        // 到了边界, 还在下滑\n        if (dyConsumed == 0 && dyUnconsumed < 0) {\n            setAnimator(child, false);\n        }\n\n    }\n\n    /**\n     * 处理动画效果\n     */\n    private void setAnimator(View target, final boolean isUp) {\n        if (getAppearAnimator(target).isRunning() || getDismissAnimator(target).isRunning()) return;\n        if (isUp) {// 处理上滑显示\n            if (target.getVisibility() == View.INVISIBLE) {\n                getAppearAnimator(target).start();\n            }\n        } else {// 处理下滑消失\n            if (target.getVisibility() == View.VISIBLE) {\n                getDismissAnimator(target).start();\n            }\n        }\n    }\n\n    /**\n     * 获取呈现动画\n     */\n    private AnimatorSet getAppearAnimator(final View target) {\n        if (mAppearAnimatorSet == null) {\n            mAppearAnimatorSet = new AnimatorSet();\n            mAppearAnimatorSet.playTogether(\n                    ObjectAnimator.ofFloat(target, \"scaleX\", 0f, 1f),\n                    ObjectAnimator.ofFloat(target, \"scaleY\", 0f, 1f)\n            );\n            mAppearAnimatorSet.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    target.setVisibility(View.VISIBLE);\n                }\n            });\n            mAppearAnimatorSet.setDuration(200);\n        }\n        return mAppearAnimatorSet;\n    }\n\n    /**\n     * 获取消失动画\n     */\n    private AnimatorSet getDismissAnimator(final View target) {\n        // 当且仅当处于 上滑状态, Animator动画结束, 且fab为可见状态时才执行下列方法\n        if (mDismissAnimatorSet == null) {\n            mDismissAnimatorSet = new AnimatorSet();\n            mDismissAnimatorSet.playTogether(\n                    ObjectAnimator.ofFloat(target, \"scaleX\", 1f, 0f),\n                    ObjectAnimator.ofFloat(target, \"scaleY\", 1f, 0f)\n            );\n            mDismissAnimatorSet.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    target.setVisibility(View.VISIBLE);\n                }\n\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    target.setVisibility(View.INVISIBLE);\n                }\n            });\n            mDismissAnimatorSet.setDuration(200);\n        }\n        return mDismissAnimatorSet;\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/RecorderButton.java",
    "content": "package com.sharry.lib.album;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.animation.ValueAnimator;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Point;\nimport android.graphics.RectF;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Nullable;\n\n/**\n * 相机拍摄录制的按钮\n * <p>\n * Please ensure u Activity implement {@link Interaction} correct when u use this widget.\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-04 09:15\n */\npublic class RecorderButton extends View implements View.OnTouchListener, View.OnClickListener {\n\n    private static final int MSG_WHAT_CALL_RECORD_START = 848;\n\n    /**\n     * 用于绘制的相关属性\n     */\n    private final Paint mPaint;\n    private final RectF mRect = new RectF();\n    private final Point mCenterPoint = new Point();\n\n    /**\n     * 圆环的半径\n     */\n    private int[] mInnerRadiusRange = new int[2];\n    private int[] mOuterRadiusRange = new int[2];\n    private int mCurInnerRadius;\n    private int mCurOuterRadius;\n\n    /**\n     * 录制进度\n     */\n    private long mMaxDuration = 100;\n    private long mCurDuration = 0;\n\n    /**\n     * 颜色相关\n     */\n    private final int mOuterColor = Color.parseColor(\"#FFF5F5F5\");\n    private final int mInnerColor = Color.WHITE;\n    private int mProgressColor;\n\n    /**\n     * Flags\n     */\n    private boolean mIsLongClickEnable = false;\n    private boolean mIsRecording = false;\n\n    /**\n     * 用于和外界交互\n     */\n    private Interaction mInteraction;\n\n    /**\n     * 处理录制开始的回调\n     */\n    private Handler mHandler = new Handler(Looper.getMainLooper()) {\n        @Override\n        public void handleMessage(Message msg) {\n            if (msg.what == MSG_WHAT_CALL_RECORD_START) {\n                mCurDuration = 0;\n                mInteraction.onRecordStart();\n            }\n        }\n    };\n\n    /**\n     * 动画集合\n     */\n    private AnimatorSet mStartAnimSet;\n    private AnimatorSet mFinishAnimSet;\n\n    public RecorderButton(Context context) {\n        this(context, null);\n    }\n\n    public RecorderButton(Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public RecorderButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        if (context instanceof Interaction) {\n            mInteraction = (Interaction) context;\n        } else {\n            throw new UnsupportedOperationException(\"Please ensure u activity implements RecorderButton.Interaction\");\n        }\n        // 初始化画笔\n        mPaint = new Paint();\n        mPaint.setAntiAlias(true);\n        mPaint.setDither(true);\n        mProgressColor = Color.parseColor(\"#ff00b0ff\");\n        setOnClickListener(this);\n        setOnTouchListener(this);\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        return super.dispatchTouchEvent(event);\n    }\n\n    @Override\n    public boolean onTouch(View v, MotionEvent event) {\n        if (mIsLongClickEnable) {\n            switch (event.getAction() & MotionEvent.ACTION_MASK) {\n                case MotionEvent.ACTION_DOWN:\n                    handleRecordStart();\n                    break;\n                case MotionEvent.ACTION_CANCEL:\n                case MotionEvent.ACTION_UP:\n                    handleRecordFinish();\n                    break;\n                default:\n                    break;\n            }\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void onClick(View v) {\n        mInteraction.onTakePicture();\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        int validWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();\n        int validHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();\n        int validSize = Math.min(validWidth, validHeight);\n        // 计算中心位置\n        mCenterPoint.x = getPaddingLeft() + validWidth >> 1;\n        mCenterPoint.y = getPaddingTop() + validHeight >> 1;\n        // 计算外环半径范围\n        mOuterRadiusRange[1] = validSize >> 1;\n        mOuterRadiusRange[0] = mOuterRadiusRange[1] * 3 / 4;\n        // 计算内环半径范围\n        mInnerRadiusRange[1] = mOuterRadiusRange[0] * 3 / 4;\n        mInnerRadiusRange[0] = mOuterRadiusRange[1] / 3;\n        // 初始化区域\n        mCurInnerRadius = mInnerRadiusRange[1];\n        mCurOuterRadius = mOuterRadiusRange[0];\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        // 绘制外部圆环\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setColor(mOuterColor);\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCurOuterRadius, mPaint);\n        // 绘制内部圆环\n        mPaint.setColor(mInnerColor);\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCurInnerRadius, mPaint);\n        if (mIsRecording) {\n            // 绘制进度\n            int strokeWidth = mInnerRadiusRange[0] >> 2;\n            int halfOfStrokeWidth = strokeWidth >> 1;\n            // 确定进度的范围\n            mRect.top = mCenterPoint.y - mCurOuterRadius + halfOfStrokeWidth;\n            mRect.left = mCenterPoint.x - mCurOuterRadius + halfOfStrokeWidth;\n            mRect.right = mRect.left + (mCurOuterRadius << 1) - strokeWidth;\n            mRect.bottom = mRect.top + (mCurOuterRadius << 1) - strokeWidth;\n            // 配置画笔\n            mPaint.setStrokeWidth(strokeWidth);\n            mPaint.setStyle(Paint.Style.STROKE);\n            mPaint.setColor(mProgressColor);\n            canvas.drawArc(mRect, -90, (mCurDuration * 360f / mMaxDuration), false, mPaint);\n        }\n    }\n\n    /**\n     * 设置进度条的颜色\n     */\n    public void setProgressColor(@ColorInt int color) {\n        mProgressColor = color;\n    }\n\n    /**\n     * 是否支持长按\n     */\n    public void setLongClickEnable(boolean isLongClickEnable) {\n        mIsLongClickEnable = isLongClickEnable;\n    }\n\n    /**\n     * 设置录制的最大时长\n     */\n    public void setMaxProgress(long maxDuration) {\n        this.mMaxDuration = maxDuration;\n    }\n\n    /**\n     * 设置当前录制的时长\n     */\n    public void setCurrentProgress(long curDuration) {\n        // 若处于未录制状态, 则无需影响进度更新\n        if (!mIsRecording) {\n            return;\n        }\n        // 说明此时正在处理结束的动作, 无效再响应进度更新了\n        if (mFinishAnimSet != null && mFinishAnimSet.isStarted()) {\n            return;\n        }\n        if (curDuration <= mCurDuration) {\n            return;\n        }\n        this.mCurDuration = curDuration;\n        if (mCurDuration >= mMaxDuration) {\n            // 处理录制结束\n            handleRecordFinish();\n        } else {\n            invalidate();\n        }\n    }\n\n    /**\n     * 处理录制开始\n     */\n    private void handleRecordStart() {\n        if (mStartAnimSet == null) {\n            mStartAnimSet = createStartAnim();\n        }\n        if (mStartAnimSet.isStarted()) {\n            return;\n        }\n        mStartAnimSet.start();\n    }\n\n    /**\n     * 处理录制结束\n     */\n    private void handleRecordFinish() {\n        if (!mIsRecording) {\n            return;\n        }\n        if (mFinishAnimSet == null) {\n            mFinishAnimSet = createFinishAnim();\n        }\n        if (mFinishAnimSet.isStarted()) {\n            return;\n        }\n        // 取消录制开始动画\n        mStartAnimSet.cancel();\n        // 启动结束动画\n        mFinishAnimSet.start();\n    }\n\n    private AnimatorSet createStartAnim() {\n        // 内圆缩小\n        ValueAnimator innerAnimator = ObjectAnimator.ofInt(mCurInnerRadius, mInnerRadiusRange[0])\n                .setDuration(200);\n        innerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mCurInnerRadius = (int) animation.getAnimatedValue();\n                invalidate();\n            }\n        });\n        // 外圆放大\n        ValueAnimator outerAnimator = ObjectAnimator.ofInt(mCurOuterRadius, mOuterRadiusRange[1])\n                .setDuration(200);\n        outerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mCurOuterRadius = (int) animation.getAnimatedValue();\n            }\n        });\n        // 执行动画集合\n        AnimatorSet startAnimSet = new AnimatorSet();\n        startAnimSet.playTogether(innerAnimator, outerAnimator);\n        startAnimSet.addListener(new AnimatorListenerAdapter() {\n\n            @Override\n            public void onAnimationStart(Animator animation) {\n                // 能够执行该方法说明录制已经开始了\n                mIsRecording = true;\n                // 1s 之后回调外界开始录制, 1s 之内视为拍照\n                mHandler.sendMessageDelayed(\n                        Message.obtain(mHandler, MSG_WHAT_CALL_RECORD_START),\n                        1000\n                );\n            }\n\n        });\n        return startAnimSet;\n    }\n\n    private AnimatorSet createFinishAnim() {\n        // 内圆放大\n        ValueAnimator innerAnimator = ObjectAnimator.ofInt(mCurInnerRadius, mInnerRadiusRange[1]).setDuration(200);\n        innerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mCurInnerRadius = (int) animation.getAnimatedValue();\n                invalidate();\n            }\n        });\n        // 外圆缩小\n        ValueAnimator outerAnimator = ObjectAnimator.ofInt(mCurOuterRadius, mOuterRadiusRange[0]).setDuration(200);\n        outerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mCurOuterRadius = (int) animation.getAnimatedValue();\n            }\n        });\n        // 执行动画集合\n        AnimatorSet finishAnimSet = new AnimatorSet();\n        finishAnimSet.playTogether(innerAnimator, outerAnimator);\n        finishAnimSet.addListener(new AnimatorListenerAdapter() {\n\n            boolean isTakePicture = false;\n\n            @Override\n            public void onAnimationStart(Animator animation) {\n                // 禁止响应触摸事件\n                setEnabled(false);\n                // 尝试移除录制开始的消息, 若移除成功则说明录制尚未启动, 则触发拍照\n                isTakePicture = mHandler.hasMessages(MSG_WHAT_CALL_RECORD_START);\n                if (isTakePicture) {\n                    mHandler.removeMessages(MSG_WHAT_CALL_RECORD_START);\n                }\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                if (isTakePicture) {\n                    mInteraction.onTakePicture();\n                } else {\n                    mInteraction.onRecordFinish(mCurDuration);\n                }\n                // 置为非录制状态\n                mIsRecording = false;\n                // 重置为 0\n                mCurDuration = 0;\n                // 重新响应触摸事件\n                setEnabled(true);\n            }\n\n        });\n        return finishAnimSet;\n    }\n\n    public interface Interaction {\n\n        /**\n         * Take a picture\n         */\n        void onTakePicture();\n\n        /**\n         * Record video.\n         */\n        void onRecordStart();\n\n        /**\n         * Record complete.\n         *\n         * @param duration total duration.\n         */\n        void onRecordFinish(long duration);\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/Compat.java",
    "content": "/*******************************************************************************\n * Copyright 2011, 2012 Chris Banes.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *******************************************************************************/\npackage com.sharry.lib.album.photoview;\n\nimport android.annotation.TargetApi;\nimport android.os.Build.VERSION;\nimport android.os.Build.VERSION_CODES;\nimport android.view.View;\n\nclass Compat {\n\n    private static final int SIXTY_FPS_INTERVAL = 1000 / 60;\n\n    public static void postOnAnimation(View view, Runnable runnable) {\n        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {\n            postOnAnimationJellyBean(view, runnable);\n        } else {\n            view.postDelayed(runnable, SIXTY_FPS_INTERVAL);\n        }\n    }\n\n    @TargetApi(16)\n    private static void postOnAnimationJellyBean(View view, Runnable runnable) {\n        view.postOnAnimation(runnable);\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/CustomGestureDetector.java",
    "content": "/*******************************************************************************\n * Copyright 2011, 2012 Chris Banes.\n * <p/>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p/>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p/>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *******************************************************************************/\npackage com.sharry.lib.album.photoview;\n\nimport android.content.Context;\nimport android.view.MotionEvent;\nimport android.view.ScaleGestureDetector;\nimport android.view.VelocityTracker;\nimport android.view.ViewConfiguration;\n\n/**\n * Does a whole lot of gesture detecting.\n */\nclass CustomGestureDetector {\n\n    private static final int INVALID_POINTER_ID = -1;\n\n    private int mActivePointerId = INVALID_POINTER_ID;\n    private int mActivePointerIndex = 0;\n    private final ScaleGestureDetector mDetector;\n\n    private VelocityTracker mVelocityTracker;\n    private boolean mIsDragging;\n    private float mLastTouchX;\n    private float mLastTouchY;\n    private final float mTouchSlop;\n    private final float mMinimumVelocity;\n    private OnGestureListener mListener;\n\n    CustomGestureDetector(Context context, OnGestureListener listener) {\n        final ViewConfiguration configuration = ViewConfiguration\n                .get(context);\n        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();\n        mTouchSlop = configuration.getScaledTouchSlop();\n\n        mListener = listener;\n        ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {\n\n            @Override\n            public boolean onScale(ScaleGestureDetector detector) {\n                float scaleFactor = detector.getScaleFactor();\n\n                if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))\n                    return false;\n\n                mListener.onScale(scaleFactor,\n                        detector.getFocusX(), detector.getFocusY());\n                return true;\n            }\n\n            @Override\n            public boolean onScaleBegin(ScaleGestureDetector detector) {\n                return true;\n            }\n\n            @Override\n            public void onScaleEnd(ScaleGestureDetector detector) {\n                // NO-OP\n            }\n        };\n        mDetector = new ScaleGestureDetector(context, mScaleListener);\n    }\n\n    private float getActiveX(MotionEvent ev) {\n        try {\n            return ev.getX(mActivePointerIndex);\n        } catch (Exception e) {\n            return ev.getX();\n        }\n    }\n\n    private float getActiveY(MotionEvent ev) {\n        try {\n            return ev.getY(mActivePointerIndex);\n        } catch (Exception e) {\n            return ev.getY();\n        }\n    }\n\n    public boolean isScaling() {\n        return mDetector.isInProgress();\n    }\n\n    public boolean isDragging() {\n        return mIsDragging;\n    }\n\n    public boolean onTouchEvent(MotionEvent ev) {\n        try {\n            mDetector.onTouchEvent(ev);\n            return processTouchEvent(ev);\n        } catch (IllegalArgumentException e) {\n            // Fix for support lib bug, happening when onDestroy is called\n            return true;\n        }\n    }\n\n    private boolean processTouchEvent(MotionEvent ev) {\n        final int action = ev.getAction();\n        switch (action & MotionEvent.ACTION_MASK) {\n            case MotionEvent.ACTION_DOWN:\n                mActivePointerId = ev.getPointerId(0);\n\n                mVelocityTracker = VelocityTracker.obtain();\n                if (null != mVelocityTracker) {\n                    mVelocityTracker.addMovement(ev);\n                }\n\n                mLastTouchX = getActiveX(ev);\n                mLastTouchY = getActiveY(ev);\n                mIsDragging = false;\n                break;\n            case MotionEvent.ACTION_MOVE:\n                final float x = getActiveX(ev);\n                final float y = getActiveY(ev);\n                final float dx = x - mLastTouchX, dy = y - mLastTouchY;\n\n                if (!mIsDragging) {\n                    // Use Pythagoras to see if drag length is larger than\n                    // touch slop\n                    mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;\n                }\n\n                if (mIsDragging) {\n                    mListener.onDrag(dx, dy);\n                    mLastTouchX = x;\n                    mLastTouchY = y;\n\n                    if (null != mVelocityTracker) {\n                        mVelocityTracker.addMovement(ev);\n                    }\n                }\n                break;\n            case MotionEvent.ACTION_CANCEL:\n                mActivePointerId = INVALID_POINTER_ID;\n                // Recycle Velocity Tracker\n                if (null != mVelocityTracker) {\n                    mVelocityTracker.recycle();\n                    mVelocityTracker = null;\n                }\n                break;\n            case MotionEvent.ACTION_UP:\n                mActivePointerId = INVALID_POINTER_ID;\n                if (mIsDragging) {\n                    if (null != mVelocityTracker) {\n                        mLastTouchX = getActiveX(ev);\n                        mLastTouchY = getActiveY(ev);\n\n                        // Compute velocity within the last 1000ms\n                        mVelocityTracker.addMovement(ev);\n                        mVelocityTracker.computeCurrentVelocity(1000);\n\n                        final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker\n                                .getYVelocity();\n\n                        // If the velocity is greater than minVelocity, call\n                        // listener\n                        if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {\n                            mListener.onFling(mLastTouchX, mLastTouchY, -vX,\n                                    -vY);\n                        }\n                    }\n                }\n\n                // Recycle Velocity Tracker\n                if (null != mVelocityTracker) {\n                    mVelocityTracker.recycle();\n                    mVelocityTracker = null;\n                }\n                break;\n            case MotionEvent.ACTION_POINTER_UP:\n                final int pointerIndex = Util.getPointerIndex(ev.getAction());\n                final int pointerId = ev.getPointerId(pointerIndex);\n                if (pointerId == mActivePointerId) {\n                    // This was our active pointer going up. Choose a new\n                    // active pointer and adjust accordingly.\n                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;\n                    mActivePointerId = ev.getPointerId(newPointerIndex);\n                    mLastTouchX = ev.getX(newPointerIndex);\n                    mLastTouchY = ev.getY(newPointerIndex);\n                }\n                break;\n        }\n\n        mActivePointerIndex = ev\n                .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId\n                        : 0);\n        return true;\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnGestureListener.java",
    "content": "/*******************************************************************************\n * Copyright 2011, 2012 Chris Banes.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *******************************************************************************/\npackage com.sharry.lib.album.photoview;\n\ninterface OnGestureListener {\n\n    void onDrag(float dx, float dy);\n\n    void onFling(float startX, float startY, float velocityX,\n                 float velocityY);\n\n    void onScale(float scaleFactor, float focusX, float focusY);\n\n}"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnMatrixChangedListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.graphics.RectF;\n\n/**\n * Interface definition for a callback to be invoked when the internal Matrix has changed for\n * this View.\n */\npublic interface OnMatrixChangedListener {\n\n    /**\n     * Callback for when the Matrix displaying the Drawable has changed. This could be because\n     * the View's bounds have changed, or the user has zoomed.\n     *\n     * @param rect - Rectangle displaying the Drawable's new bounds.\n     */\n    void onMatrixChanged(RectF rect);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnOutsidePhotoTapListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.widget.ImageView;\n\n/**\n * Callback when the user tapped outside of the photo\n */\npublic interface OnOutsidePhotoTapListener {\n\n    /**\n     * The outside of the photo has been tapped\n     */\n    void onOutsidePhotoTap(ImageView imageView);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnPhotoTapListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.widget.ImageView;\n\n/**\n * A callback to be invoked when the Photo is tapped with a single\n * tap.\n */\npublic interface OnPhotoTapListener {\n\n    /**\n     * A callback to receive where the user taps on a photo. You will only receive a callback if\n     * the user taps on the actual photo, tapping on 'whitespace' will be ignored.\n     *\n     * @param view ImageView the user tapped.\n     * @param x    where the user tapped from the of the Drawable, as percentage of the\n     *             Drawable width.\n     * @param y    where the user tapped from the top of the Drawable, as percentage of the\n     *             Drawable height.\n     */\n    void onPhotoTap(ImageView view, float x, float y);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnScaleChangedListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\n\n/**\n * Interface definition for callback to be invoked when attached ImageView scale changes\n */\npublic interface OnScaleChangedListener {\n\n    /**\n     * Callback for when the scale changes\n     *\n     * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in)\n     * @param focusX      focal point X position\n     * @param focusY      focal point Y position\n     */\n    void onScaleChange(float scaleFactor, float focusX, float focusY);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnSingleFlingListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.view.MotionEvent;\n\n/**\n * A callback to be invoked when the ImageView is flung with a single\n * touch\n */\npublic interface OnSingleFlingListener {\n\n    /**\n     * A callback to receive where the user flings on a ImageView. You will receive a callback if\n     * the user flings anywhere on the view.\n     *\n     * @param e1        MotionEvent the user first touch.\n     * @param e2        MotionEvent the user last touch.\n     * @param velocityX distance of user's horizontal fling.\n     * @param velocityY distance of user's vertical fling.\n     */\n    boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnViewDragListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\n/**\n * Interface definition for a callback to be invoked when the photo is experiencing a drag event\n */\npublic interface OnViewDragListener {\n\n    /**\n     * Callback for when the photo is experiencing a drag event. This cannot be invoked when the\n     * user is scaling.\n     *\n     * @param dx The change of the coordinates in the x-direction\n     * @param dy The change of the coordinates in the y-direction\n     */\n    void onDrag(float dx, float dy);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/OnViewTapListener.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.view.View;\n\npublic interface OnViewTapListener {\n\n    /**\n     * A callback to receive where the user taps on a ImageView. You will receive a callback if\n     * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored.\n     *\n     * @param view - View the user tapped.\n     * @param x    - where the user tapped from the left of the View.\n     * @param y    - where the user tapped from the top of the View.\n     */\n    void onViewTap(View view, float x, float y);\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/PhotoView.java",
    "content": "/*******************************************************************************\n * Copyright 2011, 2012 Chris Banes.\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *******************************************************************************/\npackage com.sharry.lib.album.photoview;\n\nimport android.content.Context;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\nimport android.widget.ImageView;\n\n/**\n * A zoomable {@link ImageView}. See {@link PhotoViewAttacher} for most of the details on how the zooming\n * is accomplished\n */\npublic class PhotoView extends AppCompatImageView {\n\n    private PhotoViewAttacher attacher;\n    private ImageView.ScaleType pendingScaleType;\n\n    public PhotoView(Context context) {\n        this(context, null);\n    }\n\n    public PhotoView(Context context, AttributeSet attr) {\n        this(context, attr, 0);\n    }\n\n    public PhotoView(Context context, AttributeSet attr, int defStyle) {\n        super(context, attr, defStyle);\n        init();\n    }\n\n    private void init() {\n        attacher = new PhotoViewAttacher(this);\n        //We always pose as a Matrix scale type, though we can change to another scale type\n        //via the attacher\n        super.setScaleType(ScaleType.MATRIX);\n        //apply the previously applied scale type\n        if (pendingScaleType != null) {\n            setScaleType(pendingScaleType);\n            pendingScaleType = null;\n        }\n    }\n\n    /**\n     * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references\n     * to this attacher, as it has a reference to this view, which, if a reference is held in the\n     * wrong place, can cause memory leaks.\n     *\n     * @return the attacher.\n     */\n    public PhotoViewAttacher getAttacher() {\n        return attacher;\n    }\n\n    @Override\n    public ScaleType getScaleType() {\n        return attacher.getScaleType();\n    }\n\n    @Override\n    public Matrix getImageMatrix() {\n        return attacher.getImageMatrix();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        attacher.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void setOnClickListener(OnClickListener l) {\n        attacher.setOnClickListener(l);\n    }\n\n    @Override\n    public void setScaleType(ScaleType scaleType) {\n        if (attacher == null) {\n            pendingScaleType = scaleType;\n        } else {\n            attacher.setScaleType(scaleType);\n        }\n    }\n\n    @Override\n    public void setImageDrawable(Drawable drawable) {\n        super.setImageDrawable(drawable);\n        // setImageBitmap calls through to this method\n        if (attacher != null) {\n            attacher.update();\n        }\n    }\n\n    @Override\n    public void setImageResource(int resId) {\n        super.setImageResource(resId);\n        if (attacher != null) {\n            attacher.update();\n        }\n    }\n\n    @Override\n    public void setImageURI(Uri uri) {\n        super.setImageURI(uri);\n        if (attacher != null) {\n            attacher.update();\n        }\n    }\n\n    @Override\n    protected boolean setFrame(int l, int t, int r, int b) {\n        boolean changed = super.setFrame(l, t, r, b);\n        if (changed) {\n            attacher.update();\n        }\n        return changed;\n    }\n\n    /**\n     * Thanks for google framework sources, {@link ImageView}\n     */\n    public void animateTransform(Matrix matrix) {\n        Drawable drawable = getDrawable();\n        if (drawable == null) {\n            return;\n        }\n        if (matrix == null) {\n            drawable.setBounds(0, 0, getWidth(), getHeight());\n        } else {\n            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());\n            setImageMatrix(matrix);\n        }\n    }\n\n    public void setRotationTo(float rotationDegree) {\n        attacher.setRotationTo(rotationDegree);\n    }\n\n    public void setRotationBy(float rotationDegree) {\n        attacher.setRotationBy(rotationDegree);\n    }\n\n    @Deprecated\n    public boolean isZoomEnabled() {\n        return attacher.isZoomEnabled();\n    }\n\n    public boolean isZoomable() {\n        return attacher.isZoomable();\n    }\n\n    public void setZoomable(boolean zoomable) {\n        attacher.setZoomable(zoomable);\n    }\n\n    public RectF getDisplayRect() {\n        return attacher.getDisplayRect();\n    }\n\n    public void getDisplayMatrix(Matrix matrix) {\n        attacher.getDisplayMatrix(matrix);\n    }\n\n    public boolean setDisplayMatrix(Matrix finalRectangle) {\n        return attacher.setDisplayMatrix(finalRectangle);\n    }\n\n    public void getSuppMatrix(Matrix matrix) {\n        attacher.getSuppMatrix(matrix);\n    }\n\n    public boolean setSuppMatrix(Matrix matrix) {\n        return attacher.setDisplayMatrix(matrix);\n    }\n\n    public float getMinimumScale() {\n        return attacher.getMinimumScale();\n    }\n\n    public float getMediumScale() {\n        return attacher.getMediumScale();\n    }\n\n    public float getMaximumScale() {\n        return attacher.getMaximumScale();\n    }\n\n    public float getScale() {\n        return attacher.getScale();\n    }\n\n    public void setAllowParentInterceptOnEdge(boolean allow) {\n        attacher.setAllowParentInterceptOnEdge(allow);\n    }\n\n    public void setMinimumScale(float minimumScale) {\n        attacher.setMinimumScale(minimumScale);\n    }\n\n    public void setMediumScale(float mediumScale) {\n        attacher.setMediumScale(mediumScale);\n    }\n\n    public void setMaximumScale(float maximumScale) {\n        attacher.setMaximumScale(maximumScale);\n    }\n\n    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {\n        attacher.setScaleLevels(minimumScale, mediumScale, maximumScale);\n    }\n\n    public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {\n        attacher.setOnMatrixChangeListener(listener);\n    }\n\n    public void setOnPhotoTapListener(OnPhotoTapListener listener) {\n        attacher.setOnPhotoTapListener(listener);\n    }\n\n    public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) {\n        attacher.setOnOutsidePhotoTapListener(listener);\n    }\n\n    public void setOnViewTapListener(OnViewTapListener listener) {\n        attacher.setOnViewTapListener(listener);\n    }\n\n    public void setOnViewDragListener(OnViewDragListener listener) {\n        attacher.setOnViewDragListener(listener);\n    }\n\n    public void setScale(float scale) {\n        attacher.setScale(scale);\n    }\n\n    public void setScale(float scale, boolean animate) {\n        attacher.setScale(scale, animate);\n    }\n\n    public void setScale(float scale, float focalX, float focalY, boolean animate) {\n        attacher.setScale(scale, focalX, focalY, animate);\n    }\n\n    public void setZoomTransitionDuration(int milliseconds) {\n        attacher.setZoomTransitionDuration(milliseconds);\n    }\n\n    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) {\n        attacher.setOnDoubleTapListener(onDoubleTapListener);\n    }\n\n    public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) {\n        attacher.setOnScaleChangeListener(onScaleChangedListener);\n    }\n\n    public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {\n        attacher.setOnSingleFlingListener(onSingleFlingListener);\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/PhotoViewAttacher.java",
    "content": "/*******************************************************************************\n * Copyright 2011, 2012 Chris Banes.\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *******************************************************************************/\npackage com.sharry.lib.album.photoview;\n\nimport android.content.Context;\nimport android.graphics.Matrix;\nimport android.graphics.Matrix.ScaleToFit;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport androidx.core.view.MotionEventCompat;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.View.OnLongClickListener;\nimport android.view.ViewParent;\nimport android.view.animation.AccelerateDecelerateInterpolator;\nimport android.view.animation.Interpolator;\nimport android.widget.ImageView;\nimport android.widget.ImageView.ScaleType;\nimport android.widget.OverScroller;\n\n/**\n * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc.\n * It is made public in case you need to subclass something other than {@link ImageView} and still\n * gain the functionality that {@link PhotoView} offers\n */\npublic class PhotoViewAttacher implements View.OnTouchListener,\n        View.OnLayoutChangeListener {\n\n    private static float DEFAULT_MAX_SCALE = 3.0f;\n    private static float DEFAULT_MID_SCALE = 1.75f;\n    private static float DEFAULT_MIN_SCALE = 1.0f;\n    private static int DEFAULT_ZOOM_DURATION = 200;\n\n    private static final int EDGE_NONE = -1;\n    private static final int EDGE_LEFT = 0;\n    private static final int EDGE_RIGHT = 1;\n    private static final int EDGE_BOTH = 2;\n    private static int SINGLE_TOUCH = 1;\n\n    private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();\n    private int mZoomDuration = DEFAULT_ZOOM_DURATION;\n    private float mMinScale = DEFAULT_MIN_SCALE;\n    private float mMidScale = DEFAULT_MID_SCALE;\n    private float mMaxScale = DEFAULT_MAX_SCALE;\n\n    private boolean mAllowParentInterceptOnEdge = true;\n    private boolean mBlockParentIntercept = false;\n\n    private ImageView mImageView;\n\n    // Gesture Detectors\n    private GestureDetector mGestureDetector;\n    private CustomGestureDetector mScaleDragDetector;\n\n    // These are set so we don't keep allocating them on the heap\n    private final Matrix mBaseMatrix = new Matrix();\n    private final Matrix mDrawMatrix = new Matrix();\n    private final Matrix mSuppMatrix = new Matrix();\n    private final RectF mDisplayRect = new RectF();\n    private final float[] mMatrixValues = new float[9];\n\n    // Listeners\n    private OnMatrixChangedListener mMatrixChangeListener;\n    private OnPhotoTapListener mPhotoTapListener;\n    private OnOutsidePhotoTapListener mOutsidePhotoTapListener;\n    private OnViewTapListener mViewTapListener;\n    private View.OnClickListener mOnClickListener;\n    private OnLongClickListener mLongClickListener;\n    private OnScaleChangedListener mScaleChangeListener;\n    private OnSingleFlingListener mSingleFlingListener;\n    private OnViewDragListener mOnViewDragListener;\n\n    private FlingRunnable mCurrentFlingRunnable;\n    private int mScrollEdge = EDGE_BOTH;\n    private float mBaseRotation;\n\n    private boolean mZoomEnabled = true;\n    private ScaleType mScaleType = ScaleType.FIT_CENTER;\n\n    private OnGestureListener onGestureListener = new OnGestureListener() {\n        @Override\n        public void onDrag(float dx, float dy) {\n            if (mScaleDragDetector.isScaling()) {\n                return; // Do not drag if we are already scaling\n            }\n\n            if (mOnViewDragListener != null) {\n                mOnViewDragListener.onDrag(dx, dy);\n            }\n            mSuppMatrix.postTranslate(dx, dy);\n            checkAndDisplayMatrix();\n\n            /*\n             * Here we decide whether to let the ImageView's parent to startForResult taking\n             * over the touch event.\n             *\n             * First we check whether this function is enabled. We never want the\n             * parent to crop over if we're scaling. We then check the edge we're\n             * on, and the direction of the scroll (i.e. if we're pulling against\n             * the edge, aka 'overscrolling', let the parent crop over).\n             */\n            ViewParent parent = mImageView.getParent();\n            if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {\n                if (mScrollEdge == EDGE_BOTH\n                        || (mScrollEdge == EDGE_LEFT && dx >= 1f)\n                        || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {\n                    if (parent != null) {\n                        parent.requestDisallowInterceptTouchEvent(false);\n                    }\n                }\n            } else {\n                if (parent != null) {\n                    parent.requestDisallowInterceptTouchEvent(true);\n                }\n            }\n        }\n\n        @Override\n        public void onFling(float startX, float startY, float velocityX, float velocityY) {\n            mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());\n            mCurrentFlingRunnable.fling(getImageViewWidth(mImageView),\n                    getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);\n            mImageView.post(mCurrentFlingRunnable);\n        }\n\n        @Override\n        public void onScale(float scaleFactor, float focusX, float focusY) {\n            if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {\n                if (mScaleChangeListener != null) {\n                    mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);\n                }\n                mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);\n                checkAndDisplayMatrix();\n            }\n        }\n    };\n\n    public PhotoViewAttacher(ImageView imageView) {\n        mImageView = imageView;\n        imageView.setOnTouchListener(this);\n        imageView.addOnLayoutChangeListener(this);\n\n        if (imageView.isInEditMode()) {\n            return;\n        }\n\n        mBaseRotation = 0.0f;\n\n        // Create Gesture Detectors...\n        mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);\n\n        mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {\n\n            // forward long click listener\n            @Override\n            public void onLongPress(MotionEvent e) {\n                if (mLongClickListener != null) {\n                    mLongClickListener.onLongClick(mImageView);\n                }\n            }\n\n            @Override\n            public boolean onFling(MotionEvent e1, MotionEvent e2,\n                                   float velocityX, float velocityY) {\n                if (mSingleFlingListener != null) {\n                    if (getScale() > DEFAULT_MIN_SCALE) {\n                        return false;\n                    }\n\n                    if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH\n                            || MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {\n                        return false;\n                    }\n\n                    return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);\n                }\n                return false;\n            }\n        });\n\n        mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {\n            @Override\n            public boolean onSingleTapConfirmed(MotionEvent e) {\n                if (mOnClickListener != null) {\n                    mOnClickListener.onClick(mImageView);\n                }\n                final RectF displayRect = getDisplayRect();\n\n                final float x = e.getX(), y = e.getY();\n\n                if (mViewTapListener != null) {\n                    mViewTapListener.onViewTap(mImageView, x, y);\n                }\n\n                if (displayRect != null) {\n\n                    // Check to see if the user tapped on the photo\n                    if (displayRect.contains(x, y)) {\n\n                        float xResult = (x - displayRect.left)\n                                / displayRect.width();\n                        float yResult = (y - displayRect.top)\n                                / displayRect.height();\n\n                        if (mPhotoTapListener != null) {\n                            mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult);\n                        }\n                        return true;\n                    } else {\n                        if (mOutsidePhotoTapListener != null) {\n                            mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView);\n                        }\n                    }\n                }\n                return false;\n            }\n\n            @Override\n            public boolean onDoubleTap(MotionEvent ev) {\n                try {\n                    float scale = getScale();\n                    float x = ev.getX();\n                    float y = ev.getY();\n\n                    if (scale < getMediumScale()) {\n                        setScale(getMediumScale(), x, y, true);\n                    } else if (scale >= getMediumScale() && scale < getMaximumScale()) {\n                        setScale(getMaximumScale(), x, y, true);\n                    } else {\n                        setScale(getMinimumScale(), x, y, true);\n                    }\n                } catch (ArrayIndexOutOfBoundsException e) {\n                    // Can sometimes happen when getX() and getY() is called\n                }\n\n                return true;\n            }\n\n            @Override\n            public boolean onDoubleTapEvent(MotionEvent e) {\n                // Wait for the confirmed onDoubleTap() instead\n                return false;\n            }\n        });\n    }\n\n    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {\n        this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener);\n    }\n\n    public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) {\n        this.mScaleChangeListener = onScaleChangeListener;\n    }\n\n    public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {\n        this.mSingleFlingListener = onSingleFlingListener;\n    }\n\n    @Deprecated\n    public boolean isZoomEnabled() {\n        return mZoomEnabled;\n    }\n\n    public RectF getDisplayRect() {\n        checkMatrixBounds();\n        return getDisplayRect(getDrawMatrix());\n    }\n\n    public boolean setDisplayMatrix(Matrix finalMatrix) {\n        if (finalMatrix == null) {\n            throw new IllegalArgumentException(\"Matrix cannot be null\");\n        }\n\n        if (mImageView.getDrawable() == null) {\n            return false;\n        }\n\n        mSuppMatrix.set(finalMatrix);\n        checkAndDisplayMatrix();\n\n        return true;\n    }\n\n    public void setBaseRotation(final float degrees) {\n        mBaseRotation = degrees % 360;\n        update();\n        setRotationBy(mBaseRotation);\n        checkAndDisplayMatrix();\n    }\n\n    public void setRotationTo(float degrees) {\n        mSuppMatrix.setRotate(degrees % 360);\n        checkAndDisplayMatrix();\n    }\n\n    public void setRotationBy(float degrees) {\n        mSuppMatrix.postRotate(degrees % 360);\n        checkAndDisplayMatrix();\n    }\n\n    public float getMinimumScale() {\n        return mMinScale;\n    }\n\n    public float getMediumScale() {\n        return mMidScale;\n    }\n\n    public float getMaximumScale() {\n        return mMaxScale;\n    }\n\n    public float getScale() {\n        return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));\n    }\n\n    public ScaleType getScaleType() {\n        return mScaleType;\n    }\n\n    @Override\n    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {\n        // Update our base matrix, as the bounds have changed\n        if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {\n            updateBaseMatrix(mImageView.getDrawable());\n        }\n    }\n\n    @Override\n    public boolean onTouch(View v, MotionEvent ev) {\n        boolean handled = false;\n\n        if (mZoomEnabled && Util.hasDrawable((ImageView) v)) {\n            switch (ev.getAction()) {\n                case MotionEvent.ACTION_DOWN:\n                    ViewParent parent = v.getParent();\n                    // First, disable the Parent from intercepting the touch\n                    // event\n                    if (parent != null) {\n                        parent.requestDisallowInterceptTouchEvent(true);\n                    }\n\n                    // If we're flinging, and the user presses down, cancel\n                    // fling\n                    cancelFling();\n                    break;\n\n                case MotionEvent.ACTION_CANCEL:\n                case MotionEvent.ACTION_UP:\n                    // If the user has zoomed less than min scale, zoom back\n                    // to min scale\n                    if (getScale() < mMinScale) {\n                        RectF rect = getDisplayRect();\n                        if (rect != null) {\n                            v.post(new AnimatedZoomRunnable(getScale(), mMinScale,\n                                    rect.centerX(), rect.centerY()));\n                            handled = true;\n                        }\n                    } else if (getScale() > mMaxScale) {\n                        RectF rect = getDisplayRect();\n                        if (rect != null) {\n                            v.post(new AnimatedZoomRunnable(getScale(), mMaxScale,\n                                    rect.centerX(), rect.centerY()));\n                            handled = true;\n                        }\n                    }\n                    break;\n            }\n\n            // Try the Scale/Drag detector\n            if (mScaleDragDetector != null) {\n                boolean wasScaling = mScaleDragDetector.isScaling();\n                boolean wasDragging = mScaleDragDetector.isDragging();\n\n                handled = mScaleDragDetector.onTouchEvent(ev);\n\n                boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();\n                boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();\n\n                mBlockParentIntercept = didntScale && didntDrag;\n            }\n\n            // Check to see if the user double tapped\n            if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {\n                handled = true;\n            }\n\n        }\n\n        return handled;\n    }\n\n    public void setAllowParentInterceptOnEdge(boolean allow) {\n        mAllowParentInterceptOnEdge = allow;\n    }\n\n    public void setMinimumScale(float minimumScale) {\n        Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale);\n        mMinScale = minimumScale;\n    }\n\n    public void setMediumScale(float mediumScale) {\n        Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale);\n        mMidScale = mediumScale;\n    }\n\n    public void setMaximumScale(float maximumScale) {\n        Util.checkZoomLevels(mMinScale, mMidScale, maximumScale);\n        mMaxScale = maximumScale;\n    }\n\n    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {\n        Util.checkZoomLevels(minimumScale, mediumScale, maximumScale);\n        mMinScale = minimumScale;\n        mMidScale = mediumScale;\n        mMaxScale = maximumScale;\n    }\n\n    public void setOnLongClickListener(OnLongClickListener listener) {\n        mLongClickListener = listener;\n    }\n\n    public void setOnClickListener(View.OnClickListener listener) {\n        mOnClickListener = listener;\n    }\n\n    public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {\n        mMatrixChangeListener = listener;\n    }\n\n    public void setOnPhotoTapListener(OnPhotoTapListener listener) {\n        mPhotoTapListener = listener;\n    }\n\n    public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) {\n        this.mOutsidePhotoTapListener = mOutsidePhotoTapListener;\n    }\n\n    public void setOnViewTapListener(OnViewTapListener listener) {\n        mViewTapListener = listener;\n    }\n\n    public void setOnViewDragListener(OnViewDragListener listener) {\n        mOnViewDragListener = listener;\n    }\n\n    public void setScale(float scale) {\n        setScale(scale, false);\n    }\n\n    public void setScale(float scale, boolean animate) {\n        setScale(scale,\n                (mImageView.getRight()) / 2,\n                (mImageView.getBottom()) / 2,\n                animate);\n    }\n\n    public void setScale(float scale, float focalX, float focalY,\n                         boolean animate) {\n        // Check to see if the scale is within bounds\n        if (scale < mMinScale || scale > mMaxScale) {\n            throw new IllegalArgumentException(\"Scale must be within the range of minScale and maxScale\");\n        }\n\n        if (animate) {\n            mImageView.post(new AnimatedZoomRunnable(getScale(), scale,\n                    focalX, focalY));\n        } else {\n            mSuppMatrix.setScale(scale, scale, focalX, focalY);\n            checkAndDisplayMatrix();\n        }\n    }\n\n    /**\n     * Set the zoom interpolator\n     *\n     * @param interpolator the zoom interpolator\n     */\n    public void setZoomInterpolator(Interpolator interpolator) {\n        mInterpolator = interpolator;\n    }\n\n    public void setScaleType(ScaleType scaleType) {\n        if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) {\n            mScaleType = scaleType;\n            update();\n        }\n    }\n\n    public boolean isZoomable() {\n        return mZoomEnabled;\n    }\n\n    public void setZoomable(boolean zoomable) {\n        mZoomEnabled = zoomable;\n        update();\n    }\n\n    public void update() {\n        if (mZoomEnabled) {\n            // Update the base matrix using the current drawable\n            updateBaseMatrix(mImageView.getDrawable());\n        } else {\n            // Reset the Matrix...\n            resetMatrix();\n        }\n    }\n\n    /**\n     * Get the display matrix\n     *\n     * @param matrix target matrix to copy to\n     */\n    public void getDisplayMatrix(Matrix matrix) {\n        matrix.set(getDrawMatrix());\n    }\n\n    /**\n     * Get the current support matrix\n     */\n    public void getSuppMatrix(Matrix matrix) {\n        matrix.set(mSuppMatrix);\n    }\n\n    private Matrix getDrawMatrix() {\n        mDrawMatrix.set(mBaseMatrix);\n        mDrawMatrix.postConcat(mSuppMatrix);\n        return mDrawMatrix;\n    }\n\n    public Matrix getImageMatrix() {\n        return mDrawMatrix;\n    }\n\n    public void setZoomTransitionDuration(int milliseconds) {\n        this.mZoomDuration = milliseconds;\n    }\n\n    /**\n     * Helper method that 'unpacks' a Matrix and returns the required value\n     *\n     * @param matrix     Matrix to unpack\n     * @param whichValue Which value from Matrix.M* to return\n     * @return returned value\n     */\n    private float getValue(Matrix matrix, int whichValue) {\n        matrix.getValues(mMatrixValues);\n        return mMatrixValues[whichValue];\n    }\n\n    /**\n     * Resets the Matrix back to FIT_CENTER, and then displays its contents\n     */\n    private void resetMatrix() {\n        mSuppMatrix.reset();\n        setRotationBy(mBaseRotation);\n        setImageViewMatrix(getDrawMatrix());\n        checkMatrixBounds();\n    }\n\n    private void setImageViewMatrix(Matrix matrix) {\n        mImageView.setImageMatrix(matrix);\n\n        // Call MatrixChangedListener if needed\n        if (mMatrixChangeListener != null) {\n            RectF displayRect = getDisplayRect(matrix);\n            if (displayRect != null) {\n                mMatrixChangeListener.onMatrixChanged(displayRect);\n            }\n        }\n    }\n\n    /**\n     * Helper method that simply checks the Matrix, and then displays the result\n     */\n    private void checkAndDisplayMatrix() {\n        if (checkMatrixBounds()) {\n            setImageViewMatrix(getDrawMatrix());\n        }\n    }\n\n    /**\n     * Helper method that maps the supplied Matrix to the current Drawable\n     *\n     * @param matrix - Matrix to map Drawable against\n     * @return RectF - Displayed Rectangle\n     */\n    private RectF getDisplayRect(Matrix matrix) {\n        Drawable d = mImageView.getDrawable();\n        if (d != null) {\n            mDisplayRect.set(0, 0, d.getIntrinsicWidth(),\n                    d.getIntrinsicHeight());\n            matrix.mapRect(mDisplayRect);\n            return mDisplayRect;\n        }\n        return null;\n    }\n\n    /**\n     * Calculate Matrix for FIT_CENTER\n     *\n     * @param drawable - Drawable being displayed\n     */\n    private void updateBaseMatrix(Drawable drawable) {\n        if (drawable == null) {\n            return;\n        }\n\n        final float viewWidth = getImageViewWidth(mImageView);\n        final float viewHeight = getImageViewHeight(mImageView);\n        final int drawableWidth = drawable.getIntrinsicWidth();\n        final int drawableHeight = drawable.getIntrinsicHeight();\n\n        mBaseMatrix.reset();\n\n        final float widthScale = viewWidth / drawableWidth;\n        final float heightScale = viewHeight / drawableHeight;\n\n        if (mScaleType == ScaleType.CENTER) {\n            mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,\n                    (viewHeight - drawableHeight) / 2F);\n\n        } else if (mScaleType == ScaleType.CENTER_CROP) {\n            float scale = Math.max(widthScale, heightScale);\n            mBaseMatrix.postScale(scale, scale);\n            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,\n                    (viewHeight - drawableHeight * scale) / 2F);\n\n        } else if (mScaleType == ScaleType.CENTER_INSIDE) {\n            float scale = Math.min(1.0f, Math.min(widthScale, heightScale));\n            mBaseMatrix.postScale(scale, scale);\n            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,\n                    (viewHeight - drawableHeight * scale) / 2F);\n\n        } else {\n            RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);\n            RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);\n\n            if ((int) mBaseRotation % 180 != 0) {\n                mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);\n            }\n\n            switch (mScaleType) {\n                case FIT_CENTER:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);\n                    break;\n\n                case FIT_START:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);\n                    break;\n\n                case FIT_END:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);\n                    break;\n\n                case FIT_XY:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);\n                    break;\n\n                default:\n                    break;\n            }\n        }\n\n        resetMatrix();\n    }\n\n    private boolean checkMatrixBounds() {\n\n        final RectF rect = getDisplayRect(getDrawMatrix());\n        if (rect == null) {\n            return false;\n        }\n\n        final float height = rect.height(), width = rect.width();\n        float deltaX = 0, deltaY = 0;\n\n        final int viewHeight = getImageViewHeight(mImageView);\n        if (height <= viewHeight) {\n            switch (mScaleType) {\n                case FIT_START:\n                    deltaY = -rect.top;\n                    break;\n                case FIT_END:\n                    deltaY = viewHeight - height - rect.top;\n                    break;\n                default:\n                    deltaY = (viewHeight - height) / 2 - rect.top;\n                    break;\n            }\n        } else if (rect.top > 0) {\n            deltaY = -rect.top;\n        } else if (rect.bottom < viewHeight) {\n            deltaY = viewHeight - rect.bottom;\n        }\n\n        final int viewWidth = getImageViewWidth(mImageView);\n        if (width <= viewWidth) {\n            switch (mScaleType) {\n                case FIT_START:\n                    deltaX = -rect.left;\n                    break;\n                case FIT_END:\n                    deltaX = viewWidth - width - rect.left;\n                    break;\n                default:\n                    deltaX = (viewWidth - width) / 2 - rect.left;\n                    break;\n            }\n            mScrollEdge = EDGE_BOTH;\n        } else if (rect.left > 0) {\n            mScrollEdge = EDGE_LEFT;\n            deltaX = -rect.left;\n        } else if (rect.right < viewWidth) {\n            deltaX = viewWidth - rect.right;\n            mScrollEdge = EDGE_RIGHT;\n        } else {\n            mScrollEdge = EDGE_NONE;\n        }\n\n        // Finally actually translate the matrix\n        mSuppMatrix.postTranslate(deltaX, deltaY);\n        return true;\n    }\n\n    private int getImageViewWidth(ImageView imageView) {\n        return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();\n    }\n\n    private int getImageViewHeight(ImageView imageView) {\n        return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();\n    }\n\n    private void cancelFling() {\n        if (mCurrentFlingRunnable != null) {\n            mCurrentFlingRunnable.cancelFling();\n            mCurrentFlingRunnable = null;\n        }\n    }\n\n    private class AnimatedZoomRunnable implements Runnable {\n\n        private final float mFocalX, mFocalY;\n        private final long mStartTime;\n        private final float mZoomStart, mZoomEnd;\n\n        public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,\n                                    final float focalX, final float focalY) {\n            mFocalX = focalX;\n            mFocalY = focalY;\n            mStartTime = System.currentTimeMillis();\n            mZoomStart = currentZoom;\n            mZoomEnd = targetZoom;\n        }\n\n        @Override\n        public void run() {\n\n            float t = interpolate();\n            float scale = mZoomStart + t * (mZoomEnd - mZoomStart);\n            float deltaScale = scale / getScale();\n\n            onGestureListener.onScale(deltaScale, mFocalX, mFocalY);\n\n            // We haven't hit our target scale yet, so post ourselves again\n            if (t < 1f) {\n                Compat.postOnAnimation(mImageView, this);\n            }\n        }\n\n        private float interpolate() {\n            float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;\n            t = Math.min(1f, t);\n            t = mInterpolator.getInterpolation(t);\n            return t;\n        }\n    }\n\n    private class FlingRunnable implements Runnable {\n\n        private final OverScroller mScroller;\n        private int mCurrentX, mCurrentY;\n\n        public FlingRunnable(Context context) {\n            mScroller = new OverScroller(context);\n        }\n\n        public void cancelFling() {\n            mScroller.forceFinished(true);\n        }\n\n        public void fling(int viewWidth, int viewHeight, int velocityX,\n                          int velocityY) {\n            final RectF rect = getDisplayRect();\n            if (rect == null) {\n                return;\n            }\n\n            final int startX = Math.round(-rect.left);\n            final int minX, maxX, minY, maxY;\n\n            if (viewWidth < rect.width()) {\n                minX = 0;\n                maxX = Math.round(rect.width() - viewWidth);\n            } else {\n                minX = maxX = startX;\n            }\n\n            final int startY = Math.round(-rect.top);\n            if (viewHeight < rect.height()) {\n                minY = 0;\n                maxY = Math.round(rect.height() - viewHeight);\n            } else {\n                minY = maxY = startY;\n            }\n\n            mCurrentX = startX;\n            mCurrentY = startY;\n\n            // If we actually can move, fling the scroller\n            if (startX != maxX || startY != maxY) {\n                mScroller.fling(startX, startY, velocityX, velocityY, minX,\n                        maxX, minY, maxY, 0, 0);\n            }\n        }\n\n        @Override\n        public void run() {\n            if (mScroller.isFinished()) {\n                return; // remaining post that should not be handled\n            }\n\n            if (mScroller.computeScrollOffset()) {\n\n                final int newX = mScroller.getCurrX();\n                final int newY = mScroller.getCurrY();\n\n                mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);\n                checkAndDisplayMatrix();\n\n                mCurrentX = newX;\n                mCurrentY = newY;\n\n                // Post On animation\n                Compat.postOnAnimation(mImageView, this);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/photoview/Util.java",
    "content": "package com.sharry.lib.album.photoview;\n\nimport android.view.MotionEvent;\nimport android.widget.ImageView;\n\nclass Util {\n\n    static void checkZoomLevels(float minZoom, float midZoom,\n                                float maxZoom) {\n        if (minZoom >= midZoom) {\n            throw new IllegalArgumentException(\n                    \"Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value\");\n        } else if (midZoom >= maxZoom) {\n            throw new IllegalArgumentException(\n                    \"Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value\");\n        }\n    }\n\n    static boolean hasDrawable(ImageView imageView) {\n        return imageView.getDrawable() != null;\n    }\n\n    static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) {\n        if (scaleType == null) {\n            return false;\n        }\n        switch (scaleType) {\n            case MATRIX:\n                throw new IllegalStateException(\"Matrix scale type is not supported\");\n        }\n        return true;\n    }\n\n    static int getPointerIndex(int action) {\n        return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/AppBarHelper.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.Window;\n\n/**\n * 变更 App bar 的风格的帮助类\n *\n * @author Sharry <a href=\"SharryChooChn@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/8/27 23:41\n */\nclass AppBarHelper {\n\n    private static final int DEFAULT_OPTIONS = 0;\n    private int mOptions = DEFAULT_OPTIONS;\n    private Activity mActivity;\n    private Window mWindow;\n    private AppBarHelper(Context context) {\n        if (context instanceof Activity) {\n            mActivity = (Activity) context;\n            mWindow = mActivity.getWindow();\n        } else {\n            throw new IllegalArgumentException(\"Please ensure context instance of Activity.\");\n        }\n    }\n\n    /**\n     * Get AppBarHelper instance with this factory method.\n     */\n    static AppBarHelper with(Context context) {\n        return new AppBarHelper(context);\n    }\n\n    /**\n     * 设置StatusBar的风格\n     */\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    AppBarHelper setStatusBarStyle(Style style) {\n        if (!Utils.isLollipop()) {\n            return this;\n        }\n        switch (style) {\n            // 设置状态栏为全透明\n            case TRANSPARENT: {\n                int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n                mOptions = mOptions | option;\n                mWindow.setStatusBarColor(Color.TRANSPARENT);\n                break;\n            }\n            // 设置状态栏为半透明\n            case TRANSLUCENCE: {\n                int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n                mOptions = mOptions | option;\n                mWindow.setStatusBarColor(Utils.alphaColor(Color.BLACK, 0.3f));\n                break;\n            }\n            // 隐藏状态栏\n            case HIDE: {\n                int option = View.SYSTEM_UI_FLAG_FULLSCREEN;\n                mOptions = mOptions | option;\n                break;\n            }\n            // 清除透明状态栏\n            default:\n            case DEFAULT: {\n                mOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n                //获取当前Application主题中的状态栏Color\n                TypedValue typedValue = new TypedValue();\n                mActivity.getTheme().resolveAttribute(android.R.attr.colorPrimaryDark, typedValue, true);\n                int color = typedValue.data;\n                mWindow.setStatusBarColor(color);\n            }\n        }\n        return this;\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    AppBarHelper setStatusBarColor(int color) {\n        if (!Utils.isLollipop()) {\n            return this;\n        }\n        mOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n        mWindow.setStatusBarColor(color);\n        return this;\n    }\n\n    /**\n     * 设置NavigationBar的风格\n     */\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    AppBarHelper setNavigationBarStyle(Style style) {\n        if (!Utils.isLollipop()) {\n            return this;\n        }\n        switch (style) {\n            // 设置导航栏为全透明\n            case TRANSPARENT: {\n                int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;\n                mOptions = mOptions | option;\n                mWindow.setNavigationBarColor(Color.TRANSPARENT);\n                break;\n            }\n            // 设置导航栏为半透明\n            case TRANSLUCENCE: {\n                int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;\n                mOptions = mOptions | option;\n                mWindow.setNavigationBarColor(Utils.alphaColor(Color.BLACK, 0.3f));\n                break;\n            }\n            //隐藏导航栏\n            case HIDE: {\n                int option = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;\n                mOptions = mOptions | option;\n                break;\n            }\n            default:\n            case DEFAULT: {\n                mOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n                //获取当前Activity主题中的color\n                TypedValue typedValue = new TypedValue();\n                mActivity.getTheme().resolveAttribute(android.R.attr.colorPrimaryDark, typedValue, true);\n                int color = typedValue.data;\n                mWindow.setNavigationBarColor(color);\n            }\n        }\n        return this;\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    AppBarHelper setNavigationBarColor(int color) {\n        if (!Utils.isLollipop()) {\n            return this;\n        }\n        mOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;\n        mWindow.setNavigationBarColor(color);\n        return this;\n    }\n\n    /**\n     * 隐藏所有Bar(全屏模式)\n     */\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    AppBarHelper setAllBarsHide() {\n        if (!Utils.isLollipop()) {\n            return this;\n        }\n        int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION\n                | View.SYSTEM_UI_FLAG_FULLSCREEN\n                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;\n        mOptions = option;\n        return this;\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    void apply() {\n        if (!Utils.isLollipop()) {\n            return;\n        }\n        if (mOptions != DEFAULT_OPTIONS) {\n            View decorView = mWindow.getDecorView();\n            decorView.setSystemUiVisibility(mOptions);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/Builder.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.widget.LinearLayout;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.Dimension;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static androidx.annotation.Dimension.DP;\nimport static androidx.annotation.Dimension.SP;\n\n/**\n * Build SToolbar more easier.\n *\n * @author Sharry <a href=\"SharryChooChn@Gmail.com\">Contact me.</a>\n * @version 2.0\n * @since 2018/8/27 23:36\n */\npublic class Builder {\n\n    /*\n       Constants.\n     */\n    private static final int INVALIDATE = -1;\n    private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE;\n    private static final int DEFAULT_DIVIDING_LINE_HEIGHT = 0;\n    private static final int DEFAULT_DIVIDING_LINE_COLOR = Color.LTGRAY;\n    private static final int DEFAULT_TITLE_GRAVITY = Gravity.CENTER | Gravity.TOP;\n    private final List<Entity> mTitleEntities = new ArrayList<>();\n    private final List<Entity> mMenuLeftEntities = new ArrayList<>();\n    private final List<Entity> mMenuRightEntities = new ArrayList<>();\n    /*\n       Fields.\n     */\n    private Context mContext;\n    private ViewGroup mContentParent;\n    private Style mStyle = Style.DEFAULT;\n    private int mMinimumHeight = INVALIDATE;\n    private int mSubItemInterval = INVALIDATE;\n    private int mBackgroundDrawableResId = INVALIDATE;\n    private int mBackgroundColor = DEFAULT_BACKGROUND_COLOR;\n    private int mDividingLineHeight = DEFAULT_DIVIDING_LINE_HEIGHT;\n    private int mDividingLineColor = DEFAULT_DIVIDING_LINE_COLOR;\n    private int mTitleGravity = DEFAULT_TITLE_GRAVITY;\n    /*\n       View options.\n     */\n    private TextViewOptions mTitleTextOps;\n    private ImageViewOptions mTitleImageOps;\n\n    /**\n     * 给 Activity 添加 Toolbar\n     */\n    Builder(Context context) {\n        if (context instanceof Activity) {\n            mContext = context;\n            mContentParent = ((Activity) mContext).findViewById(Window.ID_ANDROID_CONTENT);\n        } else {\n            throw new IllegalArgumentException(\"Please ensure context instanceof Activity.\");\n        }\n    }\n\n    /**\n     * 给 View 添加 Toolbar, 确保传入的 View 为 LinearLayout\n     */\n    Builder(View contentView) {\n        if (contentView instanceof LinearLayout) {\n            mContentParent = (ViewGroup) contentView;\n            mContext = contentView.getContext();\n        } else {\n            throw new IllegalArgumentException(\"Please ensure contentView instanceof \" +\n                    \"LinearLayout, now is: \" + contentView);\n        }\n    }\n\n    /**\n     * Set interval associated with this toolbar sub item.\n     */\n    public Builder setSubItemInterval(@Dimension(unit = DP) int subItemInterval) {\n        mSubItemInterval = subItemInterval;\n        return this;\n    }\n\n    /**\n     * Set minimumHeight associated with this toolbar.\n     */\n    public Builder setMinimumHeight(@Dimension(unit = DP) int minimumHeight) {\n        mMinimumHeight = minimumHeight;\n        return this;\n    }\n\n    /**\n     * Set style associated with bind activity status bar.\n     */\n    public Builder setStatusBarStyle(Style statusBarStyle) {\n        mStyle = statusBarStyle;\n        return this;\n    }\n\n    /**\n     * Set the background color to a given resource. The colorResId should refer to\n     * a color int.\n     */\n    public Builder setBackgroundColorRes(@ColorRes int colorResId) {\n        mBackgroundColor = ContextCompat.getColor(mContext, colorResId);\n        return this;\n    }\n\n    /**\n     * Set the background color associated with this toolbar.\n     */\n    public Builder setBackgroundColor(@ColorInt int color) {\n        mBackgroundColor = color;\n        return this;\n    }\n\n    /**\n     * Set the background to a given resource. The resource should refer to\n     * a Drawable object or 0 to remove the background.\n     */\n    public Builder setBackgroundDrawableRes(@DrawableRes int drawableResId) {\n        mBackgroundDrawableResId = drawableResId;\n        return this;\n    }\n\n    /**\n     * Set dividing line associated with this toolbar.\n     */\n    public Builder setDividingLineHeight(@Dimension(unit = DP) int height) {\n        this.mDividingLineHeight = height;\n        return this;\n    }\n\n    /**\n     * Set dividing line color associated with this toolbar.\n     */\n    public Builder setDividingLineColorRes(@ColorRes int dividingLineColorRes) {\n        this.mDividingLineColor = ContextCompat.getColor(mContext, dividingLineColorRes);\n        return this;\n    }\n\n    /**\n     * Set dividing line color associated with this toolbar.\n     */\n    public Builder setDividingLineColor(@ColorInt int dividingLineColor) {\n        this.mDividingLineColor = dividingLineColor;\n        return this;\n    }\n\n    /**\n     * Set gravity associated with this toolbar title.\n     */\n    public Builder setTitleGravity(int gravity) {\n        mTitleGravity = gravity;\n        return this;\n    }\n\n    /**\n     * Set text associated with this toolbar title.\n     */\n    public Builder setTitleText(CharSequence text) {\n        this.setTitleText(text, TextViewOptions.DEFAULT_TITLE_TEXT_SIZE);\n        return this;\n    }\n\n    public Builder setTitleText(CharSequence text, @Dimension(unit = SP) int textSize) {\n        this.setTitleText(text, textSize, TextViewOptions.DEFAULT_TEXT_COLOR);\n        return this;\n    }\n\n    public Builder setTitleText(CharSequence text, @Dimension(unit = SP) int textSize, @ColorInt int textColor) {\n        this.setTitleText(\n                TextViewOptions.Builder()\n                        .setText(text)\n                        .setTextSize(textSize)\n                        .setTextColor(textColor)\n                        .build()\n        );\n        return this;\n    }\n\n    public Builder setTitleText(@NonNull TextViewOptions ops) {\n        mTitleTextOps = ops;\n        return this;\n    }\n\n    /**\n     * Set image associated with this toolbar title.\n     */\n    public Builder setTitleImage(@DrawableRes int drawableRes) {\n        return setTitleImage(drawableRes, INVALIDATE, INVALIDATE);\n    }\n\n    public Builder setTitleImage(@DrawableRes int drawableRes, @Dimension(unit = DP) int width,\n                                 @Dimension(unit = DP) int height) {\n        return setTitleImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(drawableRes)\n                        .setWidthWithoutPadding(Utils.dp2px(mContext, width))\n                        .setHeightWithoutPadding(Utils.dp2px(mContext, height))\n                        .build()\n        );\n    }\n\n    public Builder setTitleImage(@NonNull ImageViewOptions ops) {\n        mTitleImageOps = ops;\n        return this;\n    }\n\n    /**\n     * Add custom view associated with this toolbar title.\n     */\n    public Builder addTitleView(@NonNull View view) {\n        return addTitleView(view, null);\n    }\n\n    public Builder addTitleView(View view, Options ops) {\n        mTitleEntities.add(new Entity(view, ops));\n        return this;\n    }\n\n    /**\n     * Add back icon associated with this toolbar left menu.\n     */\n    public Builder addBackIcon(@DrawableRes int drawableRes) {\n        return addLeftMenuImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(drawableRes)\n                        .setListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                ((Activity) mContext).finish();\n                            }\n                        })\n                        .build()\n        );\n    }\n\n    /**\n     * Add sub item associated with this toolbar left menu.\n     */\n    public Builder addLeftMenuText(@NonNull TextViewOptions ops) {\n        return addLeftMenuView(null, ops);\n    }\n\n    public Builder addLeftMenuImage(@NonNull ImageViewOptions ops) {\n        return addLeftMenuView(null, ops);\n    }\n\n    public Builder addLeftMenuView(@NonNull View view) {\n        return addLeftMenuView(view, null);\n    }\n\n    public Builder addLeftMenuView(@Nullable View view, @Nullable Options ops) {\n        mMenuLeftEntities.add(new Entity(view, ops));\n        return this;\n    }\n\n    /**\n     * Add sub item associated with this toolbar right menu.\n     */\n    public Builder addRightMenuText(@NonNull TextViewOptions ops) {\n        return addRightMenuView(null, ops);\n    }\n\n    public Builder addRightMenuImage(@NonNull ImageViewOptions ops) {\n        return addRightMenuView(null, ops);\n    }\n\n    public Builder addRightMenuView(@NonNull View view) {\n        return addRightMenuView(view, null);\n    }\n\n    public Builder addRightMenuView(@NonNull View view, @NonNull Options ops) {\n        mMenuRightEntities.add(new Entity(view, ops));\n        return this;\n    }\n\n    /**\n     * Instantiation SToolbar.\n     */\n    public SToolbar build() {\n        final SToolbar toolbar = new SToolbar(mContext);\n        completion(toolbar);\n        return toolbar;\n    }\n\n    /**\n     * Instantiation SToolbar, and then add it to suitable position.\n     */\n    public SToolbar apply() {\n        final SToolbar toolbar = build();\n        // Add to container.\n        mContentParent.addView(toolbar, 0);\n        // 等待 View 的 performTraversal 完成\n        toolbar.post(new Runnable() {\n            @Override\n            public void run() {\n                adjustLayout(toolbar);\n            }\n        });\n        return toolbar;\n    }\n\n    /**\n     * Inject data to toolbar.\n     */\n    private void completion(SToolbar toolbar) {\n        // 1. Set layout params associated with the toolbar.\n        toolbar.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT));\n        // 2. Set arguments.\n        if (INVALIDATE != mMinimumHeight) {\n            toolbar.setMinimumHeight(Utils.dp2px(mContext, mMinimumHeight));\n        }\n        if (INVALIDATE != mSubItemInterval) {\n            toolbar.setSubItemInterval(Utils.dp2px(mContext, mSubItemInterval));\n        }\n        if (Style.DEFAULT != mStyle) {\n            toolbar.setStatusBarStyle(mStyle);\n        }\n        toolbar.setBackgroundColor(mBackgroundColor);\n        if (INVALIDATE != mBackgroundDrawableResId) {\n            toolbar.setBackgroundDrawableRes(mBackgroundDrawableResId);\n        }\n        toolbar.setDividingLineColor(mDividingLineColor);\n        toolbar.setDividingLineHeight(mDividingLineHeight);\n        // 3. Setup title items associated with the toolbar.\n        toolbar.setTitleGravity(mTitleGravity);\n        if (null != mTitleTextOps) {\n            toolbar.setTitleText(mTitleTextOps);\n        }\n        if (null != mTitleImageOps) {\n            toolbar.setTitleImage(mTitleImageOps);\n        }\n        if (Utils.isNotEmpty(mTitleEntities)) {\n            for (Entity titleEntity : mTitleEntities) {\n                toolbar.addTitleView(titleEntity.view, titleEntity.op);\n            }\n        }\n        // 4. Add left menu items associated with the toolbar.\n        if (Utils.isNotEmpty(mMenuLeftEntities)) {\n            for (Entity leftItem : mMenuLeftEntities) {\n                if (null != leftItem.view && null != leftItem.op) {\n                    toolbar.addLeftMenuView(leftItem.view, leftItem.op);\n                } else if (null != leftItem.op) {\n                    if (leftItem.op instanceof TextViewOptions) {\n                        toolbar.addLeftMenuText((TextViewOptions) leftItem.op);\n                    } else if (leftItem.op instanceof ImageViewOptions) {\n                        toolbar.addLeftMenuImage((ImageViewOptions) leftItem.op);\n                    } else {\n                        throw new NullPointerException(\"U setup ops cannot support auto generate view, \" +\n                                \" option is :\" + leftItem.op);\n                    }\n                } else if (null != leftItem.view) {\n                    toolbar.addLeftMenuView(leftItem.view);\n                } else {\n                    throw new NullPointerException(\"Please ensure ops or view at least one nonnull\");\n                }\n            }\n        }\n        // 5. Add right menu items associated with the toolbar.\n        if (Utils.isNotEmpty(mMenuRightEntities)) {\n            for (Entity rightEntity : mMenuRightEntities) {\n                if (null != rightEntity.view && null != rightEntity.op) {\n                    toolbar.addRightMenuView(rightEntity.view, rightEntity.op);\n                } else if (null != rightEntity.op) {\n                    if (rightEntity.op instanceof TextViewOptions) {\n                        toolbar.addRightMenuText((TextViewOptions) rightEntity.op);\n                    } else if (rightEntity.op instanceof ImageViewOptions) {\n                        toolbar.addRightMenuImage((ImageViewOptions) rightEntity.op);\n                    } else {\n                        throw new NullPointerException(\"U setup ops cannot support auto generate view, \" +\n                                \" option is :\" + rightEntity.op);\n                    }\n                } else if (null != rightEntity.view) {\n                    toolbar.addRightMenuView(rightEntity.view);\n                } else {\n                    throw new NullPointerException(\"Please ensure ops or view at least one nonnull\");\n                }\n            }\n        }\n    }\n\n    /**\n     * Adjust origin content to comfort position.\n     */\n    private void adjustLayout(SToolbar toolbar) {\n        if (null != mContentParent && !(mContentParent instanceof LinearLayout)) {\n            // Move origin content under the SToolbar.\n            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)\n                    mContentParent.getChildAt(1).getLayoutParams();\n            params.topMargin += toolbar.getHeight();\n            mContentParent.getChildAt(1).setLayoutParams(params);\n        }\n    }\n\n    /**\n     * The entity save an instance of view and the view mapper special Options.\n     */\n    private static class Entity {\n\n        View view;\n        Options op;\n\n        Entity(View view, Options op) {\n            this.view = view;\n            this.op = op;\n        }\n\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/ImageViewOptions.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\n\nimport androidx.annotation.Dimension;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\n\nimport static androidx.annotation.Dimension.PX;\n\n/**\n * Options associated with ImageView.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/28 8:50\n */\npublic class ImageViewOptions implements Options<ImageView> {\n\n    /*\n      Constants\n     */\n    static final int UN_INITIALIZE_RES_ID = -1;\n    static final ImageView.ScaleType DEFAULT_SCALE_TYPE = ImageView.ScaleType.CENTER_CROP;\n    static final int DEFAULT_WIDTH = ViewGroup.LayoutParams.WRAP_CONTENT;\n    static final int DEFAULT_Height = ViewGroup.LayoutParams.WRAP_CONTENT;\n    static final int DEFAULT_PADDING = 0;\n    /*\n      Fields associated with image menu.\n    */\n    @DrawableRes\n    int drawableResId = UN_INITIALIZE_RES_ID;\n    ImageView.ScaleType scaleType = DEFAULT_SCALE_TYPE;\n    // Widget padding\n    @Dimension(unit = PX)\n    int paddingLeft = DEFAULT_PADDING;\n    @Dimension(unit = PX)\n    int paddingRight = DEFAULT_PADDING;\n    // Layout params\n    @Dimension(unit = PX)\n    int widthExcludePadding = DEFAULT_WIDTH;\n    @Dimension(unit = PX)\n    int heightExcludePadding = DEFAULT_Height;\n    // listener callback.\n    View.OnClickListener listener = null;\n\n    private ImageViewOptions() {\n    }\n\n    /**\n     * U can get Builder instance from here.\n     */\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    /**\n     * U can rebuild Options instance from here.\n     */\n    public Builder newBuilder() {\n        return new Builder(this);\n    }\n\n    @Override\n    public void completion(ImageView view) {\n        // Set padding.\n        view.setPadding(paddingLeft, 0, paddingRight, 0);\n        // Set the layout parameters associated with this textView.\n        int validWidth = Utils.isLayoutParamsSpecialValue(widthExcludePadding) ? widthExcludePadding :\n                widthExcludePadding + view.getPaddingLeft() + view.getPaddingRight();\n        int validHeight = Utils.isLayoutParamsSpecialValue(heightExcludePadding) ? heightExcludePadding :\n                heightExcludePadding + view.getPaddingTop() + view.getPaddingBottom();\n        ViewGroup.LayoutParams params = view.getLayoutParams();\n        if (null == params) {\n            params = new ViewGroup.LayoutParams(validWidth, validHeight);\n        } else {\n            params.width = validWidth;\n            params.height = validHeight;\n        }\n        view.setLayoutParams(params);\n        // Set OnClickListener\n        if (null != listener) {\n            view.setOnClickListener(listener);\n        }\n        // Set some fields associated with this imageView.\n        view.setImageResource(drawableResId);\n        view.setScaleType(scaleType);\n    }\n\n    /**\n     * Copy values from other instance.\n     */\n    private void copyFrom(@NonNull ImageViewOptions other) {\n        this.drawableResId = other.drawableResId;\n        this.scaleType = other.scaleType;\n        this.paddingLeft = other.paddingLeft;\n        this.paddingRight = other.paddingRight;\n        this.heightExcludePadding = other.heightExcludePadding;\n        this.widthExcludePadding = other.widthExcludePadding;\n        this.listener = other.listener;\n    }\n\n    /**\n     * Builder Options instance more easier.\n     */\n    public static class Builder {\n\n        private ImageViewOptions op;\n\n        private Builder() {\n            op = new ImageViewOptions();\n        }\n\n        private Builder(@NonNull ImageViewOptions other) {\n            this();\n            op.copyFrom(other);\n        }\n\n        public Builder setDrawableResId(@DrawableRes int drawableResId) {\n            op.drawableResId = drawableResId;\n            return this;\n        }\n\n        public Builder setScaleType(ImageView.ScaleType scaleType) {\n            op.scaleType = scaleType;\n            return this;\n        }\n\n        public Builder setPaddingLeft(@Dimension(unit = PX) int paddingLeft) {\n            op.paddingLeft = paddingLeft;\n            return this;\n        }\n\n        public Builder setPaddingRight(@Dimension(unit = PX) int paddingRight) {\n            op.paddingRight = paddingRight;\n            return this;\n        }\n\n        public Builder setWidthWithoutPadding(@Dimension(unit = PX) int widthExcludePadding) {\n            op.widthExcludePadding = widthExcludePadding;\n            return this;\n        }\n\n        public Builder setHeightWithoutPadding(@Dimension(unit = PX) int heightExcludePadding) {\n            op.heightExcludePadding = heightExcludePadding;\n            return this;\n        }\n\n        public Builder setListener(View.OnClickListener listener) {\n            op.listener = listener;\n            return this;\n        }\n\n        public ImageViewOptions build() {\n            return op;\n        }\n\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/Options.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.view.View;\n\n/**\n * Options associated with <T extends View>\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/28 8:43\n */\npublic interface Options<T extends View> {\n\n    /**\n     * U can use this options to completion view\n     */\n    void completion(T view);\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/SToolbar.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.Dimension;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.ViewCompat;\n\nimport com.sharry.lib.album.R;\n\nimport static androidx.annotation.Dimension.DP;\nimport static androidx.annotation.Dimension.PX;\nimport static androidx.annotation.Dimension.SP;\n\n/**\n * SToolbar 的最小高度为系统 ActionBar 的高度\n * <p>\n * 1. 可以直接在 Xml 文件中直接使用\n * 2. 可以使用 Builder 动态的植入 {@link Builder}\n *\n * @author Sharry <a href=\"frankchoochina@gmail.com\">Contact me.</a>\n * @version 3.2\n * @since 2018/8/27 23:20\n */\npublic class SToolbar extends Toolbar {\n\n\n    /*\n       Constants\n     */\n    private static final int LOCKED_CHILDREN_COUNT = 3;\n    private static final int DEFAULT_INTERVAL = 5;\n    private final Rect mDividingLineRegion = new Rect();\n    private final Paint mDividingLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);\n    /*\n       Fields\n     */\n    @Dimension(unit = SP)\n    private int mTitleTextSize = TextViewOptions.DEFAULT_TITLE_TEXT_SIZE;\n    @Dimension(unit = SP)\n    private int mMenuTextSize = TextViewOptions.DEFAULT_MENU_TEXT_SIZE;\n    @Dimension(unit = PX)\n    private int mMinimumHeight;\n    @Dimension(unit = PX)\n    private int mSubItemInterval;\n    @Dimension(unit = PX)\n    private int mDividingLineHeight = 0;\n    @ColorInt\n    private int mTitleTextColor = TextViewOptions.DEFAULT_TEXT_COLOR;\n    @ColorInt\n    private int mMenuTextColor = TextViewOptions.DEFAULT_TEXT_COLOR;\n    /*\n       Views.\n     */\n    private LinearLayout mLeftMenuContainer;\n    private LinearLayout mCenterContainer;\n    private LinearLayout mRightMenuContainer;\n    private TextView mTitleText;\n    private ImageView mTitleImage;\n\n    public SToolbar(Context context) {\n        this(context, null);\n    }\n\n    public SToolbar(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        setWillNotDraw(false);\n        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SToolbar);\n        // Initialize default arguments before views initialing.\n        initDefaultArgs(context, array);\n        // Initialize views.\n        initViews(context);\n        // Dividing line\n        setDividingLineColor(array.getColor(R.styleable.SToolbar_dividingLineColor, Color.LTGRAY));\n        setDividingLineHeight(Utils.px2dp(context, array.getDimensionPixelSize(R.styleable.SToolbar_dividingLineHeight, 0)));\n        // Set status bar style.\n        switch (array.getInt(R.styleable.SToolbar_statusBarStyle, Style.DEFAULT.getVal())) {\n            case 0:\n                setStatusBarStyle(Style.TRANSPARENT);\n                break;\n            case 1:\n                setStatusBarStyle(Style.TRANSLUCENCE);\n                break;\n            case 2:\n                setStatusBarStyle(Style.HIDE);\n                break;\n            default:\n                break;\n        }\n        // Set title gravity.\n        switch (array.getInt(R.styleable.SToolbar_titleGravity, -1)) {\n            case 0:\n                setTitleGravity(Gravity.LEFT | Gravity.TOP);\n                break;\n            case 1:\n                setTitleGravity(Gravity.RIGHT | Gravity.TOP);\n                break;\n            default:\n                setTitleGravity(Gravity.CENTER | Gravity.TOP);\n                break;\n        }\n        // Add text title.\n        String titleText = array.getString(R.styleable.SToolbar_titleText);\n        setTitleText(TextUtils.isEmpty(titleText) ? \"\" : titleText, mTitleTextSize, mTitleTextColor);\n        // Add image title.\n        int titleImageResId = array.getResourceId(R.styleable.SToolbar_titleImage, View.NO_ID);\n        if (View.NO_ID != titleImageResId) {\n            setTitleImage(titleImageResId);\n        }\n        // Add left menu sub item.\n        int backIconResId = array.getResourceId(R.styleable.SToolbar_backIcon, View.NO_ID);\n        if (View.NO_ID != backIconResId) {\n            addBackIcon(backIconResId);\n        }\n        int leftMenuIconResId = array.getResourceId(R.styleable.SToolbar_menuLeftIcon, View.NO_ID);\n        if (View.NO_ID != leftMenuIconResId) {\n            addLeftMenuImage(ImageViewOptions.Builder().setDrawableResId(leftMenuIconResId).build());\n        }\n        String leftMenuText = array.getString(R.styleable.SToolbar_menuLeftText);\n        if (null != leftMenuText) {\n            addLeftMenuText(\n                    TextViewOptions.Builder()\n                            .setText(leftMenuText)\n                            .setTextSize(mMenuTextSize)\n                            .setTextColor(mMenuTextColor)\n                            .build()\n            );\n        }\n        // Add right menu sub item.\n        String rightMenuText = array.getString(R.styleable.SToolbar_menuRightText);\n        if (null != rightMenuText) {\n            addRightMenuText(\n                    TextViewOptions.Builder()\n                            .setText(rightMenuText)\n                            .setTextSize(mMenuTextSize)\n                            .setTextColor(mMenuTextColor)\n                            .build()\n            );\n        }\n        int rightMenuIconResId = array.getResourceId(R.styleable.SToolbar_menuRightIcon, View.NO_ID);\n        if (View.NO_ID != rightMenuIconResId) {\n            addRightMenuImage(ImageViewOptions.Builder().setDrawableResId(rightMenuIconResId).build());\n        }\n        array.recycle();\n    }\n\n    /**\n     * Get Builder instance\n     * If U want create CommonToolbar dynamic, U should invoke this method.\n     */\n    public static Builder Builder(Context context) {\n        return new Builder(context);\n    }\n\n    /**\n     * Get Builder instance\n     * If U want create CommonToolbar dynamic, U should invoke this method.\n     */\n    public static Builder Builder(View contentView) {\n        return new Builder(contentView);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        mDividingLineRegion.left = getPaddingLeft();\n        mDividingLineRegion.right = getMeasuredWidth() - getPaddingRight();\n        mDividingLineRegion.bottom = getMeasuredHeight() - getPaddingBottom();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        if (mDividingLineHeight > 0) {\n            ViewCompat.setElevation(this, 0);\n            mDividingLineRegion.top = mDividingLineRegion.bottom - mDividingLineHeight;\n            canvas.drawRect(mDividingLineRegion, mDividingLinePaint);\n        }\n    }\n\n    @Override\n    public void setLayoutParams(ViewGroup.LayoutParams params) {\n        // Lock heightExcludePadding always is WRAP_CONTENT.\n        if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) {\n            params.height = ViewGroup.LayoutParams.WRAP_CONTENT;\n        }\n        super.setLayoutParams(params);\n    }\n\n    /**\n     * Set app bar style associated with this Activity.\n     */\n    public void setStatusBarStyle(Style style) {\n        AppBarHelper.with(getContext()).setStatusBarStyle(style).apply();\n        if (Utils.isLollipop() && (style == Style.TRANSPARENT || style == Style.TRANSLUCENCE)) {\n            // Setup padding.\n            setPadding(getPaddingLeft(), getPaddingTop() + Utils.getStatusBarHeight(getContext()),\n                    getPaddingRight(), getPaddingBottom());\n        }\n    }\n\n    /**\n     * Sets the background color to a given resource. The colorResId should refer to\n     * a color int.\n     */\n    public void setBackgroundColorRes(@ColorRes int colorResId) {\n        setBackgroundColor(ContextCompat.getColor(getContext(), colorResId));\n    }\n\n    /**\n     * Set the background to a given resource. The resource should refer to\n     * a Drawable object or 0 to remove the background.\n     */\n    public void setBackgroundDrawableRes(@DrawableRes int drawableRes) {\n        setBackgroundResource(drawableRes);\n    }\n\n    /**\n     * Set the color to a given resource. The colorResId should refer to\n     * a color int.\n     */\n    public void setDividingLineColorRes(@ColorRes int colorRes) {\n        setDividingLineColor(ContextCompat.getColor(getContext(), colorRes));\n    }\n\n    /**\n     * Set the color to dividing line.\n     */\n    public void setDividingLineColor(@ColorInt int color) {\n        mDividingLinePaint.setColor(color);\n    }\n\n    /**\n     * Set diving line height.\n     */\n    public void setDividingLineHeight(@Dimension(unit = DP) int dividingLineHeight) {\n        this.mDividingLineHeight = Utils.dp2px(getContext(), dividingLineHeight);\n    }\n\n    /**\n     * Set gravity for the title associated with these LayoutParams.\n     *\n     * @see Gravity\n     */\n    public void setTitleGravity(int gravity) {\n        LayoutParams params = (LayoutParams) mCenterContainer.getLayoutParams();\n        params.gravity = gravity;\n        mCenterContainer.setLayoutParams(params);\n    }\n\n    /**\n     * Get text title associated with this toolbar.\n     */\n    public TextView getTitleText() {\n        if (null == mTitleText) {\n            mTitleText = createTextView();\n            addTitleView(mTitleText);\n        }\n        return mTitleText;\n    }\n\n    /**\n     * Set text associated with this toolbar title.\n     */\n    public void setTitleText(@StringRes int stringResId) {\n        this.setTitleText(getResources().getText(stringResId));\n    }\n\n    public void setTitleText(@NonNull CharSequence text) {\n        this.setTitleText(text, mTitleTextSize);\n    }\n\n    public void setTitleText(@NonNull TextViewOptions ops) {\n        ops.newBuilder()\n                .setTextSize(TextViewOptions.UN_INITIALIZE_TEXT_SIZE != ops.textSize\n                        ? ops.textSize : mTitleTextSize)\n                .setPaddingLeft(TextViewOptions.DEFAULT_PADDING != ops.paddingLeft\n                        ? ops.paddingLeft : mSubItemInterval)\n                .setPaddingRight(TextViewOptions.DEFAULT_PADDING != ops.paddingRight\n                        ? ops.paddingRight : mSubItemInterval)\n                .build()\n                .completion(getTitleText());\n    }\n\n    /**\n     * Get image title associated with this toolbar.\n     */\n    public ImageView getTitleImage() {\n        if (null == mTitleImage) {\n            mTitleImage = createImageView();\n            addTitleView(mTitleImage);\n        }\n        return mTitleImage;\n    }\n\n    /**\n     * Set image associated with this toolbar title.\n     */\n    public void setTitleImage(@DrawableRes int resId) {\n        this.setTitleImage(resId, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n    }\n\n    public void setTitleImage(@NonNull ImageViewOptions ops) {\n        ops.newBuilder()\n                .setPaddingLeft(ImageViewOptions.DEFAULT_PADDING != ops.paddingLeft\n                        ? ops.paddingLeft : mSubItemInterval)\n                .setPaddingRight(ImageViewOptions.DEFAULT_PADDING != ops.paddingRight\n                        ? ops.paddingRight : mSubItemInterval)\n                .build()\n                .completion(getTitleImage());\n    }\n\n    public void setTitleText(@NonNull CharSequence text, @Dimension(unit = SP) int textSize) {\n        this.setTitleText(text, textSize, mTitleTextColor);\n    }\n\n    public void setTitleText(@NonNull CharSequence text, @Dimension(unit = SP) int textSize, @ColorInt int textColor) {\n        this.setTitleText(\n                TextViewOptions.Builder()\n                        .setText(text)\n                        .setTextSize(textSize)\n                        .setTextColor(textColor)\n                        .build()\n        );\n    }\n\n    public void setTitleImage(@DrawableRes int resId, @Dimension(unit = DP) int width,\n                              @Dimension(unit = DP) int height) {\n        this.setTitleImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(resId)\n                        .setWidthWithoutPadding(width)\n                        .setHeightWithoutPadding(height)\n                        .build()\n        );\n    }\n\n    public void addTitleView(@NonNull View view) {\n        addTitleView(view, null);\n    }\n\n    /**\n     * Add custom view associated with this toolbar title.\n     * U can set view more easier when U use Options.\n     */\n    public void addTitleView(@NonNull View view, @Nullable Options ops) {\n        if (null != ops) {\n            ops.completion(view);\n        }\n        mCenterContainer.addView(view);\n    }\n\n    /**\n     * Add back icon associated with this toolbar left menu.\n     */\n    public void addBackIcon(@DrawableRes int drawableRes) {\n        this.addLeftMenuImage(\n                ImageViewOptions.Builder()\n                        .setDrawableResId(drawableRes)\n                        .setListener(new OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                if (getContext() instanceof Activity) {\n                                    ((Activity) getContext()).onBackPressed();\n                                }\n                            }\n                        })\n                        .build()\n        );\n    }\n\n    /**\n     * Add text sub item associated with this toolbar left menu.\n     */\n    public void addLeftMenuText(@NonNull TextViewOptions ops) {\n        addLeftMenuView(createTextView(), ops.newBuilder()\n                .setTextSize(TextViewOptions.UN_INITIALIZE_TEXT_SIZE != ops.textSize\n                        ? ops.textSize : mMenuTextSize)\n                .setPaddingLeft(TextViewOptions.DEFAULT_PADDING != ops.paddingLeft\n                        ? ops.paddingLeft : mSubItemInterval)\n                .build());\n    }\n\n    /**\n     * Add image sub item associated with this toolbar left menu.\n     */\n    public void addLeftMenuImage(@NonNull ImageViewOptions ops) {\n        addLeftMenuView(createImageView(), ops.newBuilder()\n                .setPaddingLeft(ImageViewOptions.DEFAULT_PADDING != ops.paddingLeft\n                        ? ops.paddingLeft : mSubItemInterval)\n                .build());\n    }\n\n    /**\n     * Add custom sub item associated with this toolbar left menu.\n     */\n    public void addLeftMenuView(@NonNull View view) {\n        addLeftMenuView(view, null);\n    }\n\n    /**\n     * Add custom sub item associated with this toolbar left menu.\n     */\n    public void addLeftMenuView(@NonNull View view, @Nullable Options ops) {\n        if (null != ops) {\n            ops.completion(view);\n        }\n        mLeftMenuContainer.addView(view);\n    }\n\n    /**\n     * Add text sub item associated with this toolbar right menu.\n     */\n    public void addRightMenuText(@NonNull TextViewOptions ops) {\n        addRightMenuView(createTextView(), ops.newBuilder()\n                .setTextSize(TextViewOptions.UN_INITIALIZE_TEXT_SIZE != ops.textSize\n                        ? ops.textSize : mMenuTextSize)\n                .setPaddingRight(TextViewOptions.DEFAULT_PADDING != ops.paddingRight\n                        ? ops.paddingRight : mSubItemInterval)\n                .build());\n    }\n\n    /**\n     * Add image sub item associated with this toolbar right menu.\n     */\n    public void addRightMenuImage(@NonNull ImageViewOptions ops) {\n        addRightMenuView(createImageView(), ops.newBuilder()\n                .setPaddingRight(ImageViewOptions.DEFAULT_PADDING != ops.paddingLeft\n                        ? ops.paddingLeft : mSubItemInterval)\n                .build());\n    }\n\n    /**\n     * Add custom sub item associated with this toolbar left menu.\n     */\n    public void addRightMenuView(@NonNull View view) {\n        addRightMenuView(view, null);\n    }\n\n    /**\n     * Add custom sub item associated with this toolbar right menu.\n     * U can set view more easier when U use Options.\n     */\n    public void addRightMenuView(@NonNull View view, @Nullable Options ops) {\n        if (null != ops) {\n            ops.completion(view);\n        }\n        mRightMenuContainer.addView(view);\n    }\n\n    /**\n     * Get view index of left menu.\n     */\n    public <T extends View> T getLeftMenuView(int index) {\n        return (T) mLeftMenuContainer.getChildAt(index);\n    }\n\n    /**\n     * Get view index of right menu.\n     */\n    public <T extends View> T getRightMenuView(int index) {\n        return (T) mRightMenuContainer.getChildAt(index);\n    }\n\n    @Override\n    public void addView(View child, int index, ViewGroup.LayoutParams params) {\n        if (LOCKED_CHILDREN_COUNT == getChildCount()) {\n            return;\n        }\n        super.addView(child, index, params);\n    }\n\n    @Override\n    public void setMinimumHeight(int minimumHeight) {\n        mMinimumHeight = minimumHeight;\n        // Reset container minimumHeight\n        mLeftMenuContainer.setMinimumHeight(mMinimumHeight);\n        mRightMenuContainer.setMinimumHeight(mMinimumHeight);\n        mCenterContainer.setMinimumHeight(mMinimumHeight);\n    }\n\n    /**\n     * Set item horizontal interval associated with this toolbar.\n     */\n    void setSubItemInterval(int subItemInterval) {\n        mSubItemInterval = subItemInterval;\n    }\n\n    private void initDefaultArgs(Context context, TypedArray array) {\n        mMinimumHeight = array.getDimensionPixelSize(R.styleable.SToolbar_minHeight, Utils.dp2px(context, 56));\n        mSubItemInterval = array.getDimensionPixelSize(R.styleable.SToolbar_subItemInterval,\n                Utils.dp2px(context, DEFAULT_INTERVAL));\n        mTitleTextColor = array.getColor(R.styleable.SToolbar_titleTextColor, mTitleTextColor);\n        mTitleTextSize = Utils.px2dp(context, array.getDimensionPixelSize(R.styleable.SToolbar_titleTextSize,\n                Utils.dp2px(context, mTitleTextSize)));\n        mMenuTextSize = Utils.px2dp(context, array.getDimensionPixelSize(R.styleable.SToolbar_menuTextSize,\n                Utils.dp2px(context, mMenuTextSize)));\n        mMenuTextColor = array.getColor(R.styleable.SToolbar_menuTextColor, mMenuTextColor);\n    }\n\n    private void initViews(Context context) {\n        // Set initialize layout params.\n        removeAllViews();\n        // 1. Add left menu container associated with this toolbar.\n        mLeftMenuContainer = new LinearLayout(context);\n        LayoutParams leftParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT);\n        leftParams.gravity = Gravity.START | Gravity.TOP;\n        mLeftMenuContainer.setLayoutParams(leftParams);\n        mLeftMenuContainer.setMinimumHeight(mMinimumHeight);\n        mLeftMenuContainer.setGravity(Gravity.CENTER_VERTICAL);\n        addView(mLeftMenuContainer);\n        // 2. Add right menu container associated with this toolbar.\n        mRightMenuContainer = new LinearLayout(context);\n        LayoutParams rightParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT);\n        rightParams.gravity = Gravity.END | Gravity.TOP;\n        mRightMenuContainer.setLayoutParams(rightParams);\n        mRightMenuContainer.setMinimumHeight(mMinimumHeight);\n        mRightMenuContainer.setGravity(Gravity.CENTER_VERTICAL);\n        addView(mRightMenuContainer);\n        // 3. Add center item container associated with this toolbar.\n        mCenterContainer = new LinearLayout(context);\n        LayoutParams centerParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT);\n        centerParams.gravity = Gravity.CENTER | Gravity.TOP;\n        mCenterContainer.setMinimumHeight(mMinimumHeight);\n        mCenterContainer.setPadding(mSubItemInterval, 0, mSubItemInterval, 0);\n        mCenterContainer.setLayoutParams(centerParams);\n        mCenterContainer.setGravity(Gravity.CENTER_VERTICAL);\n        addView(mCenterContainer);\n    }\n\n    /**\n     * Get TextView instance.\n     */\n    private TextView createTextView() {\n        TextView textView = new TextView(getContext());\n        // Set params for the view.\n        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.MATCH_PARENT\n        );\n        textView.setLayoutParams(params);\n        textView.setGravity(Gravity.CENTER);\n        return textView;\n    }\n\n    /**\n     * Get ImageView instance.\n     */\n    private ImageView createImageView() {\n        // Create ImageView instance.\n        ImageView imageView = new ImageView(getContext());\n        // Set default layout params.\n        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT\n        );\n        imageView.setLayoutParams(params);\n        return imageView;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/Style.java",
    "content": "package com.sharry.lib.album.toolbar;\n\n/**\n * StatusBar/NavigationBar 的样式\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.2\n * @since 2017/10/10 13:52\n */\npublic enum Style {\n\n    TRANSPARENT(0),\n    TRANSLUCENCE(1),\n    HIDE(3),\n    DEFAULT(4);\n\n    int val;\n\n    Style(int val) {\n        this.val = val;\n    }\n\n    int getVal() {\n        return val;\n    }\n\n}"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/TextViewOptions.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.graphics.Color;\nimport android.text.TextUtils;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Dimension;\nimport androidx.annotation.NonNull;\n\nimport static androidx.annotation.Dimension.PX;\nimport static androidx.annotation.Dimension.SP;\n\n/**\n * Options associated with TextView.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/28 8:49\n */\npublic class TextViewOptions implements Options<TextView> {\n\n    /**\n     * U can get Builder instance from here.\n     */\n    public static Builder Builder() {\n        return new Builder();\n    }\n\n    /*\n     Constants\n    */\n    static final int UN_INITIALIZE_TEXT_SIZE = 0;\n    static final int DEFAULT_TEXT_COLOR = Color.WHITE;\n    static final int DEFAULT_TITLE_TEXT_SIZE = 18;\n    static final int DEFAULT_MENU_TEXT_SIZE = 13;\n    static final int DEFAULT_MAX_EMS = 8;\n    static final int DEFAULT_LINES = 1;\n    static final int DEFAULT_PADDING = 0;\n    static final TextUtils.TruncateAt DEFAULT_ELLIPSIZE = TextUtils.TruncateAt.END;\n\n    /*\n      Fields\n     */\n    CharSequence text;\n    @Dimension(unit = SP)\n    int textSize = UN_INITIALIZE_TEXT_SIZE;\n    @ColorInt\n    int textColor = DEFAULT_TEXT_COLOR;\n    int maxEms = DEFAULT_MAX_EMS;\n    int lines = DEFAULT_LINES;\n    TextUtils.TruncateAt ellipsize = DEFAULT_ELLIPSIZE;\n    // Widget padding\n    @Dimension(unit = PX)\n    int paddingLeft = DEFAULT_PADDING;\n    @Dimension(unit = PX)\n    int paddingRight = DEFAULT_PADDING;\n    // listener callback.\n    View.OnClickListener listener = null;\n\n    private TextViewOptions() {\n    }\n\n    /**\n     * U can rebuild Options instance from here.\n     */\n    public Builder newBuilder() {\n        return new Builder(this);\n    }\n\n    @Override\n    public void completion(TextView textView) {\n        // Set padding.\n        textView.setPadding(paddingLeft, 0, paddingRight, 0);\n        ViewGroup.LayoutParams params = textView.getLayoutParams();\n        if (null == params) {\n            params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,\n                    ViewGroup.LayoutParams.MATCH_PARENT);\n        } else {\n            params.width = ViewGroup.LayoutParams.WRAP_CONTENT;\n            params.height = ViewGroup.LayoutParams.MATCH_PARENT;\n        }\n        textView.setLayoutParams(params);\n        // Set OnClickListener\n        if (null != listener) {\n            textView.setOnClickListener(listener);\n        }\n        // Set some fields associated with this textView.\n        textView.setText(text);\n        textView.setTextColor(textColor);\n        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);\n        textView.setMaxEms(maxEms);\n        textView.setLines(lines);\n        textView.setEllipsize(ellipsize);\n    }\n\n    /**\n     * Copy values from other instance.\n     */\n    private void copyFrom(@NonNull TextViewOptions other) {\n        this.text = other.text;\n        this.textSize = other.textSize;\n        this.textColor = other.textColor;\n        this.maxEms = other.maxEms;\n        this.lines = other.lines;\n        this.ellipsize = other.ellipsize;\n        this.paddingLeft = other.paddingLeft;\n        this.paddingRight = other.paddingRight;\n        this.listener = other.listener;\n    }\n\n    /**\n     * Builder TextOptions instance more easier.\n     */\n    public static class Builder {\n\n        private TextViewOptions op;\n\n        private Builder() {\n            op = new TextViewOptions();\n        }\n\n        private Builder(@NonNull TextViewOptions other) {\n            this();\n            op.copyFrom(other);\n        }\n\n        public Builder setText(@NonNull CharSequence text) {\n            op.text = text;\n            return this;\n        }\n\n        public Builder setTextSize(@Dimension(unit = SP) int textSize) {\n            op.textSize = textSize;\n            return this;\n        }\n\n        public Builder setTextColor(@ColorInt int textColor) {\n            op.textColor = textColor;\n            return this;\n        }\n\n        public Builder setMaxEms(int maxEms) {\n            op.maxEms = maxEms;\n            return this;\n        }\n\n        public Builder setLines(int lines) {\n            op.lines = lines;\n            return this;\n        }\n\n        public Builder setEllipsize(TextUtils.TruncateAt ellipsize) {\n            op.ellipsize = ellipsize;\n            return this;\n        }\n\n        public Builder setPaddingLeft(@Dimension(unit = PX) int paddingLeft) {\n            op.paddingLeft = paddingLeft;\n            return this;\n        }\n\n        public Builder setPaddingRight(@Dimension(unit = PX) int paddingRight) {\n            op.paddingRight = paddingRight;\n            return this;\n        }\n\n        public Builder setListener(View.OnClickListener listener) {\n            op.listener = listener;\n            return this;\n        }\n\n        public TextViewOptions build() {\n            if (null == op.text) {\n                throw new UnsupportedOperationException(\"Please ensure text field nonnull.\");\n            }\n            return op;\n        }\n\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/Utils.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.TypedValue;\nimport android.view.ViewGroup;\n\nimport java.util.Collection;\n\n/**\n * @author Sharry <a href=\"frankchoochina@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/8/27 23:21\n */\nclass Utils {\n\n    Utils() {\n        throw new UnsupportedOperationException(this + \" cannot be instantiated\");\n    }\n\n    /**\n     * 判断是否为 5.0 以上的系统\n     *\n     * @return if true is over Lollipop, false is below Lollipop.\n     */\n    static boolean isLollipop() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;\n    }\n\n    /**\n     * 判断 Map 是否为空\n     */\n    static boolean isNotEmpty(Collection collection) {\n        return null != collection && collection.size() != 0;\n    }\n\n    /**\n     * 是否为 LayoutParams 特殊的参数\n     */\n    static boolean isLayoutParamsSpecialValue(int paramsValue) {\n        return ViewGroup.LayoutParams.MATCH_PARENT == paramsValue\n                || ViewGroup.LayoutParams.WRAP_CONTENT == paramsValue;\n    }\n\n    /**\n     * @param baseColor    需要进行透明的Color\n     * @param alphaPercent 透明图(0-1)\n     */\n    static int alphaColor(int baseColor, float alphaPercent) {\n        if (alphaPercent < 0) alphaPercent = 0f;\n        if (alphaPercent > 1) alphaPercent = 1f;\n        // 计算基础透明度\n        int baseAlpha = (baseColor & 0xff000000) >>> 24;\n        // 根基需求计算透明度\n        int alpha = (int) (baseAlpha * alphaPercent);\n        // 根基透明度拼接新的color\n        return alpha << 24 | (baseColor & 0xffffff);\n    }\n\n    /**\n     * Dip convert 2 pixel\n     */\n    static int dp2px(Context context, float dp) {\n        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,\n                context.getResources().getDisplayMetrics());\n    }\n\n    /**\n     * Pixel convert 2 dip\n     */\n    static int px2dp(Context context, float px) {\n        float scale = context.getResources().getDisplayMetrics().density;\n        return (int) (px / scale + 0.5f);\n    }\n\n    /**\n     * Get action bar heightExcludePadding associated with the app.\n     */\n    static int getActionBarHeight(Context context) {\n        TypedValue typedValue = new TypedValue();\n        // 将属性解析到TypedValue中\n        context.getTheme().resolveAttribute(android.R.attr.actionBarSize, typedValue, true);\n        return TypedValue.complexToDimensionPixelSize(typedValue.data,\n                context.getResources().getDisplayMetrics());\n    }\n\n    /**\n     * Get status bar heightExcludePadding associated with the app.\n     */\n    static int getStatusBarHeight(Context context) {\n        int resourceId = context.getResources().getIdentifier(\"status_bar_height\",\n                \"dimen\", \"android\");\n        return resourceId > 0 ? context.getResources()\n                .getDimensionPixelSize(resourceId) : 0;\n    }\n\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/ViewOptions.java",
    "content": "package com.sharry.lib.album.toolbar;\n\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.Dimension;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\nimport static androidx.annotation.Dimension.PX;\n\n/**\n * Options associated with view.\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/28 8:48\n */\npublic class ViewOptions implements Options<View> {\n\n    /*\n      Constants\n     */\n    static final int DEFAULT_VISIBILITY = View.VISIBLE;\n    static final int DEFAULT_WIDTH = ViewGroup.LayoutParams.WRAP_CONTENT;\n    static final int DEFAULT_HEIGHT = ViewGroup.LayoutParams.WRAP_CONTENT;\n    static final int DEFAULT_PADDING = 0;\n    int visibility = DEFAULT_VISIBILITY;\n    // Widget padding\n    @Dimension(unit = PX)\n    int paddingLeft = DEFAULT_PADDING;\n    @Dimension(unit = PX)\n    int paddingTop = DEFAULT_PADDING;\n    @Dimension(unit = PX)\n    int paddingRight = DEFAULT_PADDING;\n    @Dimension(unit = PX)\n    int paddingBottom = DEFAULT_PADDING;\n    // Layout params\n    @Dimension(unit = PX)\n    int widthExcludePadding = DEFAULT_WIDTH;\n    @Dimension(unit = PX)\n    int heightExcludePadding = DEFAULT_HEIGHT;\n    // listener callback.\n    View.OnClickListener listener = null;\n    private ViewOptions() {\n    }\n\n    public Builder newBuilder() {\n        return new Builder(this);\n    }\n\n    @Override\n    public void completion(View view) {\n        view.setVisibility(visibility);\n        // Set padding.\n        view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);\n        // Set the layout parameters associated with this textView.\n        int validWidth = Utils.isLayoutParamsSpecialValue(widthExcludePadding) ? widthExcludePadding :\n                widthExcludePadding + view.getPaddingLeft() + view.getPaddingRight();\n        int validHeight = Utils.isLayoutParamsSpecialValue(heightExcludePadding) ? heightExcludePadding :\n                heightExcludePadding + view.getPaddingTop() + view.getPaddingBottom();\n        ViewGroup.LayoutParams params = view.getLayoutParams();\n        if (null == params) {\n            params = new ViewGroup.LayoutParams(validWidth, validHeight);\n        } else {\n            params.width = validWidth;\n            params.height = validHeight;\n        }\n        view.setLayoutParams(params);\n        // Set OnClickListener\n        if (null != listener) {\n            view.setOnClickListener(listener);\n        }\n    }\n\n    /**\n     * Copy values from other instance.\n     */\n    private void copyFrom(ViewOptions other) {\n        this.visibility = other.visibility;\n        this.paddingLeft = other.paddingLeft;\n        this.paddingTop = other.paddingTop;\n        this.paddingRight = other.paddingRight;\n        this.paddingBottom = other.paddingBottom;\n        this.widthExcludePadding = other.widthExcludePadding;\n        this.heightExcludePadding = other.heightExcludePadding;\n    }\n\n    @IntDef({View.VISIBLE, View.INVISIBLE, View.GONE})\n    @Retention(RetentionPolicy.SOURCE)\n    @interface Visibility {\n    }\n\n    /**\n     * Builder TextOptions instance more easier.\n     */\n    public static class Builder {\n\n        private ViewOptions op;\n\n        public Builder() {\n            op = new ViewOptions();\n        }\n\n        private Builder(@NonNull ViewOptions other) {\n            this();\n            op.copyFrom(other);\n        }\n\n        public Builder setVisibility(@Visibility int visibility) {\n            op.visibility = visibility;\n            return this;\n        }\n\n        public Builder setPaddingLeft(@Dimension(unit = PX) int paddingLeft) {\n            op.paddingLeft = paddingLeft;\n            return this;\n        }\n\n        public Builder setPaddingTop(@Dimension(unit = PX) int paddingTop) {\n            op.paddingTop = paddingTop;\n            return this;\n        }\n\n        public Builder setPaddingRight(@Dimension(unit = PX) int paddingRight) {\n            op.paddingRight = paddingRight;\n            return this;\n        }\n\n        public Builder setPaddingBottom(@Dimension(unit = PX) int paddingBottom) {\n            op.paddingBottom = paddingBottom;\n            return this;\n        }\n\n        public Builder setWidthExcludePadding(@Dimension(unit = PX) int widthExcludePadding) {\n            op.widthExcludePadding = widthExcludePadding;\n            return this;\n        }\n\n        public Builder setHeightExcludePadding(@Dimension(unit = PX) int heightExcludePadding) {\n            op.heightExcludePadding = heightExcludePadding;\n            return this;\n        }\n\n        public Builder setListener(View.OnClickListener listener) {\n            op.listener = listener;\n            return this;\n        }\n\n        public Options build() {\n            return op;\n        }\n    }\n}\n"
  },
  {
    "path": "lib-album/src/main/widget/com/sharry/lib/album/toolbar/res/values/lib_toolbar_attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <declare-styleable name=\"SToolbar\">\n        <!--Toolbar-->\n        <attr name=\"statusBarStyle\" format=\"enum\">\n            <enum name=\"Transparent\" value=\"0\" />\n            <enum name=\"Translucence\" value=\"1\" />\n            <enum name=\"Hide\" value=\"2\" />\n            <enum name=\"Default\" value=\"3\" />\n        </attr>\n        <attr name=\"minHeight\" format=\"dimension\" />\n        <attr name=\"subItemInterval\" format=\"dimension\" />\n        <attr name=\"dividingLineHeight\" format=\"dimension\" />\n        <attr name=\"dividingLineColor\" format=\"color\" />\n        <!--Title-->\n        <attr name=\"titleGravity\" format=\"enum\">\n            <enum name=\"Left\" value=\"0\" />\n            <enum name=\"Right\" value=\"1\" />\n            <enum name=\"Center\" value=\"2\" />\n        </attr>\n        <attr name=\"titleText\" format=\"string\" />\n        <attr name=\"titleTextColor\" format=\"color\" />\n        <attr name=\"titleTextSize\" format=\"dimension\" />\n        <attr name=\"titleImage\" format=\"reference\" />\n        <!--Menu-->\n        <attr name=\"backIcon\" format=\"reference\" />\n        <attr name=\"menuTextSize\" format=\"dimension\" />\n        <attr name=\"menuTextColor\" format=\"color\" />\n        <attr name=\"menuLeftText\" format=\"string\" />\n        <attr name=\"menuLeftIcon\" format=\"reference\" />\n        <attr name=\"menuRightText\" format=\"string\" />\n        <attr name=\"menuRightIcon\" format=\"reference\" />\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "lib-media-recorder/.gitignore",
    "content": "# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea\n\n\n# Keystore files\n*.jks\n\n# external files\n.externalNativeBuild/"
  },
  {
    "path": "lib-media-recorder/CMakeLists.txt",
    "content": "# CMake 最小编译版本\nCMAKE_MINIMUM_REQUIRED(VERSION 3.4.1)\n\n# 添加要打包的资源\nFILE(GLOB SRC_LISTS \"${PROJECT_SOURCE_DIR}/src/main/cpp/*.cpp\" \"${PROJECT_SOURCE_DIR}/src/main/cpp/*.c\")\n\nadd_library(\n        smedia-recorder\n        SHARED\n        ${SRC_LISTS}\n)\n\n# 将打包的 so 链接到项目中\ntarget_link_libraries(\n        # 目标库\n        smedia-recorder\n        # Android libs\n        log\n        android\n        OpenSLES\n)"
  },
  {
    "path": "lib-media-recorder/Readme.markdown",
    "content": "\n"
  },
  {
    "path": "lib-media-recorder/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\n\ngroup = 'com.github.SharryChoo'\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n        externalNativeBuild {\n            ndk {\n                abiFilters \"armeabi-v7a\"\n            }\n        }\n    }\n    externalNativeBuild {\n        cmake {\n            path \"CMakeLists.txt\"\n        }\n    }\n    sourceSets {\n        main {\n            java.srcDirs += 'src/main/api'\n            java.srcDirs += 'src/main/recorder'\n            java.srcDirs += 'src/main/encoder'\n            java.srcDirs += 'src/main/pcmprovider'\n            java.srcDirs += 'src/main/muxer'\n            java.srcDirs += 'src/main/utils'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation \"androidx.appcompat:appcompat:$supportLibraryVersion\"\n    api project(':lib-scamera')\n}\n"
  },
  {
    "path": "lib-media-recorder/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "lib-media-recorder/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.sharry.lib.media.recorder\">\n\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\n</manifest>\n"
  },
  {
    "path": "lib-media-recorder/src/main/api/com/sharry/lib/media/recorder/IMediaRecorder.java",
    "content": "package com.sharry.lib.media.recorder;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-15 17:33\n */\npublic interface IMediaRecorder {\n\n    /**\n     * 启动视频的录制\n     */\n    void start();\n\n    /**\n     * 暂停视频的录制\n     */\n    void pause();\n\n    /**\n     * 恢复录制\n     */\n    void resume();\n\n    /**\n     * 取消视频的录制\n     */\n    void cancel();\n\n    /**\n     * 完成视频的录制\n     */\n    void complete();\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/api/com/sharry/lib/media/recorder/IRecorderCallback.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.net.Uri;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * 视频录制回调\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 12/29/2018 1:52 PM\n */\npublic interface IRecorderCallback {\n\n    class Adapter implements IRecorderCallback {\n\n        @Override\n        public void onStart() {\n\n        }\n\n        @Override\n        public void onProgress(long time) {\n\n        }\n\n        @Override\n        public void onCancel() {\n\n        }\n\n        @Override\n        public void onPause() {\n\n        }\n\n        @Override\n        public void onResume() {\n\n        }\n\n        @Override\n        public void onComplete(@NonNull Uri uri, File file) {\n\n        }\n\n        @Override\n        public void onFailed(int errorCode, @NonNull Throwable e) {\n\n        }\n    }\n\n    /**\n     * Outer Constants.\n     */\n    int ERROR_CREATE_FILE_FAILED = -1;        // 创建音频输出文件失败\n    int ERROR_ENCODER_PREPARE_FAILED = -2;    // 编码器准备失败\n    int ERROR_MUXER_PREPARE_FAILED = -3;      // 编码器准备失败\n    int ERROR_START_FAILED = -4;              // 录制开始失败\n    int ERROR_UNSUPPORTED_TYPE = -5;          // 不支持的类型\n    int ERROR_ENCODE_FAILED = -6;             // 编码失败\n    int ERROR_MUXER_FAILED = -7;              // 音视频合并失败\n    int ERROR_RELEASE_FAILED = -8;            // 资源释放失败\n\n    /**\n     * Error code.\n     */\n    @IntDef(flag = true, value = {\n            ERROR_CREATE_FILE_FAILED,\n            ERROR_ENCODER_PREPARE_FAILED,\n            ERROR_MUXER_PREPARE_FAILED,\n            ERROR_START_FAILED,\n            ERROR_UNSUPPORTED_TYPE,\n            ERROR_ENCODE_FAILED,\n            ERROR_MUXER_FAILED,\n            ERROR_RELEASE_FAILED\n    })\n    @Retention(RetentionPolicy.SOURCE)\n    @interface ErrorCode {\n\n    }\n\n    @MainThread\n    void onStart();\n\n    /**\n     * Unit is ms\n     */\n    @MainThread\n    void onProgress(long time);\n\n    @MainThread\n    void onCancel();\n\n    @MainThread\n    void onPause();\n\n    @MainThread\n    void onResume();\n\n    @MainThread\n    void onComplete(@NonNull Uri uri, File file);\n\n    @MainThread\n    void onFailed(@ErrorCode int errorCode, @NonNull Throwable e);\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/api/com/sharry/lib/media/recorder/Options.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-09-05\n */\npublic final class Options {\n\n    public static class Video {\n\n        public static final int RESOLUTION_1080P = 1920 * 1080;\n        public static final int RESOLUTION_720P = 1280 * 720;\n        public static final int RESOLUTION_480P = 720 * 480;\n\n        @IntDef(value = {\n                RESOLUTION_1080P,\n                RESOLUTION_720P,\n                RESOLUTION_480P\n        })\n        public @interface Resolution {\n\n        }\n\n        private static final int DEFAULT_FRAME_RATE = 24;\n\n        /**\n         * 视频的 编码类型\n         */\n        private EncodeType.Video videoEncodeType = EncodeType.Video.H264;\n\n        /**\n         * 录制时, 音频的配置\n         */\n        private Audio audioOps = Audio.DEFAULT;\n\n        /**\n         * 音视频 封装类型\n         */\n        private MuxerType muxerType = MuxerType.MP4;\n\n        /**\n         * 视频的 帧率\n         */\n        private int frameRate = DEFAULT_FRAME_RATE;\n\n        /**\n         * 录制后的 输出目录\n         */\n        private String relativePath;\n\n        private String authority;\n\n        /**\n         * 设置录制分辨率\n         */\n        private int resolution = RESOLUTION_720P;\n\n        private Video() {\n        }\n\n        /**\n         * Get an instance of Builder.\n         */\n        public Builder reBuilder() {\n            return new Builder(this);\n        }\n\n        public EncodeType.Video getVideoEncodeType() {\n            return videoEncodeType;\n        }\n\n        public int getFrameRate() {\n            return frameRate;\n        }\n\n        public int getResolution() {\n            return resolution;\n        }\n\n        public String getRelativePath() {\n            return relativePath;\n        }\n\n        public Audio getAudioOptions() {\n            return audioOps;\n        }\n\n        public MuxerType getMuxerType() {\n            return muxerType;\n        }\n\n        public String getAuthority() {\n            return authority;\n        }\n\n        /**\n         * Build options instance easier.\n         */\n        public static class Builder {\n\n            private Video mOps;\n\n            public Builder() {\n                mOps = new Video();\n            }\n\n            private Builder(Video videoOptions) {\n                this.mOps = videoOptions;\n            }\n\n            /**\n             * 设置帧率\n             */\n            public Builder setFrameRate(int frameRate) {\n                mOps.frameRate = frameRate;\n                return this;\n            }\n\n            /**\n             * 设置视频编码格式(H.264)\n             */\n            public Builder setEncodeType(@NonNull EncodeType.Video type) {\n                mOps.videoEncodeType = type;\n                return this;\n            }\n\n            /**\n             * 设置视频的封装格式( MP4 )\n             */\n            public Builder setMuxerType(@NonNull MuxerType muxerType) {\n                mOps.muxerType = muxerType;\n                return this;\n            }\n\n            /**\n             * 设置录制分辨率\n             */\n            public Builder setResolution(@Resolution int resolution) {\n                mOps.resolution = resolution;\n                return this;\n            }\n\n            /**\n             * 设置文件输出相对路径, 拍摄后的图片会生成在目录下\n             * <p>\n             * 绝对路径: \"/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum\"\n             * 相对路径: \"SAlbum\"\n             * <p>\n             * 注:\n             * Android 10 无法在外部存储卡随意创建文件, 因此会在对应的媒体目录下追加相对路径\n             * 如: \"/storage/emulated/0/\" + {@link android.os.Environment#DIRECTORY_PICTURES} + \"SAlbum\"\n             *\n             * @param relativePath 若是传 null, 则会在 \"/storage/emulated/0/\"\n             *                     + {@link android.os.Environment#DIRECTORY_PICTURES} 中创建\n             */\n            public Builder setRelativePath(@NonNull String relativePath) {\n                mOps.relativePath = relativePath;\n                return this;\n            }\n\n            public Builder setAuthority(@NonNull String authority) {\n                mOps.authority = authority;\n                return this;\n            }\n\n            /**\n             * 设置音频录制的配置\n             */\n            public Builder setAudioOptions(@NonNull Audio audioOptions) {\n                mOps.audioOps = audioOptions;\n                return this;\n            }\n\n            public Video build() {\n                return mOps;\n            }\n        }\n    }\n\n    public static class Audio {\n\n        public static final int SAMPLE_RATE_44100 = 44100;\n        public static final int CHANNEL_LAYOUT_CENTER = 2;\n        public static final int PER_SAMPLE_SIZE = 2;\n\n        @IntDef(flag = true, value = {\n                SAMPLE_RATE_44100,\n        })\n        @Retention(RetentionPolicy.SOURCE)\n        @interface SampleRate {\n        }\n\n        @IntDef(flag = true, value = {\n                CHANNEL_LAYOUT_CENTER,\n        })\n        @Retention(RetentionPolicy.SOURCE)\n        @interface ChannelLayout {\n        }\n\n        @IntDef(flag = true, value = {\n                PER_SAMPLE_SIZE,\n        })\n        @Retention(RetentionPolicy.SOURCE)\n        @interface PerSampleSize {\n        }\n\n        private static final int DEFAULT_MAX_DURATION = Integer.MAX_VALUE;\n        private static final EncodeType.Audio DEFAULT_RECORD_TYPE = EncodeType.Audio.AAC;\n\n        public static final Audio DEFAULT = new Builder().build();\n\n        /**\n         * 音频的采样率\n         */\n        private int sampleRate = SAMPLE_RATE_44100;\n\n        /**\n         * 录制时的声音布局\n         * <p>\n         * 目前只支持立体声\n         */\n        private int channelLayout = CHANNEL_LAYOUT_CENTER;\n\n        /**\n         * 采样点的 byte 数\n         * <p>\n         * 目前只支持 2 byte(16 bit)\n         */\n        private int perSampleSize = PER_SAMPLE_SIZE;\n\n        /**\n         * 录制时长\n         */\n        private int duration = DEFAULT_MAX_DURATION;\n\n        /**\n         * 仅进行编码\n         */\n        private boolean isJustEncode = false;\n\n        /**\n         * 录制后编码的类型\n         */\n        private EncodeType.Audio audioEncodeType = DEFAULT_RECORD_TYPE;\n\n        /**\n         * 设置 PCM 数据提供器\n         */\n        private IPCMProvider pcmProvider;\n\n        /**\n         * 录制后输出的目录\n         */\n        private String relativePath;\n\n        private String authority;\n\n        private Audio() {\n        }\n\n        /**\n         * Get an instance of Builder.\n         */\n        public Builder reBuilder() {\n            return new Builder(this);\n        }\n\n        EncodeType.Audio getAudioEncodeType() {\n            return audioEncodeType;\n        }\n\n        int getDuration() {\n            return duration;\n        }\n\n        String getRelativePath() {\n            return relativePath;\n        }\n\n        int getSampleRate() {\n            return sampleRate;\n        }\n\n        boolean isJustEncode() {\n            return isJustEncode;\n        }\n\n        int getChannelLayout() {\n            return channelLayout;\n        }\n\n        int getPerSampleSize() {\n            return perSampleSize;\n        }\n\n        public String getAuthority() {\n            return authority;\n        }\n\n        IPCMProvider getPcmProvider() {\n            return pcmProvider;\n        }\n\n        /**\n         * Build options instance easier.\n         */\n        public static class Builder {\n\n            private Audio mOps;\n\n            public Builder() {\n                mOps = new Audio();\n            }\n\n            private Builder(Audio Audio) {\n                this.mOps = Audio;\n            }\n\n            /**\n             * 设置采样率\n             */\n            public Builder setSampleSize(@SampleRate int sampleSize) {\n                mOps.sampleRate = sampleSize;\n                return this;\n            }\n\n            /**\n             * 设置音频文件的输出类型\n             */\n            public Builder setEncodeType(EncodeType.Audio type) {\n                mOps.audioEncodeType = type;\n                return this;\n            }\n\n            /**\n             * 设置音频文件最大录制时长\n             *\n             * @param duration Unit millisecond.\n             */\n            public Builder setDuration(int duration) {\n                mOps.duration = duration;\n                return this;\n            }\n\n            /**\n             * 设置文件输出相对路径, 拍摄后的图片会生成在目录下\n             * <p>\n             * 绝对路径: \"/storage/emulated/0/{@link android.os.Environment#DIRECTORY_PICTURES}/SAlbum\"\n             * 相对路径: \"SAlbum\"\n             * <p>\n             * 注:\n             * Android 10 无法在外部存储卡随意创建文件, 因此会在对应的媒体目录下追加相对路径\n             * 如: \"/storage/emulated/0/\" + {@link android.os.Environment#DIRECTORY_PICTURES} + \"SAlbum\"\n             *\n             * @param relativePath 若是传 null, 则会在 \"/storage/emulated/0/\"\n             *                     + {@link android.os.Environment#DIRECTORY_PICTURES} 中创建\n             */\n            public Builder setRelativePath(@NonNull String relativePath) {\n                mOps.relativePath = relativePath;\n                return this;\n            }\n\n            public Builder setAuthority(@NonNull String authority) {\n                mOps.authority = authority;\n                return this;\n            }\n\n            /**\n             * 是否只进行编码, 不进行文件写入\n             */\n            public Builder setIsJustEncode(boolean isJustEncode) {\n                mOps.isJustEncode = isJustEncode;\n                return this;\n            }\n\n            public Builder setPerSampleSize(@PerSampleSize int perSampleSize) {\n                mOps.perSampleSize = perSampleSize;\n                return this;\n            }\n\n            public Builder setChannelLayout(@ChannelLayout int channelLayout) {\n                mOps.channelLayout = channelLayout;\n                return this;\n            }\n\n            public Builder setPcmProvider(@NonNull IPCMProvider pcmProvider) {\n                mOps.pcmProvider = pcmProvider;\n                return this;\n            }\n\n            public Audio build() {\n                return mOps;\n            }\n\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/api/com/sharry/lib/media/recorder/SMediaRecorder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.Manifest;\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RequiresPermission;\n\nimport com.sharry.lib.camera.SCameraView;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-15 17:24\n */\npublic final class SMediaRecorder implements IRecorderCallback {\n\n    private static final String TAG = SMediaRecorder.class.getSimpleName();\n\n    /**\n     * Get instance from here.\n     */\n    public static SMediaRecorder with(@NonNull Context context) {\n        return new SMediaRecorder(context);\n    }\n\n    /**\n     * Constants.\n     */\n    private final List<IRecorderCallback> mCallbacks = new ArrayList<>();\n\n    /**\n     * Fields, init after method init() invoked.\n     */\n    private final Context mContext;\n    private AudioManager mAudioManagerService;\n\n    private SMediaRecorder(Context context) {\n        mContext = context.getApplicationContext();\n        mAudioManagerService = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);\n    }\n\n    @Override\n    public void onStart() {\n        // 获取焦点\n        mAudioManagerService.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,\n                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onStart();\n        }\n    }\n\n    @Override\n    public void onProgress(long time) {\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onProgress(time);\n        }\n    }\n\n    @Override\n    public void onPause() {\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onPause();\n        }\n    }\n\n    @Override\n    public void onResume() {\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onResume();\n        }\n    }\n\n    @Override\n    public void onCancel() {\n        // 释放焦点\n        mAudioManagerService.abandonAudioFocus(null);\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onCancel();\n        }\n    }\n\n    @Override\n    public void onComplete(@NonNull final Uri uri, File file) {\n        // 释放焦点\n        mAudioManagerService.abandonAudioFocus(null);\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onComplete(uri, file);\n        }\n    }\n\n    @Override\n    public void onFailed(@ErrorCode final int errorCode, @NonNull final Throwable e) {\n        Log.w(TAG, \"SMediaRecorder record failed\", e);\n        // 释放焦点\n        mAudioManagerService.abandonAudioFocus(null);\n        for (IRecorderCallback callback : mCallbacks) {\n            callback.onFailed(errorCode, e);\n        }\n    }\n\n    private IMediaRecorder mImpl;\n\n    /**\n     * 开始录制视频\n     */\n    @RequiresPermission(anyOf = {\n            Manifest.permission.CAMERA,\n            Manifest.permission.RECORD_AUDIO,\n            Manifest.permission.READ_EXTERNAL_STORAGE,\n            Manifest.permission.WRITE_EXTERNAL_STORAGE\n    })\n    public void start(SCameraView cameraView, @NonNull Options.Video options) {\n        // 完善 Config\n        completionOptions(options);\n        // 取消之前的录制动作\n        cancel();\n        // 创建录制者\n        mImpl = new VideoRecorder(mContext, options, cameraView, this);\n        // 启动录制者\n        mImpl.start();\n    }\n\n    /**\n     * 开始采集音频\n     * Subsequent calls to {@link #cancel} after start success.\n     * Subsequent calls to {@link #complete} after start success.\n     * <p>\n     * If start success, it will be call method that {@link IRecorderCallback#onStart()}\n     * If start failed, it will be call method that {@link IRecorderCallback#onFailed(int, Throwable)}\n     * </p>\n     *\n     * @param options the options associated with this record.\n     */\n    @RequiresPermission(allOf = {\n            Manifest.permission.RECORD_AUDIO,\n            Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            Manifest.permission.READ_EXTERNAL_STORAGE\n    })\n    public void start(@NonNull Options.Audio options) {\n        // 完善 Config\n        completionOptions(options);\n        // 取消之前的录制动作\n        cancel();\n        // 创建录制者\n        mImpl = new AudioRecorder(mContext, options, this);\n        // 开始录制\n        mImpl.start();\n    }\n\n    /**\n     * 暂停录制\n     */\n    public void pause() {\n        if (mImpl != null) {\n            mImpl.pause();\n        }\n    }\n\n    /**\n     * 恢复录制\n     */\n    public void resume() {\n        if (mImpl != null) {\n            mImpl.resume();\n        }\n    }\n\n    /**\n     * 取消本次的音频采集\n     * <p>\n     * If cancel success, it will be call method that {@link IRecorderCallback#onCancel()}\n     * If cancel failed, it will be call method that {@link IRecorderCallback#onFailed(int, Throwable)}\n     * </p>\n     */\n    public void cancel() {\n        if (mImpl != null) {\n            mImpl.cancel();\n            mImpl = null;\n        }\n    }\n\n    /**\n     * 完成音频采集\n     * <p>\n     * If complete success, it will be call method that {@link IRecorderCallback#onComplete(Uri, File)}\n     * If complete failed, it will be call method that {@link IRecorderCallback#onFailed(int, Throwable)}\n     * </p>\n     */\n    public void complete() {\n        if (mImpl != null) {\n            mImpl.complete();\n            mImpl = null;\n        }\n    }\n\n    /**\n     * 注册音频录制回调\n     * <p>\n     * when use completed to avoid memory leak.\n     */\n    public void addRecordCallback(@NonNull IRecorderCallback callback) {\n        int index = mCallbacks.indexOf(callback);\n        if (index >= 0) {\n            Log.i(TAG, \"This callback already registered.\");\n        } else {\n            mCallbacks.add(callback);\n        }\n    }\n\n    /**\n     * Verify the options legality.\n     */\n    private void completionOptions(@NonNull Options.Audio audioOptions) {\n        // 若没有设置录音文件输出目录, 则指定为 App 的内部缓存目录\n        if (TextUtils.isEmpty(audioOptions.getRelativePath())) {\n            audioOptions.reBuilder().setRelativePath(mContext.getCacheDir().getAbsolutePath());\n        }\n    }\n\n    /**\n     * Verify the options legality.\n     */\n    private void completionOptions(@NonNull Options.Video videoOptions) {\n        // 若没有设置录音文件输出目录, 则指定为 App 的内部缓存目录\n        if (TextUtils.isEmpty(videoOptions.getRelativePath())) {\n            videoOptions.reBuilder().setRelativePath(mContext.getCacheDir().getAbsolutePath());\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/ConstDefine.h",
    "content": "//\n// Created by Sharry Choo on 2019-06-17.\n//\n#ifndef SMEDIA_RECORDER_CONSTDEFINE_H\n#define SMEDIA_RECORDER_CONSTDEFINE_H\n\n#include <android/log.h>\n\n/**\n * 日志相关\n */\n#define TAG \"SMedia-Recorder\"\n#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)\n#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)\n#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)\n\n\n/**\n * 用于和 Java 层回调的类\n */\n#define OPENSLES_PCM_PROVIDER_CLASS_NAME \"com/sharry/lib/media/recorder/OpenSLESPCMProvider\"\n\n#define RECORD_BUFFER_SIZE 4096\n\n#endif //SMEDIA_RECORDER_CONSTDEFINE_H\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/JNICall.cpp",
    "content": "//\n// Created by Sharry Choo on 2019-06-22.\n//\n\n#include \"JNICall.h\"\n#include \"ConstDefine.h\"\n\n////////////////////////////////////////////////////////////////////////////\n// Construct and Destruct\n////////////////////////////////////////////////////////////////////////////\n\nJNICall::JNICall(JavaVM *java_vm, jobject jopensles_pcm_provider) {\n    this->java_vm = java_vm;\n    this->jopensles_pcm_provider = jopensles_pcm_provider;\n    JNIEnv *jniEnv = NULL;\n    EnvResult result = getJniEnv(&jniEnv);\n    if (jniEnv) {\n        this->jopensles_pcm_provider = jniEnv->NewGlobalRef(jopensles_pcm_provider);\n        jclass jPlayClass = jniEnv->GetObjectClass(jopensles_pcm_provider);\n        // 状态变更回调\n        jmid_on_pcm_changed = jniEnv->GetMethodID(jPlayClass, \"OnPCMChanged\", \"([B)V\");\n    }\n    if (result == THREAD_ATTACH_TO_JVM) {\n        detach();\n    }\n}\n\nJNICall::~JNICall() {\n    JNIEnv *jniEnv = NULL;\n    EnvResult result = getJniEnv(&jniEnv);\n    if (jniEnv != NULL) {\n        jniEnv->DeleteGlobalRef(jopensles_pcm_provider);\n    }\n    if (result == THREAD_ATTACH_TO_JVM) {\n        detach();\n    }\n}\n\n////////////////////////////////////////////////////////////////////////////\n// Open Method\n////////////////////////////////////////////////////////////////////////////\n\nvoid JNICall::callOnPCMChanged(uint8_t *pcm_data, int length) {\n    JNIEnv *jniEnv = NULL;\n    EnvResult result = getJniEnv(&jniEnv);\n    if (jniEnv) {\n        // 创建并填充 java 层的 y 数组\n        jbyteArray jpcm_bytes = jniEnv->NewByteArray(length);\n        jniEnv->SetByteArrayRegion(jpcm_bytes, 0, length,\n                                   reinterpret_cast<const jbyte *>(pcm_data));\n\n        // 回调 Java 方法\n        jniEnv->CallVoidMethod(jopensles_pcm_provider, jmid_on_pcm_changed, jpcm_bytes, length);\n        // 释放 native 层引用\n        jniEnv->DeleteLocalRef(jpcm_bytes);\n    }\n    if (result == THREAD_ATTACH_TO_JVM) {\n        detach();\n    }\n}\n\n////////////////////////////////////////////////////////////////////////////\n// Private Method\n////////////////////////////////////////////////////////////////////////////\n\nEnvResult JNICall::getJniEnv(JNIEnv **env) {\n    // 尝试直接获取当前线程的 JNIEnv 指针\n    if (JNI_OK != java_vm->GetEnv(reinterpret_cast<void **>(env), JNI_VERSION_1_6)) {\n        // 将当前线程 Attach 到 JVM, 获取 JNIEnv 指针\n        if (JNI_OK != java_vm->AttachCurrentThread(env, NULL)) {\n            return ENV_RESULT_ERROR;\n        }\n        return THREAD_ATTACH_TO_JVM;\n    }\n    return ENV_RESULT_OK;\n}\n\nvoid JNICall::detach() {\n    java_vm->DetachCurrentThread();\n}\n\n\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/JNICall.h",
    "content": "//\n// Created by Sharry Choo on 2019-06-22.\n//\n\n#ifndef SMEDIA_RECORDER_JNICALL_H\n#define SMEDIA_RECORDER_JNICALL_H\n\n#include <jni.h>\n\ntypedef int EnvResult;\n#define ENV_RESULT_OK 0\n#define ENV_RESULT_ERROR -106\n#define THREAD_ATTACH_TO_JVM 1\n\nclass JNICall {\n\npublic:\n    JavaVM *java_vm;\n    jobject jopensles_pcm_provider;\n    jmethodID jmid_on_pcm_changed;\n\n    JNICall(JavaVM *java_vm, jobject jopensles_pcm_provider);\n\n    ~JNICall();\n\n    /**\n     * 回调异步准备完毕\n     */\n    void callOnPCMChanged(uint8_t *pcm_data, int length);\n\nprivate:\n\n    /**\n     * 获取当前线程的 JNIEnv 对象\n     *\n     * @param env 传出参数, 内部会进行赋值操作\n     * @return if is AV_ATTACH_TO_VM, need invoke detach.\n     */\n    EnvResult getJniEnv(JNIEnv **env);\n\n    /**\n     * 解绑当前线程与 JVM 的关联\n     */\n    void detach();\n\n};\n\n\n#endif //SMEDIA_RECORDER_JNICALL_H\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/OpenSLRecorder.cpp",
    "content": "//\n// Created by Sharry on 2019-08-26.\n//\n\n#include <cassert>\n#include <pthread.h>\n#include \"OpenSLRecorder.h\"\n#include \"ConstDefine.h\"\n\nOpenSLRecorder::OpenSLRecorder(JNICall *jni_call) {\n    this->jni_call = jni_call;\n    this->buffer = new RecordBuffer(RECORD_BUFFER_SIZE);\n}\n\nOpenSLRecorder::~OpenSLRecorder() {\n    if (buffer != NULL) {\n        delete buffer;\n        buffer = NULL;\n    }\n    if (sl_obj_recorder != NULL) {\n        (*sl_obj_recorder)->Destroy(sl_obj_recorder);\n        sl_obj_recorder = NULL;\n        sl_itf_recorder = NULL;\n        sl_itf_record_buffer_queue = NULL;\n    }\n    if (sl_obj_engine != NULL) {\n        (*sl_obj_engine)->Destroy(sl_obj_engine);\n        sl_obj_engine = NULL;\n        sl_itf_engine = NULL;\n    }\n}\n\nvoid recordCallback(SLAndroidSimpleBufferQueueItf caller, void *context) {\n    OpenSLRecorder *impl = static_cast<OpenSLRecorder *>(context);\n    // 记录 OpenSL 录制线程的数据\n    impl->thread_opensl_es_recode = pthread_self();\n    // 通过 JNI 将数据回调到 Java 层\n    impl->jni_call->callOnPCMChanged(reinterpret_cast<uint8_t *>(impl->buffer->getNowBuffer()),\n                                     RECORD_BUFFER_SIZE);\n    // 将下一个 buffer 入队列\n    (*caller)->Enqueue(caller, impl->buffer->getRecordBuffer(), RECORD_BUFFER_SIZE);\n}\n\nvoid OpenSLRecorder::start() {\n    initOpenSLES();\n    (*sl_itf_recorder)->SetRecordState(sl_itf_recorder, SL_RECORDSTATE_RECORDING);\n}\n\nvoid OpenSLRecorder::pause() {\n    if (sl_itf_recorder != NULL) {\n        (*sl_itf_recorder)->SetRecordState(sl_itf_recorder, SL_RECORDSTATE_PAUSED);\n    }\n}\n\nvoid OpenSLRecorder::resume() {\n    if (sl_itf_recorder != NULL) {\n        (*sl_itf_recorder)->SetRecordState(sl_itf_recorder, SL_RECORDSTATE_RECORDING);\n    }\n}\n\nvoid OpenSLRecorder::stop() {\n    // 停止 OpenSL ES 的录制\n    (*sl_itf_recorder)->SetRecordState(sl_itf_recorder, SL_RECORDSTATE_STOPPED);\n    // 等待录制线程终止\n    if (pthread_kill(thread_opensl_es_recode, 0)) {\n        LOGI(\"thread_opensl_es_recode already killed\");\n    } else {\n        pthread_join(thread_opensl_es_recode, NULL);\n        LOGI(\"thread_opensl_es_recode exit.\");\n    }\n}\n\nvoid OpenSLRecorder::initOpenSLES() {\n    int result;\n    /// 创建 OpenSL 引擎\n    slCreateEngine(&sl_obj_engine, 0, NULL, 0, NULL, NULL);\n    result = (*sl_obj_engine)->Realize(sl_obj_engine, SL_BOOLEAN_FALSE);\n    assert(SL_RESULT_SUCCESS == result);\n    result = (*sl_obj_engine)->GetInterface(sl_obj_engine, SL_IID_ENGINE, &sl_itf_engine);\n    assert(SL_RESULT_SUCCESS == result);\n\n    // 创建 pAudioSrc\n    SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,\n                                      SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};\n    SLDataSource audio_src = {&loc_dev, NULL};\n    // 创建 pAudioSnk\n    SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};\n    SLDataFormat_PCM format_pcm = {\n            SL_DATAFORMAT_PCM,\n            2,                                                          // 通道数\n            SL_SAMPLINGRATE_44_1,                                       // 采样率\n            SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,\n            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,             // 通道布局\n            SL_BYTEORDER_LITTLEENDIAN                                   // 对其方式\n    };\n    SLDataSink audio_snk = {&loc_bq, &format_pcm};\n    // 创建 numInterfaces\n    SLInterfaceID itf_ids[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};\n    SLboolean itf_req[1] = {SL_BOOLEAN_TRUE};\n    /// 创建并实现 OpenSL 录制器\n    result = (*sl_itf_engine)->CreateAudioRecorder(sl_itf_engine, &sl_obj_recorder, &audio_src,\n                                                   &audio_snk, 1, itf_ids, itf_req);\n    assert(SL_RESULT_SUCCESS == result);\n    (*sl_obj_recorder)->Realize(sl_obj_recorder, SL_BOOLEAN_FALSE);\n\n    /// 获取相关接口\n    // 获取录制接口\n    (*sl_obj_recorder)->GetInterface(sl_obj_recorder, SL_IID_RECORD, &sl_itf_recorder);\n    // 获取缓冲队列接口\n    (*sl_obj_recorder)->GetInterface(sl_obj_recorder, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,\n                                     &sl_itf_record_buffer_queue);\n    /// 设置队列与回调\n    (*sl_itf_record_buffer_queue)->RegisterCallback(sl_itf_record_buffer_queue, recordCallback,\n                                                    this);\n    // 主动回调一次, 后面会自动开启录制\n    (*sl_itf_record_buffer_queue)->Enqueue(sl_itf_record_buffer_queue, buffer->getRecordBuffer(),\n                                           RECORD_BUFFER_SIZE);\n}"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/OpenSLRecorder.h",
    "content": "//\n// Created by Sharry on 2019-08-26.\n//\n\n#ifndef SMEDIA_OPENSLRECORDER_H\n#define SMEDIA_OPENSLRECORDER_H\n\n#include <SLES/OpenSLES.h>\n#include <SLES/OpenSLES_Android.h>\n#include <sys/types.h>\n#include \"RecordBuffer.h\"\n#include \"JNICall.h\"\n#include <pthread.h>\n\nclass OpenSLRecorder {\n\npublic:\n    JNICall *jni_call;\n    RecordBuffer *buffer;\n    pthread_t thread_opensl_es_recode;\n\n    /**\n     * OpenSL ES 相关变量\n     */\n    SLObjectItf sl_obj_engine = NULL;\n    SLEngineItf sl_itf_engine = NULL;\n\n    SLObjectItf sl_obj_recorder = NULL;\n    SLRecordItf sl_itf_recorder = NULL;\n    SLAndroidSimpleBufferQueueItf sl_itf_record_buffer_queue = NULL;\n\n    OpenSLRecorder(JNICall *jni_call);\n\n    ~OpenSLRecorder();\n\n    void start();\n\n    void pause();\n\n    void resume();\n\n    void stop();\n\n    void initOpenSLES();\n};\n\n\n#endif //SMEDIA_OPENSLRECORDER_H\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/RecordBuffer.cpp",
    "content": "//\n// Created by Sharry Choo on 2019-08-26.\n//\n\n#include \"RecordBuffer.h\"\n\nRecordBuffer::RecordBuffer(int buffer_size) {\n    buffer = new short *[2];\n    for (int i = 0; i < 2; ++i) {\n        buffer[i] = new short[buffer_size];\n    }\n}\n\nRecordBuffer::~RecordBuffer() {\n    for (int i = 0; i < 2; ++i) {\n        delete buffer[i];\n    }\n    delete buffer;\n}\n\nshort *RecordBuffer::getRecordBuffer() {\n    index++;\n    if (index > 1) {\n        index = 0;\n    }\n    return buffer[index];\n}\n\nshort *RecordBuffer::getNowBuffer() {\n    return buffer[index];\n}\n\n\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/RecordBuffer.h",
    "content": "//\n// Created by Sharry Choo on 2019-08-26.\n//\n\n#ifndef SMEDIA_RECORDBUFFER_H\n#define SMEDIA_RECORDBUFFER_H\n\n\n#include <stdint.h>\n\nclass RecordBuffer {\n\npublic:\n    short **buffer;\n    int index = -1;\npublic:\n    RecordBuffer(int buffer_size);\n\n    ~RecordBuffer();\n\npublic:\n    short *getRecordBuffer();\n\n    short *getNowBuffer();\n\n};\n\n\n#endif //SMEDIA_RECORDBUFFER_H\n"
  },
  {
    "path": "lib-media-recorder/src/main/cpp/native-bridge-recorder.cpp",
    "content": "//\n// Created by Sharry on 2019-08-26.\n//\n\n\n#include <jni.h>\n#include \"ConstDefine.h\"\n#include \"OpenSLRecorder.h\"\n#include \"JNICall.h\"\n\nJavaVM *gJavaVM = NULL;\n\nint registerNativeMethods(JNIEnv *env, jclass cls);\n\nextern \"C\"\nJNIEXPORT jint JNICALL\nJNI_OnLoad(JavaVM *javaVM, void *reserverd) {\n    // 通过初始化方法获取 JavaVM\n    gJavaVM = javaVM;\n    JNIEnv *env;\n    if (javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {\n        return -1;\n    }\n    // 注册 IMediaPlayer 函数\n    jclass jclsMediaPlayer = env->FindClass(OPENSLES_PCM_PROVIDER_CLASS_NAME);\n    jclsMediaPlayer = reinterpret_cast<jclass>(env->NewGlobalRef(jclsMediaPlayer));\n    if (!jclsMediaPlayer) {\n        LOGE(\"Fail to create global reference for %s\", OPENSLES_PCM_PROVIDER_CLASS_NAME);\n    }\n    int res = registerNativeMethods(env, jclsMediaPlayer);\n    if (res != 0) {\n        LOGE(\"Failed to register native methods for class %s \", OPENSLES_PCM_PROVIDER_CLASS_NAME);\n    }\n    env->DeleteGlobalRef(jclsMediaPlayer);\n    return JNI_VERSION_1_6;\n}\n\nnamespace openslesprovider {\n\n    OpenSLRecorder *pRecorder = NULL;\n    JNICall *pJniCall = NULL;\n\n    void nativeStart(JNIEnv *, jobject jobj) {\n        if (pRecorder == NULL) {\n            pJniCall = new JNICall(gJavaVM, jobj);\n            pRecorder = new OpenSLRecorder(pJniCall);\n            pRecorder->start();\n        } else {\n            LOGI(\"Please stop first\");\n        }\n    }\n\n    void nativePause(JNIEnv *, jobject) {\n        if (pRecorder != NULL) {\n            pRecorder->pause();\n        }\n    }\n\n    void nativeResume(JNIEnv *, jobject) {\n        if (pRecorder != NULL) {\n            pRecorder->resume();\n        }\n    }\n\n    void nativeStop(JNIEnv *, jobject) {\n        if (pRecorder != NULL) {\n            pRecorder->stop();\n        }\n        if (pJniCall != NULL) {\n            delete pJniCall;\n            pJniCall = NULL;\n        }\n        if (pRecorder != NULL) {\n            delete pRecorder;\n            pRecorder = NULL;\n        }\n        LOGI(\"OpenSL ES recorder stopped.\");\n    }\n\n}\n\nJNINativeMethod gBridgeMethods[] = {\n        {\"nativeStart\",  \"()V\", (void *) openslesprovider::nativeStart},\n        {\"nativePause\",  \"()V\", (void *) openslesprovider::nativePause},\n        {\"nativeResume\", \"()V\", (void *) openslesprovider::nativeResume},\n        {\"nativeStop\",   \"()V\", (void *) openslesprovider::nativeStop}\n};\n\nint registerNativeMethods(JNIEnv *env, jclass cls) {\n    return env->RegisterNatives(cls, gBridgeMethods,\n                                sizeof(gBridgeMethods) / sizeof(gBridgeMethods[0]));\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/AACEncoder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaCodec;\nimport android.media.MediaCodecInfo;\nimport android.media.MediaFormat;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * ACC 音频的编码器\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/11/22 10:22\n */\npublic class AACEncoder implements IAudioEncoder {\n\n    private static final String TAG = AACEncoder.class.getSimpleName();\n    /**\n     * 音频常用的采样数组\n     */\n    private static int[] SAMPLE_RATES = new int[]{\n            96000, 88200, 64000, 48000, 44100, 32000,\n            24000, 22050, 16000, 12000, 11025, 8000, 7350\n    };\n\n    private static final int ACC_HEADER_ADTS_LENGTH = 7;\n\n    private static final String MIME_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;  // 当前编码器要编码的类型描述\n    private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();\n    private FileOutputStream mFileOutputSteam;\n    private Context mContext;\n    private MediaCodec mImpl;\n    private long mPts = 0;\n\n    @Override\n    public void prepare(@NonNull Context context) throws IOException {\n        mContext = context;\n        // 执行编码前的准备\n        MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, mContext.sampleRate, mContext.channelCount);\n        int bitRate = mContext.sampleRate * mContext.channelCount * context.perSampleSize;\n        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);\n        audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, mContext.channelCount);\n        audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, mContext.sampleRate);\n        audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);\n        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bitRate);\n        // 根据参数判断是否需要写入到文件\n        if (!mContext.isJustEncode) {\n            mFileOutputSteam = new FileOutputStream(context.outputFd);\n        }\n        // 初始化编码器\n        mImpl = MediaCodec.createEncoderByType(MIME_TYPE);\n        mImpl.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);\n        mImpl.start();\n    }\n\n    @Override\n    public void encode(@Nullable byte[] pcmBytes) {\n        if (pcmBytes == null) {\n            return;\n        }\n        // 1. 将输入流传递给编码器的 inputBuffer 队列, 等待编码\n        final int indexOfInputBuffer = mImpl.dequeueInputBuffer(0);\n        if (indexOfInputBuffer >= 0) {\n            final ByteBuffer inputBuffer;\n            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {\n                inputBuffer = mImpl.getInputBuffer(indexOfInputBuffer);\n            } else {\n                inputBuffer = mImpl.getInputBuffers()[indexOfInputBuffer];\n            }\n            if (null == inputBuffer) {\n                return;\n            }\n            inputBuffer.clear();\n            inputBuffer.put(pcmBytes);\n            // 计算录制时间戳\n            calcPresentationTimeUs(pcmBytes.length, mContext.sampleRate, mContext.channelCount,\n                    mContext.perSampleSize);\n            mImpl.queueInputBuffer(indexOfInputBuffer, 0, pcmBytes.length, mPts, 0);\n        } else {\n            // ignore.\n            return;\n        }\n        // 2. 从 MediaCodec 中获取编码后的数据\n        boolean isAvailable = true;\n        while (isAvailable) {\n            int indexOfOutputBuffer = mImpl.dequeueOutputBuffer(mBufferInfo, 0);\n            switch (indexOfOutputBuffer) {\n                case MediaCodec.INFO_TRY_AGAIN_LATER:\n                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:\n                    isAvailable = false;\n                    break;\n                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:\n                    mContext.callback.onAudioFormatChanged(mImpl.getOutputFormat());\n                    break;\n                default:\n                    if (indexOfOutputBuffer < 0) {\n                        isAvailable = false;\n                        break;\n                    }\n                    // 获取数据\n                    final ByteBuffer outBuffer;\n                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {\n                        outBuffer = mImpl.getOutputBuffer(indexOfOutputBuffer);\n                    } else {\n                        outBuffer = mImpl.getOutputBuffers()[indexOfOutputBuffer];\n                    }\n                    if (null == outBuffer) {\n                        isAvailable = false;\n                        break;\n                    }\n                    outBuffer.position(mBufferInfo.offset);\n                    outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);\n                    // 回调音频编码数据\n                    mContext.callback.onAudioEncoded(outBuffer, mBufferInfo);\n                    // 写到文件\n                    writeToFile(outBuffer, mBufferInfo);\n                    // 释放 encoderStatus 索引处的输出缓冲流\n                    mImpl.releaseOutputBuffer(indexOfOutputBuffer, false);\n                    break;\n            }\n        }\n    }\n\n    @Override\n    public void stop() {\n        if (mImpl != null) {\n            try {\n                mImpl.flush();\n            } catch (Throwable e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n            try {\n                mImpl.stop();\n            } catch (Throwable e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n            try {\n                mImpl.release();\n            } catch (Throwable e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n            mImpl = null;\n        }\n    }\n\n    /**\n     * 计算录制时间戳\n     *\n     * @param size          采样数据的大小\n     * @param sampleRate    采样率\n     * @param channel       通道数\n     * @param preSampleSize 采样点大小\n     */\n    private void calcPresentationTimeUs(int size, int sampleRate, int channel, int preSampleSize) {\n        mPts += (long) (1.0 * size / (sampleRate * channel * preSampleSize) * 1000000.0);\n    }\n\n    /**\n     * 将编码后的 ACC 添加头部并且写入文件\n     */\n    private void writeToFile(ByteBuffer outBuffer, MediaCodec.BufferInfo bufferInfo) {\n        if (mFileOutputSteam == null) {\n            return;\n        }\n        // 多开辟 7 个 byte 给流数据添加 ADTS 头部字段\n        int len = bufferInfo.size + ACC_HEADER_ADTS_LENGTH;\n        byte[] accBytes = new byte[len];\n        // 为数组填充 ADTS 头部信息\n        addADTStoPacket(accBytes, len, mContext.sampleRate, mContext.channelCount);\n        // 将 ACC 数据写入到头部信息之后\n        outBuffer.get(accBytes, ACC_HEADER_ADTS_LENGTH, bufferInfo.size);\n        // 写入文件\n        try {\n            mFileOutputSteam.write(accBytes);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 给 ACC 码流添加 ADTS 头字段\n     *\n     * @param packet    空出 7 个字节, 填充 ADTS 字段\n     * @param packetLen 真实数据的长度\n     */\n    private void addADTStoPacket(byte[] packet, int packetLen, int sampleRate, int channelCount) {\n        int profile = 2; // AAC LC\n        int freqIdx = sampleRateMapperFrequency(sampleRate);\n        // fill in ADTS data\n        packet[0] = (byte) 0xFF;\n        packet[1] = (byte) 0xF9;\n        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (channelCount >> 2));\n        packet[3] = (byte) (((channelCount & 3) << 6) + (packetLen >> 11));\n        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);\n        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);\n        packet[6] = (byte) 0xFC;\n    }\n\n    /**\n     * 获取采样率对应的频率 ID.\n     * <p>\n     * 0: 96000 Hz\n     * 1: 88200 Hz\n     * 2: 64000 Hz\n     * 3: 48000 Hz\n     * 4: 44100 Hz\n     * 5: 32000 Hz\n     * 6: 24000 Hz\n     * 7: 22050 Hz\n     * 8: 16000 Hz\n     * 9: 12000 Hz\n     * 10: 11025 Hz\n     * 11: 8000 Hz\n     * 12: 7350 Hz\n     * 13: Reserved\n     * 14: Reserved\n     * 15: frequency is written explictly\n     * </p>\n     *\n     * @return the sample rate mapped special frequency id.\n     */\n    private int sampleRateMapperFrequency(int sampleRate) {\n        int frequencyId = -1;\n        for (int index = 0; index < SAMPLE_RATES.length; index++) {\n            if (sampleRate == SAMPLE_RATES[index]) {\n                frequencyId = index;\n                break;\n            }\n        }\n        return frequencyId;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/EncodeType.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaFormat;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-20 11:16\n */\npublic class EncodeType {\n\n    public enum Video {\n\n        H264(\"H.264\");\n\n        private String desc;\n\n        Video(String desc) {\n            this.desc = desc;\n        }\n\n        public String getDesc() {\n            return desc;\n        }\n\n    }\n\n    public enum Audio {\n\n        AAC(MediaFormat.MIMETYPE_AUDIO_AAC, \".aac\");\n\n        private String mime;\n        private String suffix;\n\n        /**\n         * 定义录音类型\n         *\n         * @param mime   编码格式\n         * @param suffix 录音文件扩展名\n         */\n        Audio(String mime, String suffix) {\n            this.mime = mime;\n            this.suffix = suffix;\n        }\n\n        public String getMIME() {\n            return mime;\n        }\n\n        public String getFileSuffix() {\n            return suffix;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/EncoderFactory.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport androidx.annotation.NonNull;\n\n/**\n * 编码器 的 简单工厂\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/2/2019 4:24 PM\n */\nclass EncoderFactory {\n\n    /**\n     * 创建视频编码器\n     */\n    @NonNull\n    static IVideoEncoder create(EncodeType.Video videoEncodeType) {\n        IVideoEncoder result;\n        switch (videoEncodeType) {\n            case H264:\n                result = new H264Encoder();\n                break;\n            default:\n                throw new UnsupportedOperationException();\n        }\n        return result;\n    }\n\n    /**\n     * 创建音频编码器\n     */\n    @NonNull\n    static IAudioEncoder create(EncodeType.Audio type) {\n        IAudioEncoder result;\n        switch (type) {\n            case AAC:\n                result = new AACEncoder();\n                break;\n            default:\n                throw new UnsupportedOperationException();\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/H264Encoder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaCodec;\nimport android.media.MediaCodecInfo;\nimport android.media.MediaFormat;\nimport android.util.Log;\nimport android.view.Surface;\n\nimport androidx.annotation.NonNull;\n\nimport com.sharry.lib.opengles.util.EglCore;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * H.264 编码类\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/28/2019 3:05 PM\n */\npublic class H264Encoder implements IVideoEncoder {\n\n    private static final String TAG = H264Encoder.class.getSimpleName();\n\n    /**\n     * H.264 encode type.\n     */\n    private static final String MIME_TYPE = \"video/avc\";\n\n    private final Object mPauseLock = new Object();\n    private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();\n\n    /**\n     * 以下的属性在 prepare 中初始化\n     */\n    private MediaCodec mImpl;\n    private Context mContext;\n    private Surface mInputSurface;\n    private RendererThread mRenderThread;\n    private EncodeThread mEncodeThread;\n\n    private volatile boolean mIsEncoding;\n    private volatile boolean mIsPausing;\n\n    @Override\n    public void prepare(@NonNull Context context) throws IOException {\n        mContext = context;\n        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mContext.frameWidth, mContext.frameHeight);\n        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);\n        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);\n        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mContext.frameRate);\n        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mContext.frameWidth * mContext.frameHeight * 4);\n        // 创建编码器\n        mImpl = MediaCodec.createEncoderByType(MIME_TYPE);\n        mImpl.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);\n        // 将 Camera 中的数据拷贝到这个 Surface 上, 当做输入流\n        mInputSurface = mImpl.createInputSurface();\n        // 创建线程\n        mRenderThread = new RendererThread();\n        mEncodeThread = new EncodeThread();\n    }\n\n    @Override\n    public void start() {\n        mIsEncoding = true;\n        mRenderThread.start();\n        mEncodeThread.start();\n    }\n\n    @Override\n    public void pause() {\n        mIsPausing = true;\n    }\n\n    @Override\n    public void resume() {\n        mIsPausing = false;\n        synchronized (mPauseLock) {\n            mPauseLock.notify();\n        }\n    }\n\n    @Override\n    public void stop() {\n        mIsPausing = false;\n        synchronized (mPauseLock) {\n            mPauseLock.notify();\n        }\n        // 使用这个方法, 通知 MediaCodec 渲染结束\n        mImpl.signalEndOfInputStream();\n        try {\n            mRenderThread.join();\n        } catch (Throwable e) {\n            Log.w(TAG, e.getMessage(), e);\n        } finally {\n            mRenderThread = null;\n        }\n        try {\n            mEncodeThread.join();\n        } catch (Throwable e) {\n            Log.w(TAG, e.getMessage(), e);\n        } finally {\n            mEncodeThread = null;\n        }\n    }\n\n    /**\n     * 录制的渲染线程\n     */\n    private final class RendererThread extends Thread {\n\n        private final long mFrameIntervalMills;\n        private final EglCore mEglCore;\n        private final H264Render mRenderer;\n        private boolean mIsContextCreated = true;\n        private boolean mIsSizeChanged = true;\n        private long nextFramePts;\n\n        RendererThread() {\n            mEglCore = new EglCore();\n            mRenderer = new H264Render(mContext.textureId);\n            mFrameIntervalMills = 800L / mContext.frameRate;\n        }\n\n        @Override\n        public void run() {\n            while (mIsEncoding) {\n                if (mIsPausing) {\n                    synchronized (mPauseLock) {\n                        try {\n                            mPauseLock.wait();\n                        } catch (InterruptedException e) {\n                            Log.w(TAG, e.getMessage());\n                        }\n                    }\n                    continue;\n                }\n                if (mIsContextCreated) {\n                    // 初始化创建 EGL 环境，然后回调 Renderer\n                    mEglCore.initialize(mInputSurface, mContext.eglContext);\n                    mRenderer.onAttach();\n                    mIsContextCreated = false;\n                }\n                // 说明手机横竖屏切换, 导致尺寸变更了\n                if (mIsSizeChanged) {\n                    mRenderer.onSizeChanged(mContext.frameWidth, mContext.frameHeight);\n                    mIsSizeChanged = true;\n                }\n                // 不停的绘制\n                mRenderer.onDraw();\n                mEglCore.setPresentationTime(nextFramePts);\n                mEglCore.swapBuffers();\n                // 更新下一帧渲染时间\n                nextFramePts += mFrameIntervalMills * 1000 * 1000;\n                // 睡眠一下, 等待下次绘制\n                try {\n                    sleep(mFrameIntervalMills);\n                } catch (InterruptedException e) {\n                    // ignore.\n                }\n            }\n            onDestroy();\n        }\n\n        private void onDestroy() {\n            mEglCore.release();\n        }\n    }\n\n    /**\n     * 录制编码的线程\n     */\n    public final class EncodeThread extends Thread {\n\n        @Override\n        public void run() {\n            mImpl.start();\n            while (mIsEncoding) {\n                if (mIsPausing) {\n                    synchronized (mPauseLock) {\n                        try {\n                            mPauseLock.wait();\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    continue;\n                }\n                int indexOfOutputBuffer = mImpl.dequeueOutputBuffer(mBufferInfo, 0);\n                switch (indexOfOutputBuffer) {\n                    case MediaCodec.INFO_TRY_AGAIN_LATER:\n                    case  MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:\n                        break;\n                    case   MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:\n                        mContext.callback.onVideoFormatChanged(mImpl.getOutputFormat());\n                        break;\n                    default:\n                        if (indexOfOutputBuffer < 0) {\n                            continue;\n                        }\n                        // 存在 BUFFER_FLAG_END_OF_STREAM flag 则说明渲染结束了\n                        if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {\n                            mIsEncoding = false;\n                            continue;\n                        }\n                        // 处理编码后的输出数据\n                        ByteBuffer outputBuffer;\n                        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {\n                            outputBuffer = mImpl.getOutputBuffer(indexOfOutputBuffer);\n                        } else {\n                            outputBuffer = mImpl.getOutputBuffers()[indexOfOutputBuffer];\n                        }\n                        if (null == outputBuffer) {\n                            continue;\n                        }\n                        outputBuffer.position(mBufferInfo.offset);\n                        outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);\n                        // 回调 onAudioEncoded\n                        mContext.callback.onVideoEncoded(outputBuffer, mBufferInfo);\n                        // 3.2 释放指定位置的输出缓冲流\n                        mImpl.releaseOutputBuffer(indexOfOutputBuffer, false);\n                        break;\n                }\n            }\n            try {\n                mImpl.flush();\n            } catch (Exception e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n            try {\n                mImpl.stop();\n            } catch (Throwable e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n            try {\n                mImpl.release();\n            } catch (Throwable e) {\n                Log.w(TAG, e.getMessage(), e);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/H264Render.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.opengl.GLES20;\n\nimport com.sharry.lib.opengles.texture.ITextureRenderer;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\n\npublic class H264Render implements ITextureRenderer {\n\n    /**\n     * 顶点坐标\n     */\n    private float[] mVertexCoordinate = new float[]{\n            -1f, 1f,\n            -1f, -1f,\n            1f, 1f,\n            1f, -1f\n    };\n\n    /**\n     * 纹理坐标\n     */\n    private float[] mFragmentCoordinate = new float[]{\n            0f, 1f,\n            0f, 0f,\n            1f, 1f,\n            1f, 0f\n    };\n\n    private static final String vertexSource = \"attribute vec4 v_Position;\\n\" +\n            \"attribute vec2 f_Position;\\n\" +\n            \"varying vec2 ft_Position;\\n\" +\n            \"void main() {\\n\" +\n            \"    ft_Position = f_Position;\\n\" +\n            \"    gl_Position = v_Position;\\n\" +\n            \"}\\n\";\n\n    private static final String fragmentSource = \"precision mediump float;\\n\" +\n            \"varying vec2 ft_Position;\\n\" +\n            \"uniform sampler2D sTexture;\\n\" +\n            \"void main() {\\n\" +\n            \"    gl_FragColor=texture2D(sTexture, ft_Position);\\n\" +\n            \"}\\n\";\n\n    private FloatBuffer mVertexBuffer;\n    private FloatBuffer mFragmentBuffer;\n    private int mVboId;\n    private int mProgram;\n    private int vPosition;\n    private int fPosition;\n    private int mTextureId;\n\n    H264Render(int textureId) {\n        mTextureId = textureId;\n        mVertexBuffer = createBuffer(mVertexCoordinate);\n        mFragmentBuffer = createBuffer(mFragmentCoordinate);\n    }\n\n    @Override\n    public void onAttach() {\n        mProgram = createProgram(vertexSource, fragmentSource);\n        // 获取坐标\n        vPosition = GLES20.glGetAttribLocation(mProgram, \"v_Position\");\n        fPosition = GLES20.glGetAttribLocation(mProgram, \"f_Position\");\n        // 创建 vbos\n        int[] vBos = new int[1];\n        GLES20.glGenBuffers(1, vBos, 0);\n        // 绑定 vbos\n        mVboId = vBos[0];\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 开辟 vbos\n        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, (mVertexCoordinate.length + mFragmentCoordinate.length) * 4,\n                null, GLES20.GL_STATIC_DRAW);\n        // 赋值 vbos\n        GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, mVertexCoordinate.length * 4, mVertexBuffer);\n        GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mVertexCoordinate.length * 4,\n                mFragmentCoordinate.length * 4, mFragmentBuffer);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n    }\n\n    @Override\n    public void onSizeChanged(int width, int height) {\n        GLES20.glViewport(0, 0, width, height);\n    }\n\n    @Override\n    public void onDraw() {\n        // 激活 program\n        GLES20.glUseProgram(mProgram);\n        // 绑定纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 给顶点坐标赋值\n        GLES20.glEnableVertexAttribArray(vPosition);\n        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT,\n                false, 8, 0);\n        // 给纹理坐标赋值\n        GLES20.glEnableVertexAttribArray(fPosition);\n        GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false,\n                8, mVertexCoordinate.length * 4);\n        // 绘制到屏幕\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n        // 解绑\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    @Override\n    public void onDetach() {\n        // 释放着色器程序\n        if (mProgram != 0) {\n            GLES20.glDeleteProgram(mProgram);\n        }\n        // 释放 VBO\n        if (mVboId != 0) {\n            int size = 1;\n            int[] vboIds = new int[size];\n            vboIds[0] = mVboId;\n            GLES20.glDeleteBuffers(1, vboIds, 0);\n        }\n    }\n\n    private FloatBuffer createBuffer(float[] vertexData) {\n        FloatBuffer buffer = ByteBuffer.allocateDirect(vertexData.length * 4)\n                .order(ByteOrder.nativeOrder())\n                .asFloatBuffer();\n        buffer.put(vertexData, 0, vertexData.length)\n                .position(0);\n        return buffer;\n    }\n\n    private int createProgram(String vertexSource, String fragmentSource) {\n        // 分别加载创建着色器\n        int vertexShaderId = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource);\n        int fragmentShaderId = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);\n        if (vertexShaderId != 0 && fragmentShaderId != 0) {\n            // 创建 OpenGL 程序 ID\n            int programId = GLES20.glCreateProgram();\n            if (programId == 0) {\n                return 0;\n            }\n            // 链接上 顶点着色器\n            GLES20.glAttachShader(programId, vertexShaderId);\n            // 链接上 片段着色器\n            GLES20.glAttachShader(programId, fragmentShaderId);\n            // 链接 OpenGL 程序\n            GLES20.glLinkProgram(programId);\n            // 验证链接结果是否失败\n            int[] status = new int[1];\n            GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 失败后删除这个 OpenGL 程序\n                GLES20.glDeleteProgram(programId);\n                return 0;\n            }\n            return programId;\n        }\n        return 0;\n    }\n\n    private int compileShader(int shaderType, String source) {\n        // 创建着色器 ID\n        int shaderId = GLES20.glCreateShader(shaderType);\n        if (shaderId != 0) {\n            // 1. 将着色器 ID 和着色器程序内容关联\n            GLES20.glShaderSource(shaderId, source);\n            // 2. 编译着色器\n            GLES20.glCompileShader(shaderId);\n            // 3. 验证编译结果\n            int[] status = new int[1];\n            GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 编译失败删除这个着色器 id\n                GLES20.glDeleteShader(shaderId);\n                return 0;\n            }\n        }\n        return shaderId;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/IAudioEncoder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.FileDescriptor;\nimport java.nio.ByteBuffer;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/18/2019 5:40 PM\n */\npublic interface IAudioEncoder {\n\n    /**\n     * 编码前的准备工作\n     * <p>\n     * Subsequent calls to {@link #encode} only the encoder prepare invoked.\n     */\n    void prepare(@NonNull Context context) throws Throwable;\n\n    /**\n     * 执行编码\n     */\n    void encode(@Nullable byte[] inputBytes) throws Throwable;\n\n    /**\n     * 停止编码\n     */\n    void stop();\n\n    /**\n     * 编码的回调\n     */\n    interface Callback {\n\n        /**\n         * 输出格式时回调\n         */\n        void onAudioFormatChanged(MediaFormat outputFormat);\n\n        /**\n         * 在编码过程中回调\n         *\n         * @param byteBuffer 编码后的数据帧\n         * @param bufferInfo 数据帧的信息\n         */\n        void onAudioEncoded(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo);\n    }\n\n    class Context {\n        final int sampleRate;                           // 采样率\n        final int channelCount;                         // 通道数\n        final int perSampleSize;                        // 每个采样点的大小\n        final boolean isJustEncode;                     // 只进行编码不写入文件\n        final FileDescriptor outputFd;                        // 音频输出的文件\n        final IAudioEncoder.Callback callback;          // 视频录制的回调\n\n        public Context(int sampleRate, int channelCount, int perSampleSize, boolean isJustEncode,\n                       FileDescriptor outputFd, Callback callback) {\n            this.sampleRate = sampleRate;\n            this.channelCount = channelCount;\n            this.perSampleSize = perSampleSize;\n            this.isJustEncode = isJustEncode;\n            this.outputFd = outputFd;\n            this.callback = callback;\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/encoder/com/sharry/lib/media/recorder/IVideoEncoder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport android.opengl.EGLContext;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/18/2019 5:40 PM\n */\npublic interface IVideoEncoder {\n\n    /**\n     * 编码前的准备工作\n     * <p>\n     * Subsequent calls to {@link #start} only the encoder prepare invoked.\n     */\n    void prepare(@NonNull Context context) throws IOException;\n\n    void start();\n\n    void pause();\n\n    void resume();\n\n    void stop();\n\n    interface Callback {\n\n        /**\n         * 输出格式时回调\n         */\n        void onVideoFormatChanged(MediaFormat outputFormat);\n\n        /**\n         * 在编码过程中回调\n         *\n         * @param byteBuffer 编码后的数据帧\n         * @param bufferInfo 数据帧的信息\n         */\n        void onVideoEncoded(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo);\n    }\n\n    class Context {\n        final int frameWidth;                           // 视频帧的宽度\n        final int frameHeight;                          // 视频帧的高度\n        final int frameRate;                            // 录制的帧率\n        final int textureId;                            // Camera 的纹理 ID\n        final EGLContext eglContext;                    // Camera 的 GL 上下文\n        final IVideoEncoder.Callback callback;          // 视频录制的回调\n\n        public Context(int frameWidth, int frameHeight, int frameRate, int textureId,\n                       EGLContext eglContext, Callback callback) {\n            this.frameWidth = frameWidth;\n            this.frameHeight = frameHeight;\n            this.frameRate = frameRate;\n            this.textureId = textureId;\n            this.eglContext = eglContext;\n            this.callback = callback;\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/muxer/com/sharry/lib/media/recorder/IMuxer.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport android.net.Uri;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.nio.ByteBuffer;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/18/2019 5:40 PM\n */\npublic interface IMuxer {\n\n    IMuxer MPEG_4 = new MPEG4Muxer();\n\n\n    /**\n     * 编码前的准备工作\n     * <p>\n     * Subsequent calls to {@link #execute} only the encoder prepare invoked.\n     */\n    @MainThread\n    @TargetApi(29)\n    void prepare(Context context, Uri uri) throws Throwable;\n\n    /**\n     * 编码前的准备工作\n     * <p>\n     * Subsequent calls to {@link #execute} only the encoder prepare invoked.\n     */\n    @MainThread\n    void prepare(Context context, File file) throws Throwable;\n\n    /**\n     * 添加视频轨\n     */\n    @MainThread\n    void addVideoTrack(@NonNull MediaFormat videoFormat);\n\n    /**\n     * 添加音轨\n     */\n    @MainThread\n    void addAudioTrack(@NonNull MediaFormat audioFormat);\n\n    /**\n     * 执行编码\n     *\n     * @param data 原生音频的数据源\n     */\n    @MainThread\n    void execute(@NonNull Parcel data) throws Throwable;\n\n    /**\n     * 释放资源\n     */\n    void stop();\n\n\n    /**\n     * 混音器的元数据\n     */\n    class Parcel {\n\n        static final int TRACK_VIDEO = 316;\n        static final int TRACK_AUDIO = 748;\n\n        @IntDef(flag = true, value = {\n                TRACK_VIDEO,\n                TRACK_AUDIO,\n        })\n        @interface TrackType {\n        }\n\n        static Parcel newInstance(@TrackType int trackType, ByteBuffer byteBuf,\n                                  MediaCodec.BufferInfo bufferInfo) {\n            return new Parcel(trackType, byteBuf, bufferInfo);\n        }\n\n        int trackType;\n        ByteBuffer byteBuff;\n        MediaCodec.BufferInfo bufferInfo;\n\n        private Parcel(@TrackType int trackType, ByteBuffer byteBuff, MediaCodec.BufferInfo bufferInfo) {\n            this.trackType = trackType;\n            this.byteBuff = byteBuff;\n            this.bufferInfo = bufferInfo;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/muxer/com/sharry/lib/media/recorder/MPEG4Muxer.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.media.MediaFormat;\nimport android.media.MediaMuxer;\nimport android.net.Uri;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\n\n/**\n * Mp4 音视频封装器\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/24/2019 3:34 PM\n */\nclass MPEG4Muxer implements IMuxer {\n\n    private static final String TAG = MPEG4Muxer.class.getSimpleName();\n\n    private static final int NO_INDEX = -1;\n\n    private MediaMuxer mImpl;\n\n    /**\n     * SVideoPlayer track index associated with this mixer\n     * <p>\n     * init when {@link #addVideoTrack(MediaFormat)} invoked.\n     */\n    private int mVideoTrackIndex = NO_INDEX;\n\n    /**\n     * SAudioPlayer track index associated with this mixer\n     * <p>\n     * init when {@link #addAudioTrack(MediaFormat)} invoked.\n     */\n    private int mAudioTrackIndex = NO_INDEX;\n\n    /**\n     * Flags associated with mixer\n     */\n    private volatile boolean isMixerStart;\n\n    @Override\n    @TargetApi(29)\n    public void prepare(Context context, Uri uri) throws Throwable {\n        mImpl = new MediaMuxer(\n                context.getContentResolver().openFileDescriptor(uri, \"w\").getFileDescriptor(),\n                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4\n        );\n    }\n\n    @Override\n    public void prepare(Context context, File file) throws Throwable {\n        mImpl = new MediaMuxer(\n                file.getAbsolutePath(),\n                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4\n        );\n    }\n\n    @Override\n    public void addAudioTrack(@NonNull MediaFormat audioFormat) {\n        if (isMixerStart) {\n            Log.i(TAG, \"All track already added.\");\n            return;\n        }\n        if (mAudioTrackIndex != NO_INDEX) {\n            Log.i(TAG, \"SAudioPlayer track already added.\");\n        } else {\n            mAudioTrackIndex = mImpl.addTrack(audioFormat);\n            Log.i(TAG, \"SAudioPlayer track add successful.\");\n        }\n        // 尝试启动混合器\n        tryToLaunchMuxer();\n    }\n\n    @Override\n    public void addVideoTrack(@NonNull MediaFormat videoFormat) {\n        if (isMixerStart) {\n            Log.i(TAG, \"All track already added.\");\n            return;\n        }\n        if (mVideoTrackIndex != NO_INDEX) {\n            Log.i(TAG, \"SVideoPlayer track already added.\");\n        } else {\n            mVideoTrackIndex = mImpl.addTrack(videoFormat);\n            Log.i(TAG, \"SVideoPlayer track add successful.\");\n        }\n        // 尝试启动混合器\n        tryToLaunchMuxer();\n    }\n\n    @Override\n    public void execute(@NonNull Parcel data) throws Throwable {\n        if (!isMixerStart) {\n            return;\n        }\n        int trackIndex;\n        if (data.trackType == Parcel.TRACK_VIDEO) {\n            trackIndex = mVideoTrackIndex;\n            if (BuildConfig.DEBUG) {\n                Log.v(TAG, \"Writing video, byte size is: \" + data.bufferInfo.size\n                        + \", pts is \" + data.bufferInfo.presentationTimeUs / 1000 / 1000 + \"s\");\n            }\n        } else {\n            trackIndex = mAudioTrackIndex;\n            if (BuildConfig.DEBUG) {\n                Log.v(TAG, \"Writing audio, byte size is, \" + data.bufferInfo.size\n                        + \", pts is \" + data.bufferInfo.presentationTimeUs / 1000 / 1000 + \"s\");\n            }\n        }\n        // 写入数据, 可能会产生异常\n        mImpl.writeSampleData(trackIndex, data.byteBuff, data.bufferInfo);\n    }\n\n    @Override\n    public void stop() {\n        try {\n            // 释放资源\n            mImpl.stop();\n        } catch (Throwable e) {\n            Log.w(TAG, e.getMessage(), e);\n        }\n        try {\n            mImpl.release();\n        } catch (Throwable e) {\n            Log.w(TAG, e.getMessage(), e);\n        }\n        mAudioTrackIndex = -1;\n        mVideoTrackIndex = -1;\n        isMixerStart = false;\n        mImpl = null;\n    }\n\n    /**\n     * 请求混合器开始启动\n     */\n    private void tryToLaunchMuxer() {\n        if (isMixerStart) {\n            Log.i(TAG, \"Mixer already launched.\");\n            return;\n        }\n        if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) {\n            mImpl.start();\n            isMixerStart = true;\n            Log.i(TAG, \"Mixer launch successful.\");\n        } else {\n            Log.i(TAG, \"Mixer waiting all track added.\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/muxer/com/sharry/lib/media/recorder/MuxerFactory.java",
    "content": "package com.sharry.lib.media.recorder;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/2/2019 4:24 PM\n */\nclass MuxerFactory {\n\n    /**\n     * 根据封装类型, 创建混合器\n     */\n    static IMuxer createEncoder(MuxerType muxerType) {\n        IMuxer result;\n        switch (muxerType) {\n            case MP4:\n                result = IMuxer.MPEG_4;\n                break;\n            default:\n                throw new UnsupportedOperationException();\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/muxer/com/sharry/lib/media/recorder/MuxerType.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.MediaMuxer;\n\n/**\n * 混音器(描述视频的封装格式)\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/21/2019 5:13 PM\n */\npublic enum MuxerType {\n\n    MP4(\"video/mp4\", \".mp4\");\n\n    private String mime;\n    private String suffix;\n\n    /**\n     * 定义录音类型\n     *\n     * @param mime   编码格式\n     * @param suffix 录音文件扩展名\n     */\n    MuxerType(String mime, String suffix) {\n        this.mime = mime;\n        this.suffix = suffix;\n    }\n\n    public String getMIME() {\n        return mime;\n    }\n\n    public String getFileSuffix() {\n        return suffix;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/pcmprovider/com/sharry/lib/media/recorder/DefaultPCMProvider.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.media.AudioFormat;\nimport android.media.AudioRecord;\nimport android.media.MediaRecorder;\n\n/**\n * 系统的音频录制引擎\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-26 15:34\n */\npublic class DefaultPCMProvider implements IPCMProvider, Runnable {\n\n    private final AudioRecord mImpl;\n    private final int mMinBufferSize;\n    private final Object mLockPause = new Object();\n\n    private OnPCMChangedListener mListener;\n    private Thread mRecordThread;\n    private volatile boolean isStart;\n    private volatile boolean isPause;\n\n    DefaultPCMProvider() {\n        // 获取最小录音缓冲区大小\n        this.mMinBufferSize = AudioRecord.getMinBufferSize(\n                44100,\n                AudioFormat.CHANNEL_IN_STEREO,\n                AudioFormat.ENCODING_PCM_16BIT\n        );\n        // 构建实体对象\n        this.mImpl = new AudioRecord(\n                MediaRecorder.AudioSource.MIC,    // 采样的输入设备\n                44100,             // 采样率\n                AudioFormat.CHANNEL_IN_STEREO,    // 渠道设置(双单声道录制)\n                AudioFormat.ENCODING_PCM_16BIT,   // 输出的格式, 输出的源数据均为 PCM\n                mMinBufferSize                    // 数据缓冲的大小\n        );\n    }\n\n    @Override\n    public void start() {\n        isStart = true;\n        mRecordThread = new Thread(this);\n        mRecordThread.start();\n    }\n\n    @Override\n    public void pause() {\n        isPause = true;\n    }\n\n    @Override\n    public void resume() {\n        isPause = false;\n        synchronized (mLockPause) {\n            mLockPause.notify();\n        }\n    }\n\n    @Override\n    public void stop() {\n        isStart = false;\n        isPause = false;\n        synchronized (mLockPause) {\n            mLockPause.notify();\n        }\n        if (mRecordThread != null) {\n            try {\n                mRecordThread.join(1000);\n            } catch (InterruptedException e) {\n                // ignore.\n            }\n        }\n        mImpl.stop();\n        mImpl.release();\n    }\n\n    @Override\n    public void setOnPCMChangedListener(OnPCMChangedListener listener) {\n        this.mListener = listener;\n    }\n\n    @Override\n    public void run() {\n        mImpl.startRecording();\n        byte[] pcmData = new byte[mMinBufferSize];\n        while (isStart) {\n            if (isPause) {\n                synchronized (mLockPause) {\n                    try {\n                        mLockPause.wait();\n                    } catch (InterruptedException e) {\n                        e.printStackTrace();\n                    }\n                }\n                continue;\n            }\n            // 获取录制的音频流\n            mImpl.read(pcmData, 0, mMinBufferSize);\n            if (mListener != null) {\n                mListener.OnPCMChanged(pcmData);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/pcmprovider/com/sharry/lib/media/recorder/IPCMProvider.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport androidx.annotation.WorkerThread;\n\n/**\n * 音频 PCM 数据源的提供者\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-26 16:19\n */\npublic interface IPCMProvider {\n\n    void start();\n\n    void pause();\n\n    void resume();\n\n    void stop();\n\n    void setOnPCMChangedListener(OnPCMChangedListener listener);\n\n    interface OnPCMChangedListener {\n\n        @WorkerThread\n        void OnPCMChanged(byte[] pcmData);\n\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/pcmprovider/com/sharry/lib/media/recorder/OpenSLESPCMProvider.java",
    "content": "package com.sharry.lib.media.recorder;\n\n/**\n * 使用 OpenSL ES 实现的音频录制引擎\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-26 15:34\n */\npublic class OpenSLESPCMProvider implements IPCMProvider, IPCMProvider.OnPCMChangedListener {\n\n    static {\n        System.loadLibrary(\"smedia-recorder\");\n    }\n\n    private OnPCMChangedListener listener;\n\n    @Override\n    public void start() {\n        nativeStart();\n    }\n\n    @Override\n    public void pause() {\n        nativePause();\n    }\n\n    @Override\n    public void resume() {\n        nativeResume();\n    }\n\n    @Override\n    public void stop() {\n        nativeStop();\n        // 防止内存泄漏\n        listener = null;\n    }\n\n    @Override\n    public void OnPCMChanged(byte[] pcmData) {\n        if (listener != null) {\n            listener.OnPCMChanged(pcmData);\n        }\n    }\n\n    @Override\n    public void setOnPCMChangedListener(OnPCMChangedListener listener) {\n        this.listener = listener;\n    }\n\n    // native method.\n    private native void nativeStart();\n\n    private native void nativePause();\n\n    private native void nativeResume();\n\n    private native void nativeStop();\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/recorder/com/sharry/lib/media/recorder/AudioRecorder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport android.net.Uri;\nimport android.util.Log;\n\nimport androidx.annotation.WorkerThread;\n\nimport java.io.File;\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * 音频信息录制者\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-15 17:36\n */\nfinal class AudioRecorder extends BaseMediaRecorder implements IAudioEncoder.Callback, IPCMProvider.OnPCMChangedListener {\n\n    /**\n     * Fields.\n     */\n    private final Options.Audio mOptions;\n    private final IPCMProvider mProvider;\n    private final IAudioEncoder mEncoder;\n    private final IAudioEncoder.Context mEncodeContext;\n    private IAudioEncoder.Callback mEncodeCallback;\n\n    AudioRecorder(Context context, Options.Audio options, IRecorderCallback callback) {\n        super(context, callback);\n        this.mOptions = options;\n        // 创建 PCM 数据提供者\n        this.mProvider = options.getPcmProvider() == null ? new OpenSLESPCMProvider()\n                : options.getPcmProvider();\n        mProvider.setOnPCMChangedListener(this);\n        // 创建编码上下文\n        try {\n            FileDescriptor fd = null;\n            if (!options.isJustEncode()) {\n                if (VersionUtil.isQ()) {\n                    // Android Q 以上以 URI 为主\n                    mOutputUri = FileUtil.createAudioPendingItem(context, options.getRelativePath(),\n                            options.getAudioEncodeType().getMIME(),\n                            options.getAudioEncodeType().getFileSuffix());\n                    mOutputFile = new File(FileUtil.getAudioPath(context, mOutputUri));\n                    fd = context.getContentResolver().openFileDescriptor(mOutputUri, \"w\")\n                            .getFileDescriptor();\n                } else {\n                    // Android Q 以下以文件为主\n                    mOutputFile = FileUtil.createAudioFile(context, options.getRelativePath(),\n                            options.getAudioEncodeType().getFileSuffix());\n                    mOutputUri = FileUtil.getUriFromFile(context, options.getAuthority(), mOutputFile);\n                    Uri uri = FileUtil.getUriFromFile(context, options.getAuthority(), mOutputFile);\n                    fd = context.getContentResolver().openFileDescriptor(uri, \"w\")\n                            .getFileDescriptor();\n                }\n            }\n            mEncodeContext = new IAudioEncoder.Context(\n                    options.getSampleRate(),\n                    options.getChannelLayout(),\n                    options.getPerSampleSize(),\n                    options.isJustEncode(),\n                    fd,\n                    this\n            );\n        } catch (IOException e) {\n            throw new UnsupportedOperationException(\"Please ensure file can create correct.\");\n        }\n        // 创建编码实现者\n        this.mEncoder = EncoderFactory.create(mOptions.getAudioEncodeType());\n    }\n\n    // //////////////////////////////////// PCM 数据源回调 ////////////////////////////////////\n\n    @Override\n    @WorkerThread\n    public void OnPCMChanged(byte[] pcmData) {\n        try {\n            mEncoder.encode(pcmData);\n        } catch (Throwable e) {\n            performRecordFailed(IRecorderCallback.ERROR_ENCODE_FAILED, e);\n        }\n    }\n\n    // //////////////////////////////////// 音频编码回调 ////////////////////////////////////\n\n    @Override\n    @WorkerThread\n    public void onAudioFormatChanged(MediaFormat outputFormat) {\n        if (mEncodeCallback != null) {\n            mEncodeCallback.onAudioFormatChanged(outputFormat);\n        }\n    }\n\n    @Override\n    @WorkerThread\n    @SuppressLint(\"WrongThread\")\n    public void onAudioEncoded(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {\n        // 回调进度\n        long recordTime = bufferInfo.presentationTimeUs / 1000;\n        mCallback.onProgress(recordTime);\n        // 回调数据\n        if (mEncodeCallback != null) {\n            mEncodeCallback.onAudioEncoded(byteBuffer, bufferInfo);\n        }\n        // 超过了录制时长, 自动完成\n        if (recordTime >= mOptions.getDuration()) {\n            complete();\n        }\n    }\n\n    // //////////////////////////////////// Recorder 生命周期 ////////////////////////////////////\n\n    @Override\n    public void start() {\n        if (isRecording) {\n            Log.i(TAG, \"Is already start.\");\n            return;\n        }\n        isRecording = true;\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // 准备编码器\n                try {\n                    mEncoder.prepare(mEncodeContext);\n                } catch (Throwable e) {\n                    performRecordFailed(IRecorderCallback.ERROR_ENCODER_PREPARE_FAILED, e);\n                }\n                // 开始录制\n                try {\n                    mProvider.start();\n                } catch (Throwable throwable) {\n                    performRecordFailed(IRecorderCallback.ERROR_START_FAILED, throwable);\n                }\n            }\n        });\n    }\n\n    @Override\n    public void pause() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        mProvider.pause();\n        mCallback.onPause();\n    }\n\n    @Override\n    public void resume() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        mProvider.resume();\n        mCallback.onResume();\n    }\n\n    @Override\n    public void cancel() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // 停止录制\n                stop();\n                // 删除文件\n                deleteRecordFile();\n                // 回调录制取消\n                mCallback.onCancel();\n            }\n        });\n    }\n\n    @Override\n    public void complete() {\n        if (!isRecording) {\n            return;\n        }\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // 停止录制\n                stop();\n                if (VersionUtil.isQ()) {\n                    FileUtil.publishPendingItem(mContext, mOutputUri);\n                } else {\n                    FileUtil.notifyMediaStore(mContext, mOutputFile.getAbsolutePath());\n                }\n                // 回调录制完成\n                mCallback.onComplete(mOutputUri, mOutputFile);\n            }\n        });\n    }\n\n    @Override\n    protected void stop() {\n        if (isRecording) {\n            mProvider.stop();\n            mEncoder.stop();\n            isRecording = false;\n        }\n    }\n\n    void setEncodeCallback(IAudioEncoder.Callback callback) {\n        this.mEncodeCallback = callback;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/recorder/com/sharry/lib/media/recorder/BaseMediaRecorder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.WorkerThread;\n\nimport java.io.File;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-15\n */\nabstract class BaseMediaRecorder implements IMediaRecorder {\n\n    protected static final String TAG = IMediaRecorder.class.getSimpleName();\n    protected final Context mContext;\n    protected final IRecorderCallback mCallback;\n    protected volatile boolean isRecording = false;\n\n    /**\n     * Android Q 操作 URI\n     */\n    @TargetApi(29)\n    protected Uri mOutputUri;\n\n    /**\n     * Android Q 以下, 使用文件存储\n     */\n    protected File mOutputFile;\n\n    BaseMediaRecorder(Context context, final IRecorderCallback callback) {\n        this.mContext = context;\n        this.mCallback = (IRecorderCallback) Proxy.newProxyInstance(\n                this.getClass().getClassLoader(),\n                new Class[]{IRecorderCallback.class},\n                new InvocationHandler() {\n\n                    final Handler mainThreadHandler = new Handler(Looper.getMainLooper());\n\n                    @Override\n                    public Object invoke(Object proxy, final Method method, final Object[] args) {\n                        // Hook 方法让其在主线程回调\n                        if (callback == null) {\n                            return null;\n                        }\n                        mainThreadHandler.post(new Runnable() {\n                            @Override\n                            public void run() {\n                                try {\n                                    method.invoke(callback, args);\n                                } catch (Throwable e) {\n                                    e.printStackTrace();\n                                }\n                            }\n                        });\n                        return null;\n                    }\n                }\n        );\n    }\n\n    /**\n     * 处理录制失败的情况\n     *\n     * @param errorCode 错误码\n     * @param e         异常\n     */\n    @WorkerThread\n    @SuppressLint(\"WrongThread\")\n    void performRecordFailed(@IRecorderCallback.ErrorCode final int errorCode, @NonNull final Throwable e) {\n        Log.i(TAG, \"Record failed, errorCode is: \" + errorCode, e);\n        // 释放资源\n        stop();\n        // 删除文件\n        deleteRecordFile();\n        // 回调录制失败\n        mCallback.onFailed(errorCode, e);\n    }\n\n    /**\n     * 执行录制文件的删除\n     */\n    void deleteRecordFile() {\n        if (VersionUtil.isQ()) {\n            FileUtil.delete(mContext, mOutputUri);\n        } else {\n            FileUtil.delete(mContext, mOutputFile);\n        }\n        mOutputUri = null;\n        mOutputFile = null;\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        stop();\n        super.finalize();\n    }\n\n    protected abstract void stop();\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/recorder/com/sharry/lib/media/recorder/VideoRecorder.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport android.util.Log;\n\nimport androidx.annotation.WorkerThread;\n\nimport com.sharry.lib.camera.SCameraView;\nimport com.sharry.lib.camera.Size;\n\nimport java.io.File;\nimport java.nio.ByteBuffer;\n\nimport static com.sharry.lib.media.recorder.IRecorderCallback.ERROR_MUXER_FAILED;\nimport static com.sharry.lib.media.recorder.Options.Video.RESOLUTION_1080P;\nimport static com.sharry.lib.media.recorder.Options.Video.RESOLUTION_480P;\nimport static com.sharry.lib.media.recorder.Options.Video.RESOLUTION_720P;\n\n/**\n * 视频信息录制者\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-15 17:36\n */\nfinal class VideoRecorder extends BaseMediaRecorder implements IAudioEncoder.Callback, IVideoEncoder.Callback {\n\n    private final AudioRecorder mAudio;\n    private final IVideoEncoder mEncoder;\n    private final IVideoEncoder.Context mEncodeContext;\n    private final IMuxer mMuxer;\n\n    VideoRecorder(Context context, Options.Video options, SCameraView cameraView, IRecorderCallback callback) {\n        super(context, callback);\n        // init audio record\n        this.mAudio = new AudioRecorder(\n                context,\n                options.getAudioOptions().reBuilder().setIsJustEncode(true).build(),\n                null\n        );\n        this.mAudio.setEncodeCallback(this);\n        // inflate Context\n        int[] frameSize = new int[2];\n        calculateRecordFrameSize(options.getResolution(), frameSize, cameraView.getPreviewer().getSize(), cameraView.isLandscape());\n        this.mEncodeContext = new IVideoEncoder.Context(\n                frameSize[0], frameSize[1],\n                options.getFrameRate(),\n                cameraView.getPreviewer().getRenderer().getPreviewerTextureId(),\n                cameraView.getPreviewer().getEglContext(),\n                this\n        );\n        // Step1. Create an instance of video encoder.\n        this.mEncoder = EncoderFactory.create(options.getVideoEncodeType());\n        // Step2. Create an instance of video muxer and prepare.\n        this.mMuxer = MuxerFactory.createEncoder(options.getMuxerType());\n        if (VersionUtil.isQ()) {\n            this.mOutputUri = FileUtil.createVideoPendingItem(context, options.getRelativePath(),\n                    options.getMuxerType().getMIME(), options.getMuxerType().getFileSuffix());\n            this.mOutputFile = new File(FileUtil.getVideoPath(context, mOutputUri));\n        } else {\n            this.mOutputFile = FileUtil.createVideoFile(context, options.getRelativePath(),\n                    options.getMuxerType().getFileSuffix());\n            this.mOutputUri = FileUtil.getUriFromFile(context, options.getAuthority(), mOutputFile);\n        }\n    }\n\n    // //////////////////////////////////// IAudioEncoder.Callback ////////////////////////////////////\n\n    @Override\n    public void onAudioFormatChanged(MediaFormat outputFormat) {\n        mMuxer.addAudioTrack(outputFormat);\n    }\n\n    @Override\n    public void onAudioEncoded(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {\n        // 回调录制进度\n        mCallback.onProgress(bufferInfo.presentationTimeUs / 1000);\n        // 合并音视频\n        try {\n            mMuxer.execute(\n                    IMuxer.Parcel.newInstance(IMuxer.Parcel.TRACK_AUDIO, byteBuffer, bufferInfo)\n            );\n        } catch (Throwable e) {\n            performRecordFailed(ERROR_MUXER_FAILED, e);\n        }\n    }\n\n    // //////////////////////////////////// IVideoEncoder.Callback  ////////////////////////////////////\n\n    @Override\n    public void onVideoFormatChanged(MediaFormat outputFormat) {\n        mMuxer.addVideoTrack(outputFormat);\n    }\n\n    @Override\n    public void onVideoEncoded(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {\n        // 回调录制进度\n        mCallback.onProgress(bufferInfo.presentationTimeUs / 1000);\n        // 合并音视频\n        try {\n            mMuxer.execute(\n                    IMuxer.Parcel.newInstance(IMuxer.Parcel.TRACK_VIDEO, byteBuffer, bufferInfo)\n            );\n        } catch (Throwable e) {\n            performRecordFailed(ERROR_MUXER_FAILED, e);\n        }\n    }\n\n    // //////////////////////////////////// IMediaRecorder ////////////////////////////////////\n\n    @Override\n    public void start() {\n        if (isRecording) {\n            Log.i(TAG, \"Is already start.\");\n            return;\n        }\n        isRecording = true;\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // prepare encoder.\n                try {\n                    mEncoder.prepare(mEncodeContext);\n                } catch (Throwable e) {\n                    performRecordFailed(IRecorderCallback.ERROR_ENCODER_PREPARE_FAILED, e);\n                    return;\n                }\n                // prepare muxer.\n                try {\n                    if (VersionUtil.isQ()) {\n                        mMuxer.prepare(mContext, mOutputUri);\n                    } else {\n                        mMuxer.prepare(mContext, mOutputFile);\n                    }\n                } catch (Throwable e) {\n                    performRecordFailed(IRecorderCallback.ERROR_MUXER_PREPARE_FAILED, e);\n                    return;\n                }\n                // start everything.\n                mEncoder.start();\n                mAudio.start();\n                mCallback.onStart();\n            }\n        });\n    }\n\n    @Override\n    public void pause() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        mEncoder.pause();\n        mAudio.pause();\n        mCallback.onPause();\n    }\n\n    @Override\n    public void resume() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        mEncoder.resume();\n        mAudio.resume();\n        mCallback.onResume();\n    }\n\n    @Override\n    public void cancel() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // 停止录制\n                stop();\n                // 删除文件\n                deleteRecordFile();\n                // 回调取消\n                mCallback.onCancel();\n            }\n        });\n    }\n\n    @Override\n    public void complete() {\n        if (!isRecording) {\n            Log.i(TAG, \"Not recording.\");\n            return;\n        }\n        AVPoolExecutor.getInstance().execute(new Runnable() {\n            @Override\n            public void run() {\n                // 释放资源\n                stop();\n                if (VersionUtil.isQ()) {\n                    FileUtil.publishPendingItem(mContext, mOutputUri);\n                } else {\n                    FileUtil.notifyMediaStore(mContext, mOutputFile.getAbsolutePath());\n                }\n                // 回调完成\n                mCallback.onComplete(mOutputUri, mOutputFile);\n            }\n        });\n    }\n\n    // //////////////////////////////////// BaseMediaRecorder ////////////////////////////////////\n\n    @Override\n    @WorkerThread\n    protected void stop() {\n        if (isRecording) {\n            // 音频停止\n            mAudio.stop();\n            // 停止编码器\n            mEncoder.stop();\n            // 停止音视频合并\n            mMuxer.stop();\n            // 变更标记位\n            isRecording = false;\n        }\n    }\n\n    /**\n     * 计算录制时视频帧的尺寸\n     *\n     * @param resolution    需要录制的分辨率\n     * @param frameSize     传出参数, 用于保存计算后的数据\n     * @param previewerSize 相机数据源的尺寸\n     * @param isLandscape   手机是否为横屏\n     */\n    private void calculateRecordFrameSize(int resolution, int[] frameSize, Size previewerSize, boolean isLandscape) {\n        float resolutionWidth, resolutionHeight;\n        switch (resolution) {\n            case RESOLUTION_1080P:\n                resolutionWidth = 1080f;\n                resolutionHeight = 1920f;\n                break;\n            case RESOLUTION_480P:\n                resolutionWidth = 480f;\n                resolutionHeight = 720f;\n                break;\n            case RESOLUTION_720P:\n            default:\n                resolutionWidth = 720f;\n                resolutionHeight = 1280f;\n                break;\n        }\n        // 若为横屏, 则翻转宽高\n        if (isLandscape) {\n            float temp = resolutionWidth;\n            resolutionWidth = resolutionHeight;\n            resolutionHeight = temp;\n        }\n        float scale = Math.min(resolutionWidth / previewerSize.getWidth(),\n                resolutionHeight / previewerSize.getHeight());\n        // 预览数据的尺寸比目标分辨率小, 则直接使用预览尺寸\n        if (scale >= 1.0f) {\n            frameSize[0] = previewerSize.getWidth();\n            frameSize[1] = previewerSize.getHeight();\n        }\n        // 缩放预览尺寸到符合分辨率的标准之后再进行采集\n        else {\n            frameSize[0] = (int) (previewerSize.getWidth() * scale);\n            frameSize[1] = (int) (previewerSize.getHeight() * scale);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/utils/com/sharry/lib/media/recorder/AVPoolExecutor.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.CancellationException;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.RejectedExecutionHandler;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 处理音视频录制的线程池\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 12/29/2018 4:40 PM\n */\nclass AVPoolExecutor extends ThreadPoolExecutor {\n\n    private static final String TAG = AVPoolExecutor.class.getSimpleName();\n    //    Thread args\n    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();\n    private static final int INIT_THREAD_COUNT = CPU_COUNT + 1;\n    private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT;\n    private static final long SURPLUS_THREAD_LIFE = 30L;\n\n    private static AVPoolExecutor sInstance;\n\n    static {\n        sInstance = new AVPoolExecutor(\n                INIT_THREAD_COUNT,\n                MAX_THREAD_COUNT,\n                SURPLUS_THREAD_LIFE,\n                TimeUnit.SECONDS,\n                new ArrayBlockingQueue<Runnable>(64),\n                new ThreadFactory() {\n                    @Override\n                    public Thread newThread(@NonNull Runnable r) {\n                        Thread thread = new Thread(r, AVPoolExecutor.class.getSimpleName());\n                        thread.setDaemon(false);\n                        return thread;\n                    }\n                }\n        );\n    }\n\n    public static AVPoolExecutor getInstance() {\n        return sInstance;\n    }\n\n    private AVPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,\n                           BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {\n        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,\n                new RejectedExecutionHandler() {\n                    @Override\n                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {\n                        Log.e(TAG, \"Task rejected, too many task!\");\n                    }\n                });\n    }\n\n    /**\n     * Handle exceptions when thread has completed.\n     *\n     * @param r the runnable that has completed\n     * @param t the exception that caused termination, or null if\n     */\n    @Override\n    protected void afterExecute(Runnable r, Throwable t) {\n        super.afterExecute(r, t);\n        if (t == null && r instanceof Future<?>) {\n            try {\n                ((Future<?>) r).get();\n            } catch (CancellationException ce) {\n                t = ce;\n            } catch (ExecutionException ee) {\n                t = ee.getCause();\n            } catch (InterruptedException ie) {\n                Thread.currentThread().interrupt(); // ignore/resetMatrix\n            }\n        }\n        if (t != null) {\n            Log.e(TAG, \"Running task appeared exception! Thread [\" +\n                    Thread.currentThread().getName() + \"], because [\" + t.getMessage() + \"]\\n\" +\n                    t.getMessage());\n        }\n    }\n\n}"
  },
  {
    "path": "lib-media-recorder/src/main/utils/com/sharry/lib/media/recorder/FileUtil.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.annotation.TargetApi;\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.media.MediaScannerConnection;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.text.format.DateFormat;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\nimport java.io.File;\nimport java.util.Calendar;\nimport java.util.Locale;\n\n/**\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 1/22/2019 4:31 PM\n */\nclass FileUtil {\n\n    /**\n     * 创建音频 URI\n     */\n    @NonNull\n    @TargetApi(29)\n    static Uri createAudioPendingItem(Context context, String relativePath, String mime, String suffix) {\n        String fileName = \"audio_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + suffix;\n        ContentValues values = new ContentValues();\n        // 创建相对路径\n        if (TextUtils.isEmpty(relativePath)) {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC);\n        } else {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + \"/\" + relativePath);\n        }\n        values.put(MediaStore.Audio.Media.MIME_TYPE, mime);\n        values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);\n        values.put(MediaStore.Images.Media.IS_PENDING, 1);\n        ContentResolver resolver = context.getContentResolver();\n        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {\n            return resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);\n        } else {\n            return resolver.insert(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, values);\n        }\n    }\n\n    /**\n     * 创建音频文件\n     */\n    static File createAudioFile(Context context, String relativePath, String suffix) {\n        String fileName = \"audio_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + suffix;\n        File dir = TextUtils.isEmpty(relativePath) ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)\n                : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), relativePath);\n        try {\n            // 获取默认路径\n            if (!dir.exists()) {\n                dir.mkdirs();\n            }\n            File file = new File(dir, fileName);\n            if (file.exists()) {\n                file.delete();\n            }\n            file.createNewFile();\n            return file;\n        } catch (Throwable e) {\n            throw new UnsupportedOperationException(\"Cannot create file at:  \" + dir);\n        }\n    }\n\n    /**\n     * 创建视频 URI\n     */\n    @NonNull\n    @TargetApi(29)\n    static Uri createVideoPendingItem(Context context, String relativePath, String mime, String suffix) {\n        String fileName = \"video_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + suffix;\n        ContentValues values = new ContentValues();\n        // 创建相对路径\n        if (TextUtils.isEmpty(relativePath)) {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);\n        } else {\n            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + \"/\" + relativePath);\n        }\n        values.put(MediaStore.Video.Media.MIME_TYPE, mime);\n        values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);\n        values.put(MediaStore.Images.Media.IS_PENDING, 1);\n        ContentResolver resolver = context.getContentResolver();\n        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {\n            return resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);\n        } else {\n            return resolver.insert(MediaStore.Video.Media.INTERNAL_CONTENT_URI, values);\n        }\n    }\n\n    /**\n     * 创建视频 File\n     */\n    @NonNull\n    static File createVideoFile(Context context, String relativePath, String suffix) {\n        String fileName = \"video_\" + DateFormat.format(\"yyyyMMdd_HH_mm_ss\",\n                Calendar.getInstance(Locale.CHINA)) + suffix;\n        File dir = TextUtils.isEmpty(relativePath) ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)\n                : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), relativePath);\n        try {\n            // 获取默认路径\n            if (!dir.exists()) {\n                dir.mkdirs();\n            }\n            File file = new File(dir, fileName);\n            if (file.exists()) {\n                file.delete();\n            }\n            file.createNewFile();\n            return file;\n        } catch (Throwable e) {\n            throw new UnsupportedOperationException(\"Cannot create file at:  \" + dir);\n        }\n    }\n\n    /**\n     * 获取 URI\n     */\n    static Uri getUriFromFile(Context context, String authority, File file) {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?\n                FileProvider.getUriForFile(context, authority, file) : Uri.fromFile(file);\n    }\n\n    /**\n     * 删除 Uri\n     */\n    static void delete(Context context, Uri uri) {\n        if (context != null && uri != null) {\n            context.getContentResolver().delete(uri, null, null);\n        }\n    }\n\n    /**\n     * 删除文件\n     */\n    static void delete(Context context, File file) {\n        if (context != null && file != null && file.exists() && file.isFile()) {\n            if (file.delete()) {\n                notifyMediaStore(context, file.getAbsolutePath());\n            }\n        }\n    }\n\n    /**\n     * 通知 MediaStore 文件发布\n     */\n    @TargetApi(29)\n    static void publishPendingItem(Context context, Uri item) {\n        if (context == null || item == null) {\n            return;\n        }\n        ContentValues values = new ContentValues();\n        values.put(MediaStore.Images.Media.IS_PENDING, 0);\n        context.getContentResolver().update(item, values, null, null);\n    }\n\n    /**\n     * 通知 MediaStore 文件更替\n     */\n    static void notifyMediaStore(Context context, String filePath) {\n        if (context == null || TextUtils.isEmpty(filePath)) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            MediaScannerConnection.scanFile(context.getApplicationContext(), new String[]{filePath}, null, null);\n        } else {\n            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(\"file://\" + Environment.getExternalStorageDirectory())));\n        }\n    }\n\n    /**\n     * 根据视频 URI 获取视频路径\n     */\n    static String getVideoPath(final Context context, final Uri uri) {\n        if (null == uri) {\n            return null;\n        }\n        final String scheme = uri.getScheme();\n        String data = null;\n\n        if (scheme == null) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {\n            Cursor cursor = context.getContentResolver().query(uri,\n                    new String[]{MediaStore.Video.VideoColumns.DATA}, null,\n                    null, null\n            );\n            if (null != cursor) {\n                if (cursor.moveToFirst()) {\n                    int index = cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA);\n                    if (index > -1) {\n                        data = cursor.getString(index);\n                    }\n                }\n                cursor.close();\n            }\n        }\n        return data;\n    }\n\n    /**\n     * 根据音频 URI 获取音频路径\n     */\n    static String getAudioPath(final Context context, final Uri uri) {\n        if (null == uri) {\n            return null;\n        }\n        final String scheme = uri.getScheme();\n        String data = null;\n\n        if (scheme == null) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n            data = uri.getPath();\n        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {\n            Cursor cursor = context.getContentResolver().query(uri,\n                    new String[]{MediaStore.Audio.AudioColumns.DATA}, null,\n                    null, null\n            );\n            if (null != cursor) {\n                if (cursor.moveToFirst()) {\n                    int index = cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA);\n                    if (index > -1) {\n                        data = cursor.getString(index);\n                    }\n                }\n                cursor.close();\n            }\n        }\n        return data;\n    }\n\n}\n"
  },
  {
    "path": "lib-media-recorder/src/main/utils/com/sharry/lib/media/recorder/NetworkUtil.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.Manifest;\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.telephony.TelephonyManager;\n\nimport androidx.annotation.RequiresPermission;\n\n/**\n * 网络连接工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/11/8 13:34\n */\nclass NetworkUtil {\n\n    public static final int NETWORK_NONE = 0; // 没有网络连接\n    public static final int NETWORK_2G = 2; // 2G\n    public static final int NETWORK_3G = 3; // 3G\n    public static final int NETWORK_4G = 4; // 4G\n    public static final int NETWORK_WIFI = 5; // 手机流量\n\n    /**\n     * 获取运营商名字\n     *\n     * @param context context\n     * @return int\n     */\n    public static String getOperatorName(Context context) {\n        /*\n         * getSimOperatorName() 就可以直接获取到运营商的名字\n         * 也可以使用 IMSI 获取，getSimOperator()，然后根据返回值判断，例如\"46000\"为移动\n         * IMSI 相关链接：http://baike.baidu.com/item/imsi\n         */\n        TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);\n        // getSimOperatorName 就可以直接获取到运营商的名字\n        return telephonyManager.getSimOperatorName();\n    }\n\n    /**\n     * 获取当前网络连接的类型\n     *\n     * @param context context\n     * @return int\n     */\n    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)\n    public static int getNetworkState(Context context) {\n        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); // 获取网络服务\n        // 为空则认为无网络\n        if (null == connManager) {\n            return NETWORK_NONE;\n        }\n        // 获取网络类型，如果为空，返回无网络\n        NetworkInfo activeNetInfo = connManager.getActiveNetworkInfo();\n        if (activeNetInfo == null || !activeNetInfo.isAvailable()) {\n            return NETWORK_NONE;\n        }\n        // 判断是否为WIFI\n        NetworkInfo wifiInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);\n        if (null != wifiInfo) {\n            NetworkInfo.State state = wifiInfo.getState();\n            if (null != state) {\n                if (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.CONNECTING) {\n                    return NETWORK_WIFI;\n                }\n            }\n        }\n        // 若不是WIFI，则去判断是2G、3G、4G网\n        TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);\n        int networkType = telephonyManager.getNetworkType();\n        switch (networkType) {\n            /*\n             GPRS : 2G(2.5) General Packet Radia Service 114kbps\n             EDGE : 2G(2.75G) Enhanced Data Rate for GSM Evolution 384kbps\n             UMTS : 3G WCDMA 联通3G Universal Mobile Telecommunication System 完整的3G移动通信技术标准\n             CDMA : 2G 电信 Code Division Multiple Access 码分多址\n             EVDO_0 : 3G (EVDO 全程 CDMA2000 1xEV-DO) Evolution - Data Only (Data Optimized) 153.6kps - 2.4mbps 属于3G\n             EVDO_A : 3G 1.8mbps - 3.1mbps 属于3G过渡，3.5G\n             1xRTT : 2G CDMA2000 1xRTT (RTT - 无线电传输技术) 144kbps 2G的过渡,\n             HSDPA : 3.5G 高速下行分组接入 3.5G WCDMA High Speed Downlink Packet Access 14.4mbps\n             HSUPA : 3.5G High Speed Uplink Packet Access 高速上行链路分组接入 1.4 - 5.8 mbps\n             HSPA : 3G (分HSDPA,HSUPA) High Speed Packet Access\n             IDEN : 2G Integrated Dispatch Enhanced Networks 集成数字增强型网络 （属于2G，来自维基百科）\n             EVDO_B : 3G EV-DO Rev.B 14.7Mbps 下行 3.5G\n             LTE : 4G Long Term Evolution FDD-LTE 和 TDD-LTE , 3G过渡，升级版 LTE Advanced 才是4G\n             EHRPD : 3G CDMA2000向LTE 4G的中间产物 Evolved High Rate Packet Data HRPD的升级\n             HSPAP : 3G HSPAP 比 HSDPA 快些\n             */\n            // 2G网络\n            case TelephonyManager.NETWORK_TYPE_GPRS:\n            case TelephonyManager.NETWORK_TYPE_CDMA:\n            case TelephonyManager.NETWORK_TYPE_EDGE:\n            case TelephonyManager.NETWORK_TYPE_1xRTT:\n            case TelephonyManager.NETWORK_TYPE_IDEN:\n                return NETWORK_2G;\n            // 3G网络\n            case TelephonyManager.NETWORK_TYPE_EVDO_A:\n            case TelephonyManager.NETWORK_TYPE_UMTS:\n            case TelephonyManager.NETWORK_TYPE_EVDO_0:\n            case TelephonyManager.NETWORK_TYPE_HSDPA:\n            case TelephonyManager.NETWORK_TYPE_HSUPA:\n            case TelephonyManager.NETWORK_TYPE_HSPA:\n            case TelephonyManager.NETWORK_TYPE_EVDO_B:\n            case TelephonyManager.NETWORK_TYPE_EHRPD:\n            case TelephonyManager.NETWORK_TYPE_HSPAP:\n                return NETWORK_3G;\n            // 4G网络\n            case TelephonyManager.NETWORK_TYPE_LTE:\n                return NETWORK_4G;\n            default:\n                return NETWORK_NONE;\n        }\n    }\n\n    /**\n     * 判断网络是否连接\n     *\n     * @param context context\n     * @return true/false\n     */\n    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)\n    public static boolean isNetConnected(Context context) {\n        ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n        if (connectivity != null) {\n            NetworkInfo info = connectivity.getActiveNetworkInfo();\n            if (info != null && info.isConnected()) {\n                if (info.getState() == NetworkInfo.State.CONNECTED) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * 判断是否wifi连接\n     *\n     * @param context context\n     * @return true/false\n     */\n    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)\n    public static synchronized boolean isWifiConnected(Context context) {\n        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n        if (connectivityManager != null) {\n            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();\n            if (networkInfo != null) {\n                int networkInfoType = networkInfo.getType();\n                if (networkInfoType == ConnectivityManager.TYPE_WIFI || networkInfoType == ConnectivityManager.TYPE_ETHERNET) {\n                    return networkInfo.isConnected();\n                }\n            }\n        }\n        return false;\n    }\n\n}"
  },
  {
    "path": "lib-media-recorder/src/main/utils/com/sharry/lib/media/recorder/VersionUtil.java",
    "content": "package com.sharry.lib.media.recorder;\n\nimport android.os.Build;\n\n/**\n * 版本控制相关的工具类\n *\n * @author Sharry <a href=\"SharryChooCHN@Gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2018/9/22 17:46\n */\nclass VersionUtil {\n\n    static boolean isJellyBeanMr1() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;\n    }\n\n    static boolean isLollipop() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;\n    }\n\n    static boolean isQ() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/.gitignore",
    "content": "# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea\n\n\n# Keystore files\n*.jks\n\n# external files\n.externalNativeBuild/"
  },
  {
    "path": "lib-opengles/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\n\ngroup = 'com.github.SharryChoo'\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n    }\n    sourceSets {\n        main {\n            java.srcDirs += 'src/main/gltextureview'\n            java.srcDirs += 'src/main/glsurfaceview'\n            java.srcDirs += 'src/main/utils'\n            jniLibs.srcDirs = ['src/main/jniLibs']\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation \"androidx.appcompat:appcompat:$supportLibraryVersion\"\n}\n"
  },
  {
    "path": "lib-opengles/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "lib-opengles/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"com.sharry.lib.opengles\" />\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/surface/ContextSharedGLSurfaceView.java",
    "content": "package com.sharry.lib.opengles.surface;\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.util.AttributeSet;\n\nimport javax.microedition.khronos.egl.EGL10;\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.egl.EGLContext;\nimport javax.microedition.khronos.egl.EGLDisplay;\n\nimport static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;\n\npublic class ContextSharedGLSurfaceView extends GLSurfaceView {\n\n    private EGLContext mEGLContext;\n\n    public ContextSharedGLSurfaceView(Context context) {\n        this(context, null);\n    }\n\n    public ContextSharedGLSurfaceView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        setEGLContextClientVersion(2);\n        // 利用 setEGLContextFactory 这种方式共享其他 GLSurfaceView 的 EGLContext\n        setEGLContextFactory(new EGLContextFactory() {\n            @Override\n            public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {\n                int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};\n                if (mEGLContext != null) {\n                    mEGLContext = egl.eglCreateContext(display, eglConfig, mEGLContext, attrib_list);\n                } else {\n                    mEGLContext = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);\n                }\n                return mEGLContext;\n            }\n\n            @Override\n            public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {\n                // ignore.\n            }\n        });\n    }\n\n    public void setEGLContext(EGLContext eglContext) {\n        this.mEGLContext = eglContext;\n    }\n\n    public EGLContext getEGLContext() {\n        return mEGLContext;\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/texture/GLTextureView.java",
    "content": "package com.sharry.lib.opengles.texture;\n\nimport android.content.Context;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGLContext;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport android.util.AttributeSet;\nimport android.view.TextureView;\n\nimport androidx.annotation.Nullable;\n\nimport com.sharry.lib.opengles.util.EglCore;\n\nimport java.lang.ref.WeakReference;\n\n/**\n * 利用 TextureView 实现对外来 SurfaceTexture 的加工绘制\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 2.0\n * @since 2019-07-27\n */\npublic class GLTextureView extends TextureView {\n\n    private volatile ITextureRenderer mRenderer;\n    private volatile RenderWorker mRenderWorker;\n\n    public GLTextureView(Context context) {\n        this(context, null);\n    }\n\n    public GLTextureView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public GLTextureView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        setSurfaceTextureListener(new SurfaceTextureListener() {\n            @Override\n            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {\n                RenderWorker renderWorker = mRenderWorker;\n                if (renderWorker == null) {\n                    // do launch\n                    renderWorker = new RenderWorker(GLTextureView.this);\n                    renderWorker.start();\n                    mRenderWorker = renderWorker;\n                }\n                renderWorker.handleSurfaceTextureChanged(surface);\n            }\n\n            @Override\n            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {\n                RenderWorker renderWorker = mRenderWorker;\n                if (renderWorker != null) {\n                    renderWorker.handleSurfaceTextureSizeChanged();\n                }\n            }\n\n            @Override\n            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {\n                RenderWorker renderWorker = mRenderWorker;\n                if (renderWorker != null) {\n                    renderWorker.quitSafely();\n                }\n                mRenderWorker = null;\n                return true;\n            }\n\n            @Override\n            public void onSurfaceTextureUpdated(SurfaceTexture surface) {\n                // nothing.\n            }\n\n        });\n    }\n\n    /**\n     * Set a renderer.\n     */\n    public void setRenderer(@Nullable ITextureRenderer renderer) {\n        if (mRenderer == renderer) {\n            return;\n        }\n        ITextureRenderer oldRenderer = mRenderer;\n        mRenderer = renderer;\n        RenderWorker renderWorker = mRenderWorker;\n        if (renderWorker != null) {\n            renderWorker.handleRendererChanged(oldRenderer);\n        }\n    }\n\n    /**\n     * request renderer.\n     */\n    public void requestRenderer() {\n        RenderWorker renderWorker = mRenderWorker;\n        if (renderWorker != null) {\n            renderWorker.handleDrawFrame();\n        }\n    }\n\n    /**\n     * Gets a EGLContext\n     *\n     * @return return a instance of EGLContext. if mRendererThread not start, will be null.\n     */\n    public EGLContext getEglContext() {\n        EGLContext res = null;\n        RenderWorker renderWorker = mRenderWorker;\n        if (renderWorker != null) {\n            res = renderWorker.mEglCore.getContext();\n        }\n        return res;\n    }\n\n    static class RenderWorker extends HandlerThread implements Handler.Callback,\n            SurfaceTexture.OnFrameAvailableListener {\n\n        private static final int MSG_SURFACE_TEXTURE_CHANGED = 0;\n        private static final int MSG_RENDERER_CHANGED = 1;\n        private static final int MSG_SURFACE_SIZE_CHANGED = 2;\n        private static final int MSG_DRAW_FRAME = 3;\n        private static final int MSG_DESTROY = 4;\n\n        private final WeakReference<GLTextureView> mWkRef;\n        private final EglCore mEglCore = new EglCore();\n        private Handler mHandler = null;\n\n        private RenderWorker(GLTextureView view) {\n            super(RenderWorker.class.getSimpleName());\n            mWkRef = new WeakReference<>(view);\n        }\n\n        ////////////////////////////////////////////////////////////////////////////\n        // Lifecycle\n        ////////////////////////////////////////////////////////////////////////////\n\n        @Override\n        public synchronized void start() {\n            super.start();\n            // 实例化 Handler\n            mHandler = new Handler(getLooper(), this);\n        }\n\n        @Override\n        public boolean quit() {\n            return quitSafely();\n        }\n\n        @Override\n        public boolean quitSafely() {\n            handleDestroy();\n            boolean res = super.quitSafely();\n            mHandler = null;\n            return res;\n        }\n\n        ////////////////////////////////////////////////////////////////////////////\n        // Handler.Callback\n        ////////////////////////////////////////////////////////////////////////////\n\n        @Override\n        public boolean handleMessage(Message msg) {\n            switch (msg.what) {\n                // 画布变更\n                case MSG_SURFACE_TEXTURE_CHANGED:\n                    if (msg.obj instanceof SurfaceTexture) {\n                        preformSurfaceTextureChanged((SurfaceTexture) msg.obj);\n                    }\n                    break;\n                // 渲染器变更\n                case MSG_RENDERER_CHANGED:\n                    if (msg.obj instanceof ITextureRenderer) {\n                        performRendererChanged((ITextureRenderer) msg.obj);\n                    }\n                    break;\n                // 画布尺寸变更\n                case MSG_SURFACE_SIZE_CHANGED:\n                    performSurfaceSizeChanged();\n                    break;\n                // 绘制数据帧\n                case MSG_DRAW_FRAME:\n                    performDrawFrame();\n                    break;\n                // 处理线程退出\n                case MSG_DESTROY:\n                    performDestroy();\n                    break;\n                default:\n                    break;\n            }\n            return false;\n        }\n\n        ////////////////////////////////////////////////////////////////////////////\n        // SurfaceTexture.OnFrameAvailableListener\n        ////////////////////////////////////////////////////////////////////////////\n\n        @Override\n        public void onFrameAvailable(SurfaceTexture surfaceTexture) {\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.sendEmptyMessage(MSG_DRAW_FRAME);\n            }\n        }\n\n        ////////////////////////////////////////////////////////////////////////////\n        // Open method invoke at UI Thread\n        ////////////////////////////////////////////////////////////////////////////\n\n        /**\n         * {@link #preformSurfaceTextureChanged}\n         */\n        void handleSurfaceTextureChanged(SurfaceTexture surface) {\n            Handler handler = mHandler;\n            if (handler != null) {\n                Message msg = Message.obtain();\n                msg.what = MSG_SURFACE_TEXTURE_CHANGED;\n                msg.obj = surface;\n                handler.sendMessage(msg);\n            }\n        }\n\n        /**\n         * {@link #performRendererChanged}\n         */\n        void handleRendererChanged(ITextureRenderer oldRenderer) {\n            Handler handler = mHandler;\n            if (handler != null) {\n                Message msg = Message.obtain();\n                msg.what = MSG_RENDERER_CHANGED;\n                msg.obj = oldRenderer;\n                handler.sendMessage(msg);\n            }\n        }\n\n        /**\n         * {@link #performSurfaceSizeChanged}\n         */\n        void handleSurfaceTextureSizeChanged() {\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.sendEmptyMessage(MSG_SURFACE_SIZE_CHANGED);\n            }\n        }\n\n        /**\n         * {@link #performSurfaceSizeChanged}\n         */\n        void handleDrawFrame() {\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.sendEmptyMessage(MSG_DRAW_FRAME);\n            }\n        }\n\n        /**\n         * {@link #performDestroy()}\n         */\n        void handleDestroy() {\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.sendEmptyMessage(MSG_DESTROY);\n            }\n        }\n\n        ////////////////////////////////////////////////////////////////////////////\n        // Private method invoke at Handler Thread\n        ////////////////////////////////////////////////////////////////////////////\n\n        private void preformSurfaceTextureChanged(SurfaceTexture surfaceTexture) {\n            // 释放之前的 EGL 环境\n            performDestroy();\n            // 重新初始化 EGL 环境\n            GLTextureView view = mWkRef.get();\n            if (view == null) {\n                return;\n            }\n            // Recreate egl context\n            mEglCore.initialize(surfaceTexture, null);\n            // invoke render lifecycle\n            ITextureRenderer renderer = view.mRenderer;\n            if (renderer != null) {\n                renderer.onAttach();\n                renderer.onSizeChanged(view.getWidth(), view.getHeight());\n            }\n        }\n\n        private void performRendererChanged(ITextureRenderer oldRenderer) {\n            // 移除所有的绘制动作\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.removeMessages(MSG_DRAW_FRAME);\n            }\n            GLTextureView view = mWkRef.get();\n            if (view == null) {\n                return;\n            }\n            // 回调之前 Renderer 的解绑方法\n            if (oldRenderer != null) {\n                oldRenderer.onDetach();\n            }\n            // 重新回调生命周期\n            ITextureRenderer renderer = view.mRenderer;\n            if (renderer != null) {\n                renderer.onAttach();\n                renderer.onSizeChanged(view.getWidth(), view.getHeight());\n            }\n        }\n\n        private void performSurfaceSizeChanged() {\n            GLTextureView view = mWkRef.get();\n            if (view == null) {\n                return;\n            }\n            ITextureRenderer renderer = view.mRenderer;\n            if (renderer != null) {\n                renderer.onSizeChanged(view.getWidth(), view.getHeight());\n            }\n        }\n\n        private void performDrawFrame() {\n            GLTextureView view = mWkRef.get();\n            if (view == null) {\n                return;\n            }\n            // 更新纹理数据\n            ITextureRenderer renderer = view.mRenderer;\n            // 执行渲染器的绘制\n            if (renderer != null) {\n                renderer.onDraw();\n            }\n            // 将 EGL 绘制的数据, 输出到 View 的 preview 中\n            mEglCore.swapBuffers();\n        }\n\n        private void performDestroy() {\n            // 移除所有待执行的消息\n            Handler handler = mHandler;\n            if (handler != null) {\n                handler.removeMessages(MSG_SURFACE_TEXTURE_CHANGED);\n                handler.removeMessages(MSG_SURFACE_SIZE_CHANGED);\n                handler.removeMessages(MSG_DRAW_FRAME);\n            }\n            // 回调 Renderer 的解绑方法\n            GLTextureView view = mWkRef.get();\n            if (view != null) {\n                ITextureRenderer renderer = view.mRenderer;\n                if (renderer != null) {\n                    renderer.onDetach();\n                }\n            }\n            // 释放该线程的 EGL 环境\n            mEglCore.release();\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/texture/ITextureRenderer.java",
    "content": "package com.sharry.lib.opengles.texture;\n\nimport androidx.annotation.WorkerThread;\n\n/**\n * OpenGL ES 基础的 Texture Renderer\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-08 14:10\n */\npublic interface ITextureRenderer {\n\n    @WorkerThread\n    void onAttach();\n\n    @WorkerThread\n    void onSizeChanged(int width, int height);\n\n    @WorkerThread\n    void onDraw();\n\n    @WorkerThread\n    void onDetach();\n\n}\n\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/util/EglCore.java",
    "content": "package com.sharry.lib.opengles.util;\n\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGL14;\nimport android.opengl.EGLConfig;\nimport android.opengl.EGLContext;\nimport android.opengl.EGLDisplay;\nimport android.opengl.EGLExt;\nimport android.opengl.EGLSurface;\nimport android.util.Log;\nimport android.view.Surface;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;\n\n/**\n * An EGL helper class.\n * <p>\n * The EGLContext must only be attached to one thread at a time.  This class is not thread-safe.\n * <p>\n * Get more details<href>https://github.com/google/grafika/blob/master/app/src/main/java/com/android/grafika/gles/EglCore.java</href>\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-29\n */\npublic class EglCore {\n\n    public static final int EGL_VERSION_2 = 2;\n    public static final int EGL_VERSION_3 = 3;\n\n    @IntDef(value = {\n            EGL_VERSION_2,\n            EGL_VERSION_3\n    })\n    private @interface EGLVersion {\n\n    }\n\n    private static final String TAG = EglCore.class.getSimpleName();\n\n    private final int mEGLVersion;\n    private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;\n    private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;\n    private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;\n\n    public EglCore() {\n        this(EGL_VERSION_2);\n    }\n\n    public EglCore(@EGLVersion int eglVersion) {\n        mEGLVersion = eglVersion;\n    }\n\n    /**\n     * Initialize EGL for a given configuration spec.\n     *\n     * @param surface    native window\n     * @param eglContext if null will create new context, false will use shared context\n     */\n    public void initialize(@NonNull Surface surface, @Nullable EGLContext eglContext) {\n        initializeInternal(surface, eglContext == null ? EGL14.EGL_NO_CONTEXT : eglContext);\n    }\n\n    /**\n     * Initialize EGL for a given configuration spec.\n     *\n     * @param surfaceTexture native window\n     * @param eglContext     if null will create new context, false will use shared context\n     */\n    public void initialize(@NonNull SurfaceTexture surfaceTexture, @Nullable EGLContext eglContext) {\n        initializeInternal(surfaceTexture, eglContext == null ? EGL14.EGL_NO_CONTEXT : eglContext);\n    }\n\n    /**\n     * Makes our EGL context current, using the supplied \"draw\" and \"read\" surfaces.\n     */\n    public void makeCurrent() {\n        if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {\n            throw new RuntimeException(\"eglMakeCurrent failed\");\n        }\n    }\n\n    /**\n     * Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.\n     */\n    public void setPresentationTime(long nsecs) {\n        EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);\n    }\n\n    /**\n     * Calls eglSwapBuffers.  Use this to \"publish\" the current frame.\n     *\n     * @return false on failure\n     */\n    public boolean swapBuffers() {\n        return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);\n    }\n\n    /**\n     * Discards all resources held by this class, notably the EGL context.  This must be\n     * called from the thread where the context was created.\n     * <p>\n     * On completion, no context will be current.\n     */\n    public void release() {\n        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {\n            EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,\n                    EGL14.EGL_NO_CONTEXT);\n            EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);\n            EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);\n            EGL14.eglTerminate(mEGLDisplay);\n        }\n        mEGLContext = EGL14.EGL_NO_CONTEXT;\n        mEGLDisplay = EGL14.EGL_NO_DISPLAY;\n        mEGLSurface = EGL14.EGL_NO_SURFACE;\n    }\n\n    /**\n     * Gets the current EGLContext\n     *\n     * @return the current EGLContext\n     */\n    public EGLContext getContext() {\n        return mEGLContext;\n    }\n\n    /**\n     * Copy from {@link android.opengl.GLSurfaceView#EglHelper}\n     */\n    private void initializeInternal(Object nativeWindow, EGLContext sharedEglContext) {\n        /*\n         * Create a connection for system native window\n         */\n        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);\n        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {\n            throw new RuntimeException(\"eglGetDisplay failed\");\n        }\n\n        /*\n         * We can now initialize EGL for that display\n         */\n        int[] version = new int[2];\n        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {\n            mEGLDisplay = null;\n            throw new RuntimeException(\"eglInitialize failed\");\n        }\n\n        /*\n         * Create EGLConfig\n         */\n        EGLConfig eglConfig = chooseConfig();\n        if (eglConfig == null) {\n            throw new RuntimeException(\"Cannot find suitable config.\");\n        }\n\n        /*\n         * Create EGLContext\n         */\n        int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLVersion, EGL14.EGL_NONE};\n        EGLContext eglContext = EGL14.eglCreateContext(mEGLDisplay, eglConfig, sharedEglContext,\n                attrib_list, 0);\n        if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {\n            mEGLContext = eglContext;\n        } else {\n            throw new RuntimeException(\"Create EGLContext failed.\");\n        }\n\n        /*\n         * Create EGLSurface\n         */\n        int[] surfaceAttribs = {EGL14.EGL_NONE};\n        mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, eglConfig, nativeWindow,\n                surfaceAttribs, 0);\n        if (mEGLSurface == null || mEGLSurface == EGL14.EGL_NO_SURFACE) {\n            throw new RuntimeException(\"createWindowSurface returned EGL_BAD_NATIVE_WINDOW.\");\n        }\n\n        /*\n         * Bind context\n         */\n        makeCurrent();\n    }\n\n    /**\n     * Finds a suitable EGLConfig.\n     */\n    private EGLConfig chooseConfig() {\n        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;\n        if (mEGLVersion >= 3) {\n            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;\n        }\n        // The actual surface is generally RGBA or RGBX, so situationally omitting alpha\n        // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()\n        // when reading into a GL_RGBA buffer.\n        int[] attribList = {\n                EGL14.EGL_RED_SIZE, 8,\n                EGL14.EGL_GREEN_SIZE, 8,\n                EGL14.EGL_BLUE_SIZE, 8,\n                EGL14.EGL_ALPHA_SIZE, 8,\n                //EGL14.EGL_DEPTH_SIZE, 16,\n                //EGL14.EGL_STENCIL_SIZE, 8,\n                EGL14.EGL_RENDERABLE_TYPE, renderableType,\n                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]\n                EGL14.EGL_NONE\n        };\n        EGLConfig[] configs = new EGLConfig[1];\n        int[] numConfigs = new int[1];\n        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs,\n                0, configs.length,\n                numConfigs, 0)) {\n            Log.w(TAG, \"unable to find RGB8888 / \" + mEGLVersion + \" EGLConfig\");\n            return null;\n        }\n        return configs[0];\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        try {\n            if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {\n                // We're limited here -- finalizers don't run on the thread that holds\n                // the EGL state, so if a surface or context is still current on another\n                // thread we can't fully release it here.  Exceptions thrown from here\n                // are quietly discarded.  Complain in the log file.\n                Log.w(TAG, \"WARNING: EglCore was not explicitly released -- state may be leaked\");\n                release();\n            }\n        } finally {\n            super.finalize();\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/util/FboHelper.java",
    "content": "package com.sharry.lib.opengles.util;\n\nimport android.opengl.GLES20;\n\nimport com.sharry.lib.opengles.texture.ITextureRenderer;\n\npublic class FboHelper implements ITextureRenderer {\n\n    private int mTextureId;\n    private int mFramebufferId;\n\n    public FboHelper() {\n    }\n\n    @Override\n    public void onAttach() {\n        createFbo();\n        createTexture();\n    }\n\n    private void createFbo() {\n        // 创建 fbo\n        int[] fBoIds = new int[1];\n        GLES20.glGenFramebuffers(1, fBoIds, 0);\n        mFramebufferId = fBoIds[0];\n    }\n\n    private void createTexture() {\n        int[] textureIds = new int[1];\n        GLES20.glGenTextures(1, textureIds, 0);\n        mTextureId = textureIds[0];\n        // 绑定纹理\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);\n        // 设置纹理环绕方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);\n        // 设置纹理过滤方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n        // 解绑纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    @Override\n    public void onSizeChanged(int width, int height) {\n        GLES20.glViewport(0, 0, width, height);\n        // 更新纹理的大小\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);\n        GLES20.glTexImage2D(\n                GLES20.GL_TEXTURE_2D,\n                0,\n                GLES20.GL_RGBA,\n                width, height,\n                0,\n                GLES20.GL_RGBA,\n                GLES20.GL_UNSIGNED_BYTE,\n                null\n        );\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n        // 将纹理绑定到 FBO 上, 作为颜色附件\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferId);\n        GLES20.glFramebufferTexture2D(\n                GLES20.GL_FRAMEBUFFER,\n                GLES20.GL_COLOR_ATTACHMENT0,  // 描述为颜色附件\n                GLES20.GL_TEXTURE_2D,\n                mTextureId,\n                0\n        );\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);\n    }\n\n    @Override\n    public void onDraw() {\n        // nothing.\n    }\n\n    @Override\n    public void onDetach() {\n        // 删除 FBO\n        if (mFramebufferId != 0) {\n            int[] fBoIds = new int[1];\n            fBoIds[0] = mFramebufferId;\n            GLES20.glDeleteFramebuffers(1, fBoIds, 0);\n        }\n        // 删除 2D 纹理\n        if (mTextureId != 0) {\n            int size = 1;\n            int[] textures = new int[size];\n            textures[0] = mTextureId;\n            GLES20.glDeleteTextures(1, textures, 0);\n        }\n    }\n\n    public void bindFramebuffer() {\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferId);\n    }\n\n    public void unbindFramebuffer() {\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);\n    }\n\n    public int getTexture2DId() {\n        return mTextureId;\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/util/GlMatrixUtil.java",
    "content": "package com.sharry.lib.opengles.util;\n\nimport android.opengl.Matrix;\n\n/**\n * 用于快捷构建 GL 的 Matrix\n *\n * @author zhuxiaoyu <a href=\"zhuxiaoyu.sharry@bytedance.com\">Contact me.</a>\n * @version 1.0\n * @since 2020/3/5\n */\npublic class GlMatrixUtil {\n\n    /**\n     * 矩阵操作的类型\n     */\n    public static final int TYPE_FIT_XY = 0;\n    public static final int TYPE_CENTER_CROP = 1;\n    public static final int TYPE_CENTER_INSIDE = 2;\n    public static final int TYPE_FIT_START = 3;\n    public static final int TYPE_FIT_END = 4;\n\n    public static void getMatrix(float[] matrix, int type, int imgWidth, int imgHeight, int viewWidth,\n                                 int viewHeight) {\n        if (imgHeight == 0 || imgWidth == 0 || viewWidth == 0 || viewHeight == 0) {\n            return;\n        }\n        // 构建视图矩阵\n        float[] viewMatrix = new float[16];\n        Matrix.setLookAtM(viewMatrix, 0,\n                0, 0, 1,\n                0, 0, 0,\n                0, 1, 0\n        );\n        // 根据 ScaleType 构建投影矩阵\n        float[] projectionMatrix = new float[16];\n        float aspectImg = (float) imgWidth / imgHeight;\n        float aspectView = (float) viewWidth / viewHeight;\n        if (aspectImg > aspectView) {\n            switch (type) {\n                case TYPE_CENTER_CROP:\n                    Matrix.orthoM(projectionMatrix, 0, -aspectView / aspectImg, aspectView / aspectImg, -1, 1, -1, 1);\n                    break;\n                case TYPE_CENTER_INSIDE:\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, -aspectImg / aspectView, aspectImg / aspectView, -1, 1);\n                    break;\n                case TYPE_FIT_START:\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, 1 - 2 * aspectImg / aspectView, 1, -1, 1);\n                    break;\n                case TYPE_FIT_END:\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, -1, 2 * aspectImg / aspectView - 1, -1, 1);\n                    break;\n                case TYPE_FIT_XY:\n                default:\n                    // 初始化投影矩阵\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, -1, 1, -1, 1);\n                    break;\n            }\n        } else {\n            switch (type) {\n                case TYPE_CENTER_CROP:\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, -aspectImg / aspectView, aspectImg / aspectView, -1, 1);\n                    break;\n                case TYPE_CENTER_INSIDE:\n                    Matrix.orthoM(projectionMatrix, 0, -aspectView / aspectImg, aspectView / aspectImg, -1, 1, -1, 1);\n                    break;\n                case TYPE_FIT_START:\n                    Matrix.orthoM(projectionMatrix, 0, -1, 2 * aspectView / aspectImg - 1, -1, 1, -1, 1);\n                    break;\n                case TYPE_FIT_END:\n                    Matrix.orthoM(projectionMatrix, 0, 1 - 2 * aspectView / aspectImg, 1, -1, 1, -1, 1);\n                    break;\n                case TYPE_FIT_XY:\n                default:\n                    // 初始化投影矩阵\n                    Matrix.orthoM(projectionMatrix, 0, -1, 1, -1, 1, -1, 1);\n                    break;\n            }\n        }\n        // 合并观察矩阵和视图矩阵 -> 裁剪坐标系\n        Matrix.multiplyMM(matrix, 0, projectionMatrix, 0, viewMatrix, 0);\n    }\n\n    public static float[] flip(float[] m, boolean x, boolean y) {\n        if (x || y) {\n            Matrix.scaleM(m, 0, x ? -1 : 1, y ? -1 : 1, 1);\n        }\n        return m;\n    }\n\n    public static float[] scale(float[] m, float x, float y) {\n        Matrix.scaleM(m, 0, x, y, 1);\n        return m;\n    }\n\n    public static float[] getOriginalMatrix() {\n        return new float[]{\n                1, 0, 0, 0,\n                0, 1, 0, 0,\n                0, 0, 1, 0,\n                0, 0, 0, 1\n        };\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/java/com/sharry/lib/opengles/util/GlUtil.java",
    "content": "package com.sharry.lib.opengles.util;\n\nimport android.content.Context;\nimport android.opengl.GLES11Ext;\nimport android.opengl.GLES20;\n\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\n\npublic class GlUtil {\n\n    /**\n     * 获取 glsl 资源\n     */\n    public static String getGLResource(Context context, int rawId) {\n        InputStream inputStream = context.getResources().openRawResource(rawId);\n        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));\n        StringBuilder builder = new StringBuilder();\n        String line;\n        try {\n            while ((line = reader.readLine()) != null) {\n                builder.append(line).append(\"\\n\");\n            }\n            reader.close();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return builder.toString();\n    }\n\n    /**\n     * 创建顶点 buffer\n     */\n    public static FloatBuffer createFloatBuffer(float[] coords) {\n        FloatBuffer buffer = ByteBuffer.allocateDirect(coords.length * 4)\n                .order(ByteOrder.nativeOrder())\n                .asFloatBuffer();\n        buffer.put(coords, 0, coords.length)\n                .position(0);\n        return buffer;\n    }\n\n    /**\n     * 创建一个 OpenGL 程序\n     *\n     * @param vertexSource   顶点着色器源码\n     * @param fragmentSource 片元着色器源码\n     */\n    public static int createProgram(String vertexSource, String fragmentSource) {\n        // 分别加载创建着色器\n        int vertexShaderId = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);\n        int fragmentShaderId = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);\n        if (vertexShaderId != 0 && fragmentShaderId != 0) {\n            // 创建 OpenGL 程序 ID\n            int programId = GLES20.glCreateProgram();\n            if (programId == 0) {\n                return 0;\n            }\n            // 链接上 顶点着色器\n            GLES20.glAttachShader(programId, vertexShaderId);\n            // 链接上 片段着色器\n            GLES20.glAttachShader(programId, fragmentShaderId);\n            // 链接 OpenGL 程序\n            GLES20.glLinkProgram(programId);\n            // 验证链接结果是否失败\n            int[] status = new int[1];\n            GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 失败后删除这个 OpenGL 程序\n                GLES20.glDeleteProgram(programId);\n                return 0;\n            }\n            return programId;\n        }\n        return 0;\n    }\n\n    /**\n     * 编译着色器\n     *\n     * @param shaderType 着色器的类型\n     * @param source     资源源代码\n     */\n    private static int loadShader(int shaderType, String source) {\n        // 创建着色器 ID\n        int shaderId = GLES20.glCreateShader(shaderType);\n        if (shaderId != 0) {\n            // 1. 将着色器 ID 和着色器程序内容关联\n            GLES20.glShaderSource(shaderId, source);\n            // 2. 编译着色器\n            GLES20.glCompileShader(shaderId);\n            // 3. 验证编译结果\n            int[] status = new int[1];\n            GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 编译失败删除这个着色器 id\n                GLES20.glDeleteShader(shaderId);\n                return 0;\n            }\n        }\n        return shaderId;\n    }\n\n    ////////////////////////////////////////////////////////////////////////////\n    // Texture 创建相关\n    ////////////////////////////////////////////////////////////////////////////\n\n    /**\n     * 创建纹理 type {@link GLES20#GL_TEXTURE_2D}\n     * @return textureId\n     */\n    public static int createTexture2D() {\n        // 生成绑定纹理\n        int[] textures = new int[1];\n        GLES20.glGenTextures(1, textures, 0);\n        int textureId = textures[0];\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);\n        // 设置环绕方向\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);\n        // 设置纹理过滤方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n        // 解绑\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n        return textureId;\n    }\n\n    /**\n     * 创建纹理 type {@link GLES11Ext#GL_TEXTURE_EXTERNAL_OES}\n     * @return textureId\n     */\n    private int createOESTexture() {\n        // 生成绑定纹理\n        int[] textures = new int[1];\n        GLES20.glGenTextures(1, textures, 0);\n        int textureId = textures[0];\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);\n        // 设置环绕方向\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);\n        // 设置纹理过滤方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n        // 解绑\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);\n        return textureId;\n    }\n\n    ////////////////////////////////////////////////////////////////////////////\n    // Framebuffer 相关\n    ////////////////////////////////////////////////////////////////////////////\n\n    /**\n     * 绑定纹理附件类型的 FBO\n     *\n     * @param frameBufferId FBO ID\n     * @param texture2DId   2D 纹理 ID\n     */\n    public static void bindFrameTexture(int frameBufferId, int texture2DId) {\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId);\n        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,\n                GLES20.GL_TEXTURE_2D, texture2DId, 0);\n    }\n\n    /**\n     * 解绑 FBO\n     */\n    public static void unBindFrameBuffer() {\n        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);\n    }\n\n    public static void checkGlError(String operation) {\n        int error = GLES20.glGetError();\n        if (error != GLES20.GL_NO_ERROR) {\n            String msg = operation + \", glError \" + error;\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/utils/com/sharry/lib/opengles/EglCore.java",
    "content": "package com.sharry.lib.opengles;\n\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGL14;\nimport android.opengl.EGLConfig;\nimport android.opengl.EGLContext;\nimport android.opengl.EGLDisplay;\nimport android.opengl.EGLExt;\nimport android.opengl.EGLSurface;\nimport android.util.Log;\nimport android.view.Surface;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;\n\n/**\n * An EGL helper class.\n * <p>\n * The EGLContext must only be attached to one thread at a time.  This class is not thread-safe.\n * <p>\n * Get more details<href>https://github.com/google/grafika/blob/master/app/src/main/java/com/android/grafika/gles/EglCore.java</href>\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-29\n */\npublic class EglCore {\n\n    public static final int EGL_VERSION_2 = 2;\n    public static final int EGL_VERSION_3 = 3;\n\n    @IntDef(value = {\n            EGL_VERSION_2,\n            EGL_VERSION_3\n    })\n    private @interface EGLVersion {\n\n    }\n\n    private static final String TAG = EglCore.class.getSimpleName();\n\n    private final int mEGLVersion;\n    private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;\n    private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;\n    private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;\n\n    public EglCore() {\n        this(EGL_VERSION_2);\n    }\n\n    public EglCore(@EGLVersion int eglVersion) {\n        mEGLVersion = eglVersion;\n    }\n\n    /**\n     * Initialize EGL for a given configuration spec.\n     *\n     * @param surface    native window\n     * @param eglContext if null will create new context, false will use shared context\n     */\n    public void initialize(@NonNull Surface surface, @Nullable EGLContext eglContext) {\n        initializeInternal(surface, eglContext == null ? EGL14.EGL_NO_CONTEXT : eglContext);\n    }\n\n    /**\n     * Initialize EGL for a given configuration spec.\n     *\n     * @param surfaceTexture native window\n     * @param eglContext     if null will create new context, false will use shared context\n     */\n    public void initialize(@NonNull SurfaceTexture surfaceTexture, @Nullable EGLContext eglContext) {\n        initializeInternal(surfaceTexture, eglContext == null ? EGL14.EGL_NO_CONTEXT : eglContext);\n    }\n\n    /**\n     * Makes our EGL context current, using the supplied \"draw\" and \"read\" surfaces.\n     */\n    public void makeCurrent() {\n        if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {\n            throw new RuntimeException(\"eglMakeCurrent failed\");\n        }\n    }\n\n    /**\n     * Calls eglSwapBuffers.  Use this to \"publish\" the current frame.\n     *\n     * @return false on failure\n     */\n    public boolean swapBuffers() {\n        return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);\n    }\n\n    /**\n     * Discards all resources held by this class, notably the EGL context.  This must be\n     * called from the thread where the context was created.\n     * <p>\n     * On completion, no context will be current.\n     */\n    public void release() {\n        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {\n            EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,\n                    EGL14.EGL_NO_CONTEXT);\n            EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);\n            EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);\n            EGL14.eglTerminate(mEGLDisplay);\n        }\n        mEGLContext = EGL14.EGL_NO_CONTEXT;\n        mEGLDisplay = EGL14.EGL_NO_DISPLAY;\n        mEGLSurface = EGL14.EGL_NO_SURFACE;\n    }\n\n    /**\n     * Gets the current EGLContext\n     *\n     * @return the current EGLContext\n     */\n    public EGLContext getContext() {\n        return mEGLContext;\n    }\n\n    /**\n     * Copy from {@link android.opengl.GLSurfaceView#EglHelper}\n     */\n    private void initializeInternal(Object nativeWindow, EGLContext sharedEglContext) {\n        /*\n         * Create a connection for system native window\n         */\n        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);\n        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {\n            throw new RuntimeException(\"eglGetDisplay failed\");\n        }\n\n        /*\n         * We can now initialize EGL for that display\n         */\n        int[] version = new int[2];\n        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {\n            mEGLDisplay = null;\n            throw new RuntimeException(\"eglInitialize failed\");\n        }\n\n        /*\n         * Create EGLConfig\n         */\n        EGLConfig eglConfig = chooseConfig();\n        if (eglConfig == null) {\n            throw new RuntimeException(\"Cannot find suitable config.\");\n        }\n\n        /*\n         * Create EGLContext\n         */\n        int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLVersion, EGL14.EGL_NONE};\n        EGLContext eglContext = EGL14.eglCreateContext(mEGLDisplay, eglConfig, sharedEglContext,\n                attrib_list, 0);\n        if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {\n            mEGLContext = eglContext;\n        } else {\n            throw new RuntimeException(\"Create EGLContext failed.\");\n        }\n\n        /*\n         * Create EGLSurface\n         */\n        int[] surfaceAttribs = {EGL14.EGL_NONE};\n        mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, eglConfig, nativeWindow,\n                surfaceAttribs, 0);\n        if (mEGLSurface == null || mEGLSurface == EGL14.EGL_NO_SURFACE) {\n            throw new RuntimeException(\"createWindowSurface returned EGL_BAD_NATIVE_WINDOW.\");\n        }\n\n        /*\n         * Bind context\n         */\n        makeCurrent();\n    }\n\n    /**\n     * Finds a suitable EGLConfig.\n     */\n    private EGLConfig chooseConfig() {\n        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;\n        if (mEGLVersion >= 3) {\n            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;\n        }\n        // The actual surface is generally RGBA or RGBX, so situationally omitting alpha\n        // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()\n        // when reading into a GL_RGBA buffer.\n        int[] attribList = {\n                EGL14.EGL_RED_SIZE, 8,\n                EGL14.EGL_GREEN_SIZE, 8,\n                EGL14.EGL_BLUE_SIZE, 8,\n                EGL14.EGL_ALPHA_SIZE, 8,\n                //EGL14.EGL_DEPTH_SIZE, 16,\n                //EGL14.EGL_STENCIL_SIZE, 8,\n                EGL14.EGL_RENDERABLE_TYPE, renderableType,\n                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]\n                EGL14.EGL_NONE\n        };\n        EGLConfig[] configs = new EGLConfig[1];\n        int[] numConfigs = new int[1];\n        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs,\n                0, configs.length,\n                numConfigs, 0)) {\n            Log.w(TAG, \"unable to find RGB8888 / \" + mEGLVersion + \" EGLConfig\");\n            return null;\n        }\n        return configs[0];\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        try {\n            if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {\n                // We're limited here -- finalizers don't run on the thread that holds\n                // the EGL state, so if a surface or context is still current on another\n                // thread we can't fully release it here.  Exceptions thrown from here\n                // are quietly discarded.  Complain in the log file.\n                Log.w(TAG, \"WARNING: EglCore was not explicitly released -- state may be leaked\");\n                release();\n            }\n        } finally {\n            super.finalize();\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-opengles/src/main/utils/com/sharry/lib/opengles/GlUtil.java",
    "content": "package com.sharry.lib.opengles;\n\nimport android.content.Context;\nimport android.opengl.GLES20;\n\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\n\npublic class GlUtil {\n\n    /**\n     * 获取 glsl 资源\n     */\n    public static String getGLResource(Context context, int rawId) {\n        InputStream inputStream = context.getResources().openRawResource(rawId);\n        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));\n        StringBuilder builder = new StringBuilder();\n        String line;\n        try {\n            while ((line = reader.readLine()) != null) {\n                builder.append(line).append(\"\\n\");\n            }\n            reader.close();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return builder.toString();\n    }\n\n    /**\n     * 创建顶点 buffer\n     */\n    public static FloatBuffer createFloatBuffer(float[] coords) {\n        FloatBuffer buffer = ByteBuffer.allocateDirect(coords.length * 4)\n                .order(ByteOrder.nativeOrder())\n                .asFloatBuffer();\n        buffer.put(coords, 0, coords.length)\n                .position(0);\n        return buffer;\n    }\n\n    /**\n     * 创建一个 OpenGL 程序\n     *\n     * @param vertexSource   顶点着色器源码\n     * @param fragmentSource 片元着色器源码\n     */\n    public  static int createProgram(String vertexSource, String fragmentSource) {\n        // 分别加载创建着色器\n        int vertexShaderId = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);\n        int fragmentShaderId = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);\n        if (vertexShaderId != 0 && fragmentShaderId != 0) {\n            // 创建 OpenGL 程序 ID\n            int programId = GLES20.glCreateProgram();\n            if (programId == 0) {\n                return 0;\n            }\n            // 链接上 顶点着色器\n            GLES20.glAttachShader(programId, vertexShaderId);\n            // 链接上 片段着色器\n            GLES20.glAttachShader(programId, fragmentShaderId);\n            // 链接 OpenGL 程序\n            GLES20.glLinkProgram(programId);\n            // 验证链接结果是否失败\n            int[] status = new int[1];\n            GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 失败后删除这个 OpenGL 程序\n                GLES20.glDeleteProgram(programId);\n                return 0;\n            }\n            return programId;\n        }\n        return 0;\n    }\n\n    /**\n     * 编译着色器\n     *\n     * @param shaderType 着色器的类型\n     * @param source     资源源代码\n     */\n    private static int loadShader(int shaderType, String source) {\n        // 创建着色器 ID\n        int shaderId = GLES20.glCreateShader(shaderType);\n        if (shaderId != 0) {\n            // 1. 将着色器 ID 和着色器程序内容关联\n            GLES20.glShaderSource(shaderId, source);\n            // 2. 编译着色器\n            GLES20.glCompileShader(shaderId);\n            // 3. 验证编译结果\n            int[] status = new int[1];\n            GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, status, 0);\n            if (status[0] != GLES20.GL_TRUE) {\n                // 编译失败删除这个着色器 id\n                GLES20.glDeleteShader(shaderId);\n                return 0;\n            }\n        }\n        return shaderId;\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/.gitignore",
    "content": "# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# Intellij\n*.iml\n.idea\n\n\n# Keystore files\n*.jks\n\n# external files\n.externalNativeBuild/"
  },
  {
    "path": "lib-scamera/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\n\ngroup = 'com.github.SharryChoo'\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n    }\n    sourceSets {\n        main {\n            java.srcDirs += 'src/main/api'\n            java.srcDirs += 'src/main/device'\n            java.srcDirs += 'src/main/previewer'\n            java.srcDirs += 'src/main/orientation'\n            java.srcDirs += 'src/main/common'\n            java.srcDirs += 'src/main/utils'\n            jniLibs.srcDirs = ['src/main/jniLibs']\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation \"androidx.appcompat:appcompat:$supportLibraryVersion\"\n    api project(':lib-opengles')\n}\n"
  },
  {
    "path": "lib-scamera/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "lib-scamera/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.sharry.lib.camera\">\n\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n\n</manifest>\n"
  },
  {
    "path": "lib-scamera/src/main/api/com/sharry/lib/camera/SCameraView.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.SurfaceTexture;\nimport android.util.AttributeSet;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * The facade handle device interaction with view.\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-05\n */\npublic class SCameraView extends FrameLayout implements\n        ScreenOrientationDetector.OnDisplayChangedListener, ICameraDevice.OnCameraReadyListener {\n\n    /**\n     * The camera device faces the opposite direction as the device's screen.\n     */\n    public static final int FACING_BACK = Constants.FACING_BACK;\n\n    /**\n     * The camera device faces the same direction as the device's screen.\n     */\n    public static final int FACING_FRONT = Constants.FACING_FRONT;\n\n    /**\n     * Direction the camera faces relative to device screen.\n     */\n    @IntDef({FACING_BACK, FACING_FRONT})\n    @Retention(RetentionPolicy.SOURCE)\n    @interface Facing {\n    }\n\n    /**\n     * Flash will not be fired.\n     */\n    public static final int FLASH_OFF = Constants.FLASH_OFF;\n\n    /**\n     * Flash will always be fired during snapshot.\n     */\n    public static final int FLASH_ON = Constants.FLASH_ON;\n\n    /**\n     * Constant emission of light during preview, auto-focus and snapshot.\n     */\n    public static final int FLASH_TORCH = Constants.FLASH_TORCH;\n\n    /**\n     * Flash will be fired automatically when required.\n     */\n    public static final int FLASH_AUTO = Constants.FLASH_AUTO;\n\n    /**\n     * Flash will be fired in red-eye reduction mode.\n     */\n    public static final int FLASH_RED_EYE = Constants.FLASH_RED_EYE;\n\n    /**\n     * The mode for for the camera device's flash control\n     */\n    @IntDef({FLASH_OFF, FLASH_ON, FLASH_TORCH, FLASH_AUTO, FLASH_RED_EYE})\n    @interface Flash {\n    }\n\n    /**\n     * Control camera device\n     */\n    private final ICameraDevice mDevice;\n\n    /**\n     * The context holder data\n     */\n    private final CameraContext mContext;\n\n    /**\n     * Control preview.\n     */\n    private final IPreviewer mPreviewer;\n\n    /**\n     * Control display rotate\n     */\n    private final ScreenOrientationDetector mScreenOrientationDetector;\n\n    public SCameraView(@NonNull Context context) {\n        this(context, null);\n    }\n\n    public SCameraView(@NonNull Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        this.mContext = new CameraContext(context);\n        this.mPreviewer = new Previewer(context, this);\n        this.mScreenOrientationDetector = new ScreenOrientationDetector(context, this);\n        this.mDevice = new Camera1Device(mContext, this);\n        // Attributes\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SCameraView, defStyleAttr,\n                R.style.Widget_CameraView);\n        // set adjust view bounds\n        setAdjustViewBounds(a.getBoolean(R.styleable.SCameraView_android_adjustViewBounds, false));\n        // set facing\n        setFacing(a.getInt(R.styleable.SCameraView_facing, FACING_BACK));\n        // set aspect ratio\n        String aspectRatio = a.getString(R.styleable.SCameraView_aspectRatio);\n        setAspectRatio(aspectRatio != null ? AspectRatio.parse(aspectRatio) : AspectRatio.DEFAULT);\n        // set auto focus\n        setAutoFocus(a.getBoolean(R.styleable.SCameraView_autoFocus, true));\n        // set flash mode\n        setFlash(a.getInt(R.styleable.SCameraView_flash, Constants.FLASH_AUTO));\n        a.recycle();\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        if (!isInEditMode()) {\n            mScreenOrientationDetector.enable(getDisplay());\n        }\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        if (!isInEditMode()) {\n            mScreenOrientationDetector.disable();\n        }\n        super.onDetachedFromWindow();\n    }\n\n    @Override\n    public void onDisplayOrientationChanged(int displayOrientation) {\n        mContext.setScreenOrientationDegrees(displayOrientation);\n        mDevice.notifyScreenOrientationChanged();\n    }\n\n    @Override\n    public void onCameraReady(@NonNull SurfaceTexture dataSource, @NonNull Size size, int rotation) {\n        mPreviewer.setDataSource(dataSource);\n        mPreviewer.setRotate(rotation);\n        mPreviewer.setScaleType(ScaleType.CENTER_CROP, mScreenOrientationDetector.isLandscape(), size);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        if (isInEditMode()) {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n            return;\n        }\n        if (!mContext.isAdjustViewBounds()) {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n            return;\n        }\n        // Handle android:adjustViewBounds\n        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n        // 宽为精确测量\n        if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {\n            // 根据比例计算高\n            final AspectRatio ratio = getAspectRatio();\n            int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());\n            if (heightMode == MeasureSpec.AT_MOST) {\n                height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));\n            }\n            super.onMeasure(widthMeasureSpec,\n                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));\n        }\n        // 高为精确测量\n        else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {\n            // 根据比例计算宽\n            final AspectRatio ratio = getAspectRatio();\n            int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());\n            if (widthMode == MeasureSpec.AT_MOST) {\n                width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));\n            }\n            super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),\n                    heightMeasureSpec);\n        } else {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        }\n        // Measure the PreviewView\n        int width = getMeasuredWidth();\n        int height = getMeasuredHeight();\n        // 若为横屏, 则颠倒一下比例, 方便计算\n        AspectRatio ratio = getAspectRatio();\n        if (!mScreenOrientationDetector.isLandscape()) {\n            ratio = ratio.inverse();\n        }\n        if (height < width * ratio.getY() / ratio.getX()) {\n            mPreviewer.getView().measure(\n                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),\n                    MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),\n                            MeasureSpec.EXACTLY));\n        } else {\n            mPreviewer.getView().measure(\n                    MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),\n                            MeasureSpec.EXACTLY),\n                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));\n        }\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n        mContext.setDesiredSize(mPreviewer.getSize());\n        mDevice.notifyDesiredSizeChanged();\n    }\n\n    /**\n     * Open a camera device and start showing camera preview. This is typically called from\n     * {@link Activity#onResume}.\n     */\n    public void startPreview() {\n        post(new Runnable() {\n            @Override\n            public void run() {\n                mDevice.open();\n            }\n        });\n    }\n\n    /**\n     * Stop camera preview and close the device. This is typically called from\n     * {@link Activity#onPause}\n     */\n    public void stopPreview() {\n        mDevice.close();\n    }\n\n    /**\n     * 获取照片\n     */\n    @Nullable\n    public Bitmap takePicture() {\n        stopPreview();\n        return mPreviewer.getBitmap();\n    }\n\n    /**\n     * Chooses camera by the direction it faces.\n     *\n     * @param facing The camera facing. Must be either {@link #FACING_BACK} or\n     *               {@link #FACING_FRONT}.\n     */\n    public void setFacing(@Facing int facing) {\n        mContext.setFacing(facing);\n        mDevice.notifyFacingChanged();\n    }\n\n    /**\n     * Sets the aspect ratio of camera.\n     *\n     * @param ratio The {@link AspectRatio} to be set.\n     */\n    public void setAspectRatio(@NonNull AspectRatio ratio) {\n        if (mContext.getAspectRatio().equals(ratio)) {\n            return;\n        }\n        mContext.setAspectRatio(ratio);\n        mDevice.notifyAspectRatioChanged();\n        requestLayout();\n    }\n\n    /**\n     * Enables or disables the continuous auto-focus mode. When the current camera doesn't support\n     * auto-focus, calling this method will be ignored.\n     *\n     * @param autoFocus {@code true} to enable continuous auto-focus mode. {@code false} to\n     *                  disable it.\n     */\n    public void setAutoFocus(boolean autoFocus) {\n        mContext.setAutoFocus(autoFocus);\n        mDevice.notifyAutoFocusChanged();\n    }\n\n    /**\n     * Sets the flash mode.\n     *\n     * @param flash The desired flash mode.\n     */\n    public void setFlash(@Flash int flash) {\n        mContext.setFlashMode(flash);\n        mDevice.notifyFlashModeChanged();\n    }\n\n    /**\n     * @param adjustViewBounds {@code true} if you want the CameraView to adjust its bounds to\n     *                         preserve the aspect ratio of camera.\n     */\n    public void setAdjustViewBounds(boolean adjustViewBounds) {\n        if (mContext.isAdjustViewBounds() != adjustViewBounds) {\n            mContext.setAdjustViewBounds(adjustViewBounds);\n            requestLayout();\n        }\n    }\n\n    /**\n     * Gets the direction that the current camera faces.\n     *\n     * @return The camera facing.\n     */\n    public int getFacing() {\n        return mContext.getFacing();\n    }\n\n    /**\n     * Gets the current aspect ratio of camera.\n     *\n     * @return The current {@link AspectRatio}. Default is 4:3.\n     */\n    @NonNull\n    public AspectRatio getAspectRatio() {\n        return mContext.getAspectRatio();\n    }\n\n    /**\n     * Returns whether the continuous auto-focus mode is enabled.\n     *\n     * @return {@code true} if the continuous auto-focus mode is enabled. {@code false} if it is\n     * disabled, or if it is not supported by the current camera.\n     */\n    public boolean getAutoFocus() {\n        return mContext.isAutoFocus();\n    }\n\n    /**\n     * Gets the current flash mode.\n     *\n     * @return The current flash mode.\n     */\n    public int getFlash() {\n        //noinspection WrongConstant\n        return mContext.getFlashMode();\n    }\n\n    /**\n     * Returns whether the adjustViewBounds is enabled.\n     *\n     * @return {@code true} if the adjustViewBounds is enabled. {@code false} if it is disabled\n     */\n    public boolean getAdjustViewBounds() {\n        return mContext.getAdjustViewBounds();\n    }\n\n    /**\n     * Gets the previewer.\n     *\n     * @return The render view.\n     */\n    public IPreviewer getPreviewer() {\n        return mPreviewer;\n    }\n\n    /**\n     * Returns whether the display orientation is landscape.\n     *\n     * @return {@code true} if the adjustViewBounds is landscape.\n     */\n    public boolean isLandscape() {\n        return mScreenOrientationDetector.isLandscape();\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/common/com/sharry/lib/camera/AspectRatio.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.sharry.lib.camera;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.NonNull;\nimport androidx.collection.SparseArrayCompat;\n\n/**\n * Immutable class for describing proportional relationship between width and height.\n */\npublic class AspectRatio implements Comparable<AspectRatio>, Parcelable {\n\n    public static final AspectRatio DEFAULT = new AspectRatio(4, 3);\n    private final static SparseArrayCompat<SparseArrayCompat<AspectRatio>> sCache\n            = new SparseArrayCompat<>(16);\n\n    private final int mX;\n    private final int mY;\n\n    /**\n     * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values.\n     * The values {@code x} and {@code} will be reduced by their greatest common divider.\n     *\n     * @param x The width\n     * @param y The height\n     * @return An instance of {@link AspectRatio}\n     */\n    public static AspectRatio of(int x, int y) {\n        int gcd = gcd(x, y);\n        x /= gcd;\n        y /= gcd;\n        SparseArrayCompat<AspectRatio> arrayX = sCache.get(x);\n        if (arrayX == null) {\n            AspectRatio ratio = new AspectRatio(x, y);\n            arrayX = new SparseArrayCompat<>();\n            arrayX.put(y, ratio);\n            sCache.put(x, arrayX);\n            return ratio;\n        } else {\n            AspectRatio ratio = arrayX.get(y);\n            if (ratio == null) {\n                ratio = new AspectRatio(x, y);\n                arrayX.put(y, ratio);\n            }\n            return ratio;\n        }\n    }\n\n    /**\n     * Parse an {@link AspectRatio} from a {@link String} formatted like \"4:3\".\n     *\n     * @param s The string representation of the aspect ratio\n     * @return The aspect ratio\n     * @throws IllegalArgumentException when the format is incorrect.\n     */\n    public static AspectRatio parse(String s) {\n        int position = s.indexOf(':');\n        if (position == -1) {\n            throw new IllegalArgumentException(\"Malformed aspect ratio: \" + s);\n        }\n        try {\n            int x = Integer.parseInt(s.substring(0, position));\n            int y = Integer.parseInt(s.substring(position + 1));\n            return AspectRatio.of(x, y);\n        } catch (NumberFormatException e) {\n            throw new IllegalArgumentException(\"Malformed aspect ratio: \" + s, e);\n        }\n    }\n\n    private AspectRatio(int x, int y) {\n        mX = x;\n        mY = y;\n    }\n\n    public int getX() {\n        return mX;\n    }\n\n    public int getY() {\n        return mY;\n    }\n\n    public boolean matches(Size size) {\n        int gcd = gcd(size.getWidth(), size.getHeight());\n        int x = size.getWidth() / gcd;\n        int y = size.getHeight() / gcd;\n        return mX == x && mY == y;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null) {\n            return false;\n        }\n        if (this == o) {\n            return true;\n        }\n        if (o instanceof AspectRatio) {\n            AspectRatio ratio = (AspectRatio) o;\n            return mX == ratio.mX && mY == ratio.mY;\n        }\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        return mX + \":\" + mY;\n    }\n\n    public float toFloat() {\n        return (float) mX / mY;\n    }\n\n    @Override\n    public int hashCode() {\n        // assuming most sizes are <2^16, doing a rotate will give us perfect hashing\n        return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2)));\n    }\n\n    @Override\n    public int compareTo(@NonNull AspectRatio another) {\n        if (equals(another)) {\n            return 0;\n        } else if (toFloat() - another.toFloat() > 0) {\n            return 1;\n        }\n        return -1;\n    }\n\n    /**\n     * @return The inverse of this {@link AspectRatio}.\n     */\n    public AspectRatio inverse() {\n        //noinspection SuspiciousNameCombination\n        return AspectRatio.of(mY, mX);\n    }\n\n    /**\n     * 计算 a 与 b 的最大公约数\n     * <p>\n     * 辗转相除法：两个整数的最大公约数等于其中较小的那个数和两个数相除余数的最大公约数。\n     */\n    private static int gcd(int a, int b) {\n        while (b != 0) {\n            int c = b;\n            b = a % b;\n            a = c;\n        }\n        return a;\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeInt(mX);\n        dest.writeInt(mY);\n    }\n\n    public static final Creator<AspectRatio> CREATOR\n            = new Creator<AspectRatio>() {\n\n        @Override\n        public AspectRatio createFromParcel(Parcel source) {\n            int x = source.readInt();\n            int y = source.readInt();\n            return AspectRatio.of(x, y);\n        }\n\n        @Override\n        public AspectRatio[] newArray(int size) {\n            return new AspectRatio[size];\n        }\n    };\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/common/com/sharry/lib/camera/CameraContext.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.content.Context;\nimport android.content.ContextWrapper;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-07\n */\nclass CameraContext extends ContextWrapper {\n\n    private AspectRatio aspectRatio = AspectRatio.DEFAULT;\n    private int facing;\n    private boolean autoFocus;\n    private int flashMode;\n    private int screenOrientationDegrees;\n    boolean adjustViewBounds;\n    Size desiredSize;\n\n    CameraContext(Context base) {\n        super(base);\n    }\n\n    AspectRatio getAspectRatio() {\n        return aspectRatio;\n    }\n\n    void setAspectRatio(AspectRatio aspectRatio) {\n        this.aspectRatio = aspectRatio;\n    }\n\n    int getFacing() {\n        return facing;\n    }\n\n    void setFacing(int facing) {\n        this.facing = facing;\n    }\n\n    boolean isAutoFocus() {\n        return autoFocus;\n    }\n\n    void setAutoFocus(boolean autoFocus) {\n        this.autoFocus = autoFocus;\n    }\n\n    int getFlashMode() {\n        return flashMode;\n    }\n\n    void setFlashMode(int flashMode) {\n        this.flashMode = flashMode;\n    }\n\n    int getScreenOrientationDegrees() {\n        return screenOrientationDegrees;\n    }\n\n    void setScreenOrientationDegrees(int screenOrientationDegrees) {\n        this.screenOrientationDegrees = screenOrientationDegrees;\n    }\n\n    boolean isAdjustViewBounds() {\n        return adjustViewBounds;\n    }\n\n    public void setAdjustViewBounds(boolean adjustViewBounds) {\n        this.adjustViewBounds = adjustViewBounds;\n    }\n\n    public boolean getAdjustViewBounds() {\n        return adjustViewBounds;\n    }\n\n    public void setDesiredSize(Size desiredSize) {\n        this.desiredSize = desiredSize;\n    }\n\n    public Size getDesiredSize() {\n        return desiredSize;\n    }\n}\n"
  },
  {
    "path": "lib-scamera/src/main/common/com/sharry/lib/camera/Constants.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.sharry.lib.camera;\n\n\nimport android.hardware.Camera;\n\ninterface Constants {\n\n    /**\n     * Constants of facing\n     */\n    int FACING_BACK = Camera.CameraInfo.CAMERA_FACING_BACK;\n    int FACING_FRONT = Camera.CameraInfo.CAMERA_FACING_FRONT;\n\n    /**\n     * Constants of flash\n     */\n    int FLASH_OFF = 0;\n    int FLASH_ON = 1;\n    int FLASH_TORCH = 2;\n    int FLASH_AUTO = 3;\n    int FLASH_RED_EYE = 4;\n\n    /**\n     * Constants of orientation\n     */\n    int LANDSCAPE_90 = 90;\n    int LANDSCAPE_270 = 270;\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/common/com/sharry/lib/camera/Size.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.sharry.lib.camera;\n\nimport androidx.annotation.NonNull;\n\n/**\n * Immutable class for describing width and height dimensions in pixels.\n */\npublic class Size implements Comparable<Size> {\n\n    private final int mWidth;\n    private final int mHeight;\n\n    /**\n     * Create a new immutable Size instance.\n     *\n     * @param width  The width of the size, in pixels\n     * @param height The height of the size, in pixels\n     */\n    public Size(int width, int height) {\n        mWidth = width;\n        mHeight = height;\n    }\n\n    public int getWidth() {\n        return mWidth;\n    }\n\n    public int getHeight() {\n        return mHeight;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null) {\n            return false;\n        }\n        if (this == o) {\n            return true;\n        }\n        if (o instanceof Size) {\n            Size size = (Size) o;\n            return mWidth == size.mWidth && mHeight == size.mHeight;\n        }\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        return mWidth + \"x\" + mHeight;\n    }\n\n    @Override\n    public int hashCode() {\n        // assuming most sizes are <2^16, doing a rotate will give us perfect hashing\n        return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));\n    }\n\n    @Override\n    public int compareTo(@NonNull Size another) {\n        return mWidth * mHeight - another.mWidth * another.mHeight;\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/common/com/sharry/lib/camera/SizeMap.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.sharry.lib.camera;\n\nimport android.util.ArrayMap;\n\nimport java.util.Set;\nimport java.util.SortedSet;\nimport java.util.TreeSet;\n\n/**\n * A collection class that automatically groups {@link Size}s by their {@link AspectRatio}s.\n */\nclass SizeMap {\n\n    private final ArrayMap<AspectRatio, SortedSet<Size>> mRatios = new ArrayMap<>();\n\n    /**\n     * Add a new {@link Size} to this collection.\n     *\n     * @param size The size to add.\n     * @return {@code true} if it is added, {@code false} if it already exists and is not added.\n     */\n    public boolean add(Size size) {\n        for (AspectRatio ratio : mRatios.keySet()) {\n            if (ratio.matches(size)) {\n                final SortedSet<Size> sizes = mRatios.get(ratio);\n                if (sizes.contains(size)) {\n                    return false;\n                } else {\n                    sizes.add(size);\n                    return true;\n                }\n            }\n        }\n        // None of the existing ratio matches the provided size; add a new key\n        SortedSet<Size> sizes = new TreeSet<>();\n        sizes.add(size);\n        mRatios.put(AspectRatio.of(size.getWidth(), size.getHeight()), sizes);\n        return true;\n    }\n\n    /**\n     * Removes the specified aspect ratio and all sizes associated with it.\n     *\n     * @param ratio The aspect ratio to be removed.\n     */\n    public void remove(AspectRatio ratio) {\n        mRatios.remove(ratio);\n    }\n\n    Set<AspectRatio> ratios() {\n        return mRatios.keySet();\n    }\n\n    SortedSet<Size> sizes(AspectRatio ratio) {\n        return mRatios.get(ratio);\n    }\n\n    void clear() {\n        mRatios.clear();\n    }\n\n    boolean isEmpty() {\n        return mRatios.isEmpty();\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/device/com/sharry/lib/camera/AbsCameraDevice.java",
    "content": "package com.sharry.lib.camera;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-05\n */\nabstract class AbsCameraDevice implements ICameraDevice {\n\n    final CameraContext context;\n    OnCameraReadyListener listener;\n    AspectRatio aspectRatio = AspectRatio.DEFAULT;\n    int facing;\n    boolean autoFocus;\n    int flashMode;\n    int screenOrientationDegrees;\n    int previewWidth, previewHeight;\n\n    AbsCameraDevice(CameraContext context, OnCameraReadyListener listener) {\n        this.context = context;\n        this.listener = listener;\n    }\n\n    @Override\n    public void notifyFacingChanged() {\n        if (this.facing == context.getFacing()) {\n            return;\n        }\n        this.facing = context.getFacing();\n        if (isCameraOpened()) {\n            open();\n        }\n    }\n\n    @Override\n    public void notifyAspectRatioChanged() {\n        // Handle this later when camera is opened\n        if (!isCameraOpened()) {\n            aspectRatio = context.getAspectRatio();\n        }\n        // if camera opened\n        if (!aspectRatio.equals(context.getAspectRatio())) {\n            aspectRatio = context.getAspectRatio();\n            open();\n        }\n    }\n\n    @Override\n    public void notifyScreenOrientationChanged() {\n        if (this.screenOrientationDegrees == context.getScreenOrientationDegrees()) {\n            return;\n        }\n        this.screenOrientationDegrees = context.getScreenOrientationDegrees();\n        if (isCameraOpened()) {\n            open();\n        }\n    }\n\n    @Override\n    public void notifyDesiredSizeChanged() {\n        if (previewWidth == context.getDesiredSize().getWidth()\n                && previewHeight == context.getDesiredSize().getHeight()) {\n            return;\n        }\n        previewWidth = context.getDesiredSize().getWidth();\n        previewHeight = context.getDesiredSize().getHeight();\n        if (isCameraOpened()) {\n            open();\n        }\n    }\n\n    /**\n     * Test if the supplied orientation is in landscape.\n     *\n     * @return True if in landscape, false if portrait\n     */\n    protected boolean isLandscape() {\n        return (screenOrientationDegrees == Constants.LANDSCAPE_90\n                || screenOrientationDegrees == Constants.LANDSCAPE_270);\n    }\n\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/device/com/sharry/lib/camera/Camera1Device.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.graphics.Bitmap;\nimport android.graphics.SurfaceTexture;\nimport android.hardware.Camera;\nimport android.util.Log;\n\nimport androidx.collection.SparseArrayCompat;\n\nimport java.util.List;\nimport java.util.SortedSet;\n\n/**\n * Camera1 实现的相机引擎\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-04-24\n */\nclass Camera1Device extends AbsCameraDevice {\n\n    private static final String TAG = Camera1Device.class.getSimpleName();\n    private static final SparseArrayCompat<String> FLASH_MODES = new SparseArrayCompat<>();\n\n    static {\n        FLASH_MODES.put(Constants.FLASH_OFF, Camera.Parameters.FLASH_MODE_OFF);\n        FLASH_MODES.put(Constants.FLASH_ON, Camera.Parameters.FLASH_MODE_ON);\n        FLASH_MODES.put(Constants.FLASH_TORCH, Camera.Parameters.FLASH_MODE_TORCH);\n        FLASH_MODES.put(Constants.FLASH_AUTO, Camera.Parameters.FLASH_MODE_AUTO);\n        FLASH_MODES.put(Constants.FLASH_RED_EYE, Camera.Parameters.FLASH_MODE_RED_EYE);\n    }\n\n    private static final int MAGIC_TEXTURE_ID = 0;\n    private static final int INVALID_CAMERA_ID = -1;\n\n    private final Camera.CameraInfo mCameraInfo = new Camera.CameraInfo();\n    private final SizeMap mPreviewSizes = new SizeMap();\n    private final SizeMap mPictureSizes = new SizeMap();\n    private final SurfaceTexture mBufferTexture;\n\n    private Camera mImpl;\n    private Camera.Parameters mCameraParams;\n\n    Camera1Device(CameraContext context, OnCameraReadyListener listener) {\n        super(context, listener);\n        mBufferTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);\n    }\n\n    @Override\n    public boolean isCameraOpened() {\n        return mImpl != null;\n    }\n\n    @Override\n    public void open() {\n        // Stop preview first.\n        close();\n        // 根据 Options 初始化相机\n        startPreviewInternal();\n    }\n\n    @Override\n    public void close() {\n        if (null != mImpl) {\n            try {\n                // 停止预览\n                mImpl.stopPreview();\n                /*\n                 移除回调, 否则会扔出: Camera is being used after Camera.release() was called\n                */\n                mImpl.setPreviewCallback(null);\n                mImpl.release();\n                mImpl = null;\n                Log.i(TAG, \"Camera release success.\");\n            } catch (Throwable e) {\n                // ignore.\n            }\n        }\n    }\n\n    @Override\n    public Bitmap takePicture() {\n        close();\n        return null;\n    }\n\n    @Override\n    public void notifyAutoFocusChanged() {\n        // is previewing\n        if (isCameraOpened()) {\n            if (this.autoFocus == context.isAutoFocus()) {\n                return;\n            }\n            this.autoFocus = context.isAutoFocus();\n            // resetMatrix params\n            setAutoFocusInternal(autoFocus);\n            mImpl.setParameters(mCameraParams);\n        }\n        // not previewing\n        else {\n            this.autoFocus = context.isAutoFocus();\n        }\n    }\n\n    @Override\n    public void notifyFlashModeChanged() {\n        // is previewing\n        if (isCameraOpened()) {\n            if (flashMode == context.getFlashMode()) {\n                return;\n            }\n            // resetMatrix params\n            if (setFlashInternal(context.getFlashMode())) {\n                mImpl.setParameters(mCameraParams);\n            }\n        }\n        // not previewing\n        else {\n            flashMode = context.getFlashMode();\n        }\n    }\n\n    /**\n     * 开启预览真正的逻辑实现\n     */\n    private void startPreviewInternal() {\n        try {\n            // 1. 打开相机\n            int cameraId = chooseCamera(facing);\n            mImpl = Camera.open(cameraId);\n            // 2. 设置相机参数\n            mCameraParams = mImpl.getParameters();\n            /*\n             3. 设置预览尺寸\n             */\n            // 采集所有的预览尺寸\n            mPreviewSizes.clear();\n            for (Camera.Size size : mCameraParams.getSupportedPreviewSizes()) {\n                mPreviewSizes.add(new Size(size.width, size.height));\n            }\n            // 获取用户期望的比例的集合\n            SortedSet<Size> previewSizes = mPreviewSizes.sizes(aspectRatio);\n            if (previewSizes == null) {\n                // 用户期望的比例不存在, 获取默认比例\n                previewSizes = mPreviewSizes.sizes(chooseDefaultAspectRatio());\n            }\n            final Size previewSize = chooseOptimalPreviewSize(previewSizes);\n            mCameraParams.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());\n\n            /*\n             4. 设置拍照尺寸\n             */\n            // 采集所有照片的尺寸\n            mPictureSizes.clear();\n            for (Camera.Size size : mCameraParams.getSupportedPictureSizes()) {\n                mPictureSizes.add(new Size(size.width, size.height));\n            }\n            // 获取用户期望的比例集合\n            SortedSet<Size> pictureSizes = mPictureSizes.sizes(aspectRatio);\n            if (pictureSizes == null) {\n                // 用户期望的尺寸不存在, 获取默认比例\n                pictureSizes = mPreviewSizes.sizes(chooseDefaultAspectRatio());\n            }\n            // 选择期望集合中, 尺寸最大的一个, 保证拍照后输出图像的清晰度\n            Size pictureSize = pictureSizes.last();\n            mCameraParams.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());\n            // 5. 设置拍摄后的图像输出的方向\n            mCameraParams.setRotation(calcTakenPictureRotation(screenOrientationDegrees));\n            // 6. 处理自动对焦\n            setAutoFocusInternal(autoFocus);\n            // 7. 处理闪光灯变化\n            setFlashInternal(flashMode);\n            mImpl.setParameters(mCameraParams);\n            // 8. 设置预览帧的图像的输出方向\n            mImpl.setDisplayOrientation(calcPreviewFrameOrientation(screenOrientationDegrees));\n            // 9. 设置图像输出的画布\n            mImpl.setPreviewTexture(mBufferTexture);\n            // 10. 启动预览\n            mImpl.startPreview();\n            // 6. 通知外界, Camera 数据准备好了\n            listener.onCameraReady(mBufferTexture, previewSize, 0);\n            Log.i(TAG, \"Camera start preview success.\");\n        } catch (Throwable e) {\n            Log.e(TAG, \"Camera start preview failed.\", e);\n            close();\n        }\n    }\n\n    /**\n     * 选择相机 id\n     */\n    private int chooseCamera(int facing) {\n        int cameraId = INVALID_CAMERA_ID;\n        for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {\n            Camera.getCameraInfo(i, mCameraInfo);\n            if (mCameraInfo.facing == facing) {\n                cameraId = i;\n                break;\n            }\n        }\n        return cameraId;\n    }\n\n    /**\n     * 获取默认比例\n     */\n    private AspectRatio chooseDefaultAspectRatio() {\n        AspectRatio result = null;\n        for (AspectRatio ratio : mPreviewSizes.ratios()) {\n            result = ratio;\n            if (AspectRatio.DEFAULT.equals(ratio)) {\n                break;\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 设置自动对焦\n     * <p>\n     * it will modify {@link #mCameraParams}.\n     */\n    private void setAutoFocusInternal(boolean autoFocus) {\n        final List<String> modes = mCameraParams.getSupportedFocusModes();\n        if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {\n            mCameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);\n        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {\n            mCameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);\n        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {\n            mCameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);\n        } else {\n            mCameraParams.setFocusMode(modes.get(0));\n        }\n    }\n\n    /**\n     * 设置闪光灯\n     *\n     * @return {@code true} if {@link #mCameraParams} was modified.\n     */\n    private boolean setFlashInternal(int flash) {\n        List<String> modes = mCameraParams.getSupportedFlashModes();\n        String mode = FLASH_MODES.get(flash);\n        if (modes != null && modes.contains(mode)) {\n            mCameraParams.setFlashMode(mode);\n            flashMode = flash;\n            return true;\n        }\n        String currentMode = FLASH_MODES.get(flashMode);\n        if (modes == null || !modes.contains(currentMode)) {\n            mCameraParams.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);\n            flashMode = Constants.FLASH_OFF;\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * 选择最合适的预览尺寸\n     */\n    private Size chooseOptimalPreviewSize(SortedSet<Size> sizes) {\n        int desiredWidth;\n        int desiredHeight;\n        if (isLandscape()) {\n            desiredWidth = previewWidth;\n            desiredHeight = previewHeight;\n        } else {\n            desiredWidth = previewHeight;\n            desiredHeight = previewWidth;\n        }\n        Size result = null;\n        for (Size size : sizes) {\n            result = size;\n            // Iterate from small to large\n            if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {\n                break;\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Calculate camera rotate\n     * <p>\n     * This calculation is applied to the output JPEG either via Exif Orientation tag\n     * or by actually transforming the bitmap. (Determined by vendor camera API implementation)\n     * <p>\n     * Note: This is not the same calculation as the display orientation\n     *\n     * @param screenOrientationDegrees Screen orientation in degrees\n     * @return Number of degrees to rotate image in order for it to view correctly.\n     */\n    private int calcTakenPictureRotation(int screenOrientationDegrees) {\n        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {\n            return (mCameraInfo.orientation + screenOrientationDegrees) % 360;\n        } else {  // back-facing\n            final int landscapeFlip = isLandscape() ? 180 : 0;\n            return (mCameraInfo.orientation + screenOrientationDegrees + landscapeFlip) % 360;\n        }\n    }\n\n    /**\n     * Calculate display orientation\n     * https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)\n     * <p>\n     * This calculation is used for orienting the preview\n     * <p>\n     * Note: This is not the same calculation as the camera rotate\n     *\n     * @param screenOrientationDegrees Screen orientation in degrees(anticlockwise)\n     * @return Number of degrees required to rotate preview\n     */\n    private int calcPreviewFrameOrientation(int screenOrientationDegrees) {\n        int result;\n        // front-facing\n        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {\n            result = (mCameraInfo.orientation + screenOrientationDegrees) % 360;\n            // compensate the mirror\n            result = (360 - result) % 360;\n        }\n        // back-facing\n        else {\n            result = (mCameraInfo.orientation - screenOrientationDegrees + 360) % 360;\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/device/com/sharry/lib/camera/ICameraDevice.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.graphics.Bitmap;\nimport android.graphics.SurfaceTexture;\n\nimport androidx.annotation.NonNull;\n\n/**\n * The interface desc camera device.\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-04-24\n */\ninterface ICameraDevice {\n\n    void open();\n\n    void close();\n\n    Bitmap takePicture();\n\n    boolean isCameraOpened();\n\n    void notifyFacingChanged();\n\n    void notifyAspectRatioChanged();\n\n    void notifyAutoFocusChanged();\n\n    void notifyFlashModeChanged();\n\n    void notifyScreenOrientationChanged();\n\n    void notifyDesiredSizeChanged();\n\n    interface OnCameraReadyListener {\n\n        void onCameraReady(@NonNull SurfaceTexture cameraTexture, @NonNull Size textureSize, int displayRotation);\n\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/orientation/com/sharry/lib/camera/ScreenOrientationDetector.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.content.Context;\nimport android.util.SparseIntArray;\nimport android.view.Display;\nimport android.view.OrientationEventListener;\nimport android.view.Surface;\n\n/**\n * 屏幕方向探测器\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-05\n */\nclass ScreenOrientationDetector {\n\n    /**\n     * Mapping from Surface.Rotation_n to degrees.\n     */\n    private static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray();\n\n    static {\n        DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0);\n        DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90);\n        DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180);\n        DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270);\n    }\n\n    private final OrientationEventListener mOrientationEventListener;\n    private OnDisplayChangedListener mListener;\n    /**\n     * This is either Surface.Rotation_0, _90, _180, _270, or -1 (invalid).\n     */\n    private int mLastRotation = 0;\n    private Display mDisplay;\n\n    ScreenOrientationDetector(Context context, final OnDisplayChangedListener listener) {\n        this.mListener = listener;\n        this.mOrientationEventListener = new OrientationEventListener(context) {\n            @Override\n            public void onOrientationChanged(int orientation) {\n                if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN ||\n                        mDisplay == null) {\n                    return;\n                }\n                final int rotation = mDisplay.getRotation();\n                if (mLastRotation != rotation) {\n                    mLastRotation = rotation;\n                    mListener.onDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(mLastRotation));\n                }\n            }\n        };\n    }\n\n    void enable(Display display) {\n        mDisplay = display;\n        mOrientationEventListener.enable();\n        // callback at once\n        mLastRotation = mDisplay.getRotation();\n        mListener.onDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(mLastRotation));\n    }\n\n    void disable() {\n        mOrientationEventListener.disable();\n        mDisplay = null;\n    }\n\n    boolean isLandscape() {\n        int screenOrientationDegrees = DISPLAY_ORIENTATIONS.get(mLastRotation);\n        return (screenOrientationDegrees == Constants.LANDSCAPE_90\n                || screenOrientationDegrees == Constants.LANDSCAPE_270);\n    }\n\n    interface OnDisplayChangedListener {\n\n        /**\n         * Called when display orientation is changed.\n         *\n         * @param displayOrientation One of 0, 90, 180, and 270.\n         */\n        void onDisplayOrientationChanged(int displayOrientation);\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/DefaultPreviewerRenderer.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.content.Context;\nimport android.opengl.GLES20;\n\nimport com.sharry.lib.opengles.util.GlUtil;\n\nimport java.nio.FloatBuffer;\n\n/**\n * 默认的预览渲染器\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-28\n */\npublic class DefaultPreviewerRenderer extends PreviewerRendererWrapper {\n\n    private static final String VERTEX_SHADER_STR = \"attribute vec4 aVertexPosition;\\n\" +\n            \"    attribute vec2 aTexturePosition;\\n\" +\n            \"    varying vec2 vPosition;\\n\" +\n            \"    void main() {\\n\" +\n            \"        vPosition = aTexturePosition;\\n\" +\n            \"        gl_Position = aVertexPosition;\\n\" +\n            \"    }\";\n\n\n    private static final String FRAGMENT_SHADER_STR = \"precision mediump float;\\n\" +\n            \"varying vec2 vPosition;\\n\" +\n            \"uniform sampler2D uTexture;\\n\" +\n            \"void main() {\\n\" +\n            \"    gl_FragColor=texture2D(uTexture, vPosition);\\n\" +\n            \"}\";\n\n    private final float[] mVertexCoordinate = new float[]{\n            -1f, 1f,  // 左上\n            -1f, -1f, // 左下\n            1f, 1f,   // 右上\n            1f, -1f   // 右下\n    };\n    private final float[] mTextureCoordinate = new float[]{\n            0f, 1f,   // 左上\n            0f, 0f,   // 左下\n            1f, 1f,   // 右上\n            1f, 0f    // 右下\n    };\n\n    private final FloatBuffer mVertexBuffer = GlUtil.createFloatBuffer(mVertexCoordinate);\n    private final FloatBuffer mTextureBuffer = GlUtil.createFloatBuffer(mTextureCoordinate);\n    private int mProgramId;\n    private int aVertexPosition;\n    private int aTexturePosition;\n    private int mVboId;\n    private int uTexture;\n\n    public DefaultPreviewerRenderer(Context context) {\n        super(new PreviewerRendererImpl(context));\n    }\n\n    @Override\n    public void onAttach() {\n        super.onAttach();\n        // 初始化程序\n        setupShaders();\n        // 初始化顶点坐标\n        setupCoordinates();\n    }\n\n    private void setupShaders() {\n        mProgramId = GlUtil.createProgram(VERTEX_SHADER_STR, FRAGMENT_SHADER_STR);\n        aVertexPosition = GLES20.glGetAttribLocation(mProgramId, \"aVertexPosition\");\n        aTexturePosition = GLES20.glGetAttribLocation(mProgramId, \"aTexturePosition\");\n        uTexture = GLES20.glGetUniformLocation(mProgramId, \"uTexture\");\n    }\n\n    private void setupCoordinates() {\n        // 创建 vbo\n        int vboSize = 1;\n        int[] vboIds = new int[vboSize];\n        GLES20.glGenBuffers(vboSize, vboIds, 0);\n        // 将顶点坐标写入 vbo\n        mVboId = vboIds[0];\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 开辟 VBO 空间\n        GLES20.glBufferData(\n                GLES20.GL_ARRAY_BUFFER,\n                (mVertexCoordinate.length + mTextureCoordinate.length) * 4,\n                null,\n                GLES20.GL_STATIC_DRAW\n        );\n        // 写入顶点坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                0,\n                (mVertexCoordinate.length) * 4,\n                mVertexBuffer\n        );\n        // 写入纹理坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                (mVertexCoordinate.length) * 4,\n                (mTextureCoordinate.length) * 4,\n                mTextureBuffer\n        );\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n    }\n\n    @Override\n    protected void onDrawTexture(int textureId) {\n        GLES20.glUseProgram(mProgramId);\n        // 绑定纹理\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);\n        // 写入顶点坐标\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        GLES20.glEnableVertexAttribArray(aVertexPosition);\n        GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false,\n                8, 0);\n        // 写入纹理坐标\n        GLES20.glEnableVertexAttribArray(aTexturePosition);\n        GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false,\n                8, mVertexCoordinate.length * 4);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        // 给 uTexture 赋值\n        GLES20.glUniform1i(uTexture, 0);\n        // 绘制到屏幕\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n        // 解绑纹理\n        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);\n    }\n\n    @Override\n    public void onDetach() {\n        super.onDetach();\n        // 释放着色器程序\n        if (mProgramId != 0) {\n            GLES20.glDeleteProgram(mProgramId);\n        }\n        // 释放 VBO\n        if (mVboId != 0) {\n            int size = 1;\n            int[] vboIds = new int[size];\n            vboIds[0] = mVboId;\n            GLES20.glDeleteBuffers(1, vboIds, 0);\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/IPreviewer.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.graphics.Bitmap;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGLContext;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.sharry.lib.opengles.texture.ITextureRenderer;\n\n/**\n * 相机预览器的抽象描述\n *\n * @author Sharry <a href=\"xiaoyu.zhu@1hai.cn\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-08 10:49\n */\npublic interface IPreviewer {\n\n    /**\n     * 设置要预览的数据源\n     */\n    void setDataSource(@NonNull SurfaceTexture bufferTexture);\n\n    /**\n     * 设置 previewer 的渲染器\n     */\n    void setRenderer(@Nullable Renderer renderer);\n\n    /**\n     * 设置旋转角度\n     */\n    void setRotate(int degrees);\n\n    /**\n     * 设置缩放类型\n     */\n    void setScaleType(ScaleType type, boolean landscape, Size dataSourceSize);\n\n    /**\n     * 获取渲染器\n     */\n    @NonNull\n    Renderer getRenderer();\n\n    /**\n     * 获取用于预览的 view\n     */\n    View getView();\n\n    /**\n     * 获取渲染器的尺寸\n     */\n    @NonNull\n    Size getSize();\n\n    /**\n     * 获取当前帧的数据\n     */\n    Bitmap getBitmap();\n\n    /**\n     * 获取当前的渲染环境\n     */\n    EGLContext getEglContext();\n\n    /**\n     * 相机预览器的 Renderer\n     * <p>\n     * 对 ITextureRenderer 的增强, 拓展 matrix 功能\n     *\n     * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n     * @version 1.0\n     * @since 2019-07-27\n     */\n    interface Renderer extends ITextureRenderer {\n\n        /**\n         * 要渲染的数据源变更了\n         *\n         * @param dataSource 相机输出的外部纹理\n         */\n        void setDataSource(SurfaceTexture dataSource);\n\n        /**\n         * 获取预览器输出的纹理 ID\n         */\n        int getPreviewerTextureId();\n\n        /**\n         * 设置旋转角度\n         */\n        void setRotate(int degrees);\n\n        /**\n         * 设置缩放类型\n         */\n        void setScaleType(ScaleType type, boolean landscape, Size dataSourceSize, Size viewSize);\n\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/Previewer.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.SurfaceTexture;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.sharry.lib.opengles.texture.GLTextureView;\n\nimport java.lang.ref.WeakReference;\n\n/**\n * Camera 预览器\n * <p>\n * 使用 TextureView 渲染硬件相机输出的 SurfaceTexture\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-04-24\n */\n@SuppressLint(\"ViewConstructor\")\npublic final class Previewer extends GLTextureView implements IPreviewer {\n\n    private static final String TAG = Previewer.class.getSimpleName();\n\n    private final SurfaceTexture.OnFrameAvailableListener mFrameAvailableListener;\n    private Renderer mPreviewerRenderer;\n\n    Previewer(Context context, FrameLayout parent) {\n        super(context);\n        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n        params.gravity = Gravity.CENTER;\n        parent.addView(this, params);\n        // set default renderer\n        setRenderer(new DefaultPreviewerRenderer(context));\n        // create frame available listener\n        this.mFrameAvailableListener = new FrameAvailableListenerImpl(this);\n    }\n\n    /**\n     * 暂存 Renderer 的状态值, 方便 Renderer 切换时快速还原\n     */\n    private SurfaceTexture mDataSource;\n    private Size mDataSourceSize = getSize();\n    private int mDegree = 0;\n    private ScaleType mScaleType = ScaleType.CENTER_CROP;\n    private boolean mLandscape = false;\n\n    @Override\n    public void setDataSource(@NonNull SurfaceTexture dataSource) {\n        if (mDataSource == dataSource) {\n            Log.i(TAG, \"Data source not changed.\");\n            return;\n        }\n        // set callback\n        dataSource.setOnFrameAvailableListener(mFrameAvailableListener);\n        // notify renderer\n        Renderer renderer = mPreviewerRenderer;\n        if (renderer != null) {\n            renderer.setDataSource(dataSource);\n        }\n        // update dataSource\n        mDataSource = dataSource;\n    }\n\n    @Override\n    public void setRenderer(@Nullable Renderer newRenderer) {\n        if (mPreviewerRenderer == newRenderer) {\n            return;\n        }\n        if (newRenderer != null) {\n            newRenderer.setDataSource(mDataSource);\n            newRenderer.setRotate(mDegree);\n            newRenderer.setScaleType(mScaleType, mLandscape, mDataSourceSize, getSize());\n        }\n        mPreviewerRenderer = newRenderer;\n        super.setRenderer(newRenderer);\n    }\n\n    @Override\n    public void setRotate(int degrees) {\n        Renderer renderer = mPreviewerRenderer;\n        if (renderer != null) {\n            renderer.setRotate(degrees);\n        }\n        mDegree = degrees;\n    }\n\n    @Override\n    public void setScaleType(ScaleType type, boolean landscape, Size dataSourceSize) {\n        Renderer renderer = mPreviewerRenderer;\n        if (renderer != null) {\n            renderer.setScaleType(type, landscape, dataSourceSize, getSize());\n        }\n        mScaleType = type;\n        mLandscape = landscape;\n        mDataSourceSize = dataSourceSize;\n    }\n\n    @Override\n    public View getView() {\n        return this;\n    }\n\n    @NonNull\n    @Override\n    public Renderer getRenderer() {\n        return mPreviewerRenderer;\n    }\n\n    @Override\n    public Size getSize() {\n        return new Size(getWidth(), getHeight());\n    }\n\n    @Override\n    public Bitmap getBitmap() {\n        Bitmap bitmap = super.getBitmap();\n        Log.e(TAG, \"bitmap width = \" + bitmap.getWidth() + \", bitmap height = \" + bitmap.getHeight());\n        return bitmap;\n    }\n\n    private static class FrameAvailableListenerImpl extends WeakReference<Previewer>\n            implements SurfaceTexture.OnFrameAvailableListener {\n\n        private FrameAvailableListenerImpl(Previewer referent) {\n            super(referent);\n        }\n\n        @Override\n        public void onFrameAvailable(SurfaceTexture surfaceTexture) {\n            Previewer previewer = get();\n            if (previewer != null) {\n                previewer.requestRenderer();\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/PreviewerRendererImpl.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.content.Context;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.GLES11Ext;\nimport android.opengl.GLES20;\nimport android.opengl.Matrix;\nimport android.util.Log;\n\nimport com.sharry.lib.opengles.util.FboHelper;\nimport com.sharry.lib.opengles.util.GlUtil;\n\nimport java.nio.FloatBuffer;\n\nimport static android.opengl.GLES20.GL_FLOAT;\nimport static android.opengl.GLES20.glGetUniformLocation;\n\n/**\n * 处理相机输出的 OES 输出到 2D Texture 中, 2D 纹理 ID  通过 {@link #getPreviewerTextureId()} 获取\n *\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-07-28\n */\npublic class PreviewerRendererImpl implements IPreviewer.Renderer {\n\n    private static final String TAG = PreviewerRendererImpl.class.getSimpleName();\n\n    private final float[] mVertexCoordinate = new float[]{\n            -1f, 1f,  // 左上\n            -1f, -1f, // 左下\n            1f, 1f,   // 右上\n            1f, -1f   // 右下\n    };\n    private final float[] mTextureCoordinate = new float[]{\n            0f, 1f,   // 左上\n            0f, 0f,   // 左下\n            1f, 1f,   // 右上\n            1f, 0f    // 右下\n    };\n    private final FloatBuffer mVertexBuffer = GlUtil.createFloatBuffer(mVertexCoordinate);\n    private final FloatBuffer mTextureBuffer = GlUtil.createFloatBuffer(mTextureCoordinate);\n\n    private final Context mContext;\n    private final FboHelper mFboHelper;\n\n    /**\n     * 着色器相关\n     */\n    private int mProgram = 0;\n    private int aVertexCoordinate;\n    private int aTextureCoordinate;\n    private int uTextureMatrix;\n    private int uVertexMatrix;\n    private int uTexture;\n\n    /**\n     * Vertex buffer object 相关\n     */\n    private int mVboId = 0;\n\n    /**\n     * 用于和数据源绑定的外部纹理 ID\n     */\n    private int mOesTextureId = 0;\n\n    /**\n     * 数据源相关变量\n     */\n    private volatile SurfaceTexture mDataSource;\n    private final float[] mDataSourceMatrix = new float[16];\n    private boolean mIsAttached = false;\n\n    /**\n     * Matrix\n     */\n    private final float[] mProjectionMatrix = new float[16];      // 投影矩阵\n    private final float[] mRotationMatrix = new float[16];        // 裁剪矩阵\n    private final float[] mFinalMatrix = new float[16];           // 裁剪矩阵\n\n    public PreviewerRendererImpl(Context context) {\n        mContext = context;\n        mFboHelper = new FboHelper();\n        // 初始化矩阵\n        Matrix.setIdentityM(mProjectionMatrix, 0);\n        Matrix.setIdentityM(mRotationMatrix, 0);\n        Matrix.setIdentityM(mFinalMatrix, 0);\n    }\n\n    ////////////////////////////////////////////////////////////////////////////\n    // 渲染器的生命周期\n    ////////////////////////////////////////////////////////////////////////////\n\n    @Override\n    public void onAttach() {\n        mFboHelper.onAttach();\n        // 配置着色器\n        setupShaders();\n        // 配置坐标\n        setupCoordinates();\n        // 创建一个 OES 的纹理 ID, 用于后续绑定 DataSource.\n        mOesTextureId = createOESTexture();\n    }\n\n    private void setupShaders() {\n        // 加载着色器\n        String vertexSource = GlUtil.getGLResource(mContext, R.raw.camera_vertex_shader);\n        String fragmentSource = GlUtil.getGLResource(mContext, R.raw.camera_fragment_shader);\n        mProgram = GlUtil.createProgram(vertexSource, fragmentSource);\n        // 加载 Program 中的变量\n        aVertexCoordinate = GLES20.glGetAttribLocation(mProgram, \"aVertexCoordinate\");\n        aTextureCoordinate = GLES20.glGetAttribLocation(mProgram, \"aTextureCoordinate\");\n        uVertexMatrix = glGetUniformLocation(mProgram, \"uVertexMatrix\");\n        uTextureMatrix = glGetUniformLocation(mProgram, \"uTextureMatrix\");\n        uTexture = glGetUniformLocation(mProgram, \"uTexture\");\n    }\n\n    private void setupCoordinates() {\n        // 创建 vbo\n        int vboSize = 1;\n        int[] vboIds = new int[vboSize];\n        GLES20.glGenBuffers(vboSize, vboIds, 0);\n        // 将顶点坐标写入 vbo\n        mVboId = vboIds[0];\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        // 开辟 VBO 空间\n        GLES20.glBufferData(\n                GLES20.GL_ARRAY_BUFFER,\n                (mVertexCoordinate.length + mTextureCoordinate.length) * 4,\n                null,\n                GLES20.GL_STATIC_DRAW\n        );\n        // 写入顶点坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                0,\n                (mVertexCoordinate.length) * 4,\n                mVertexBuffer\n        );\n        // 写入纹理坐标\n        GLES20.glBufferSubData(\n                GLES20.GL_ARRAY_BUFFER,\n                (mVertexCoordinate.length) * 4,\n                (mTextureCoordinate.length) * 4,\n                mTextureBuffer\n        );\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n    }\n\n    private int createOESTexture() {\n        // 生成绑定纹理\n        int[] textures = new int[1];\n        GLES20.glGenTextures(1, textures, 0);\n        int textureId = textures[0];\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);\n        // 设置环绕方向\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);\n        // 设置纹理过滤方式\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n        // 解绑\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);\n        return textureId;\n    }\n\n    @Override\n    public void onSizeChanged(int width, int height) {\n        mFboHelper.onSizeChanged(width, height);\n        // 设置画布尺寸\n        GLES20.glViewport(0, 0, width, height);\n        // 清屏\n        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n        GLES20.glClearColor(0f, 0f, 0f, 0f);\n    }\n\n    @Override\n    public void onDraw() {\n        // 清屏\n        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n        GLES20.glClearColor(0f, 0f, 0f, 0f);\n        // 获取 OES Texture 中的数据帧\n        if (mDataSource != null) {\n            try {\n                // 为数据源绑定纹理 ID\n                if (!mIsAttached) {\n                    attachDataSource();\n                }\n                // 从获取数据源中获取数据\n                mDataSource.updateTexImage();\n                // 获取数据源的 transform 矩阵\n                mDataSource.getTransformMatrix(mDataSourceMatrix);\n            } catch (Throwable e) {\n                // ignore.\n            }\n        }\n        // 绑定 FBO\n        mFboHelper.bindFramebuffer();\n        // 将外部纹理绘制到 FBO\n        draw();\n        // 解绑 FBO\n        mFboHelper.unbindFramebuffer();\n    }\n\n    private void attachDataSource() {\n        try {\n            mDataSource.detachFromGLContext();\n        } catch (Throwable e) {\n            // ignore.\n        }\n        try {\n            mDataSource.attachToGLContext(mOesTextureId);\n            mIsAttached = true;\n        } catch (Throwable e) {\n            // ignore.\n        }\n    }\n\n    private void draw() {\n        // 激活着色器\n        GLES20.glUseProgram(mProgram);\n        /*\n         顶点着色器\n         */\n        // 顶点坐标赋值\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);\n        GLES20.glEnableVertexAttribArray(aVertexCoordinate);\n        GLES20.glVertexAttribPointer(aVertexCoordinate, 2, GL_FLOAT, false,\n                8, 0);\n        // 纹理坐标赋值\n        GLES20.glEnableVertexAttribArray(aTextureCoordinate);\n        GLES20.glVertexAttribPointer(aTextureCoordinate, 2, GL_FLOAT, false,\n                8, mVertexCoordinate.length * 4);\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        // 使裁剪矩阵合并旋转矩阵\n        Matrix.multiplyMM(mFinalMatrix, 0, mProjectionMatrix, 0,\n                mRotationMatrix, 0);\n        // 顶点变换矩阵赋值\n        GLES20.glUniformMatrix4fv(uVertexMatrix, 1, false, mFinalMatrix, 0);\n\n        // 纹理变换矩阵赋值\n        GLES20.glUniformMatrix4fv(uTextureMatrix, 1, false, mDataSourceMatrix, 0);\n\n        /*\n         片元着色器, 为 uTexture 赋值\n         */\n        GLES20.glUniform1i(uTexture, 0);\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mOesTextureId);\n\n        // 绘制矩形区域\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n\n        // 解绑纹理\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);\n    }\n\n    @Override\n    public void onDetach() {\n        mFboHelper.onDetach();\n        // 释放着色器程序\n        if (mProgram != 0) {\n            GLES20.glDeleteProgram(mProgram);\n        }\n        // 释放 VBO\n        if (mVboId != 0) {\n            int size = 1;\n            int[] vboIds = new int[size];\n            vboIds[0] = mVboId;\n            GLES20.glDeleteBuffers(1, vboIds, 0);\n        }\n        // 释放纹理\n        if (mOesTextureId != 0) {\n            int size = 1;\n            int[] textures = new int[size];\n            textures[0] = mOesTextureId;\n            GLES20.glDeleteTextures(1, textures, 0);\n        }\n        mIsAttached = false;\n    }\n\n    ////////////////////////////////////////////////////////////////////////////\n    // 其他\n    ////////////////////////////////////////////////////////////////////////////\n\n    @Override\n    public void setDataSource(SurfaceTexture dataSource) {\n        if (mDataSource != dataSource) {\n            mDataSource = dataSource;\n            mIsAttached = false;\n        }\n    }\n\n    @Override\n    public void setRotate(int degrees) {\n        Matrix.rotateM(mRotationMatrix, 0, degrees, 0, 0, 1);\n    }\n\n    @Override\n    public void setScaleType(ScaleType type, boolean isLandscape, Size dataSourceSize, Size viewSize) {\n        if (type != ScaleType.CENTER_CROP) {\n            return;\n        }\n        // 设置正交投影\n        float aspectPlane = viewSize.getWidth() / (float) viewSize.getHeight();\n        float aspectTexture = isLandscape ? dataSourceSize.getWidth() / (float) dataSourceSize.getHeight()\n                : dataSourceSize.getHeight() / (float) dataSourceSize.getWidth();\n        float left, top, right, bottom;\n        // 1. 纹理比例 > 投影平面比例\n        if (aspectTexture > aspectPlane) {\n            left = -aspectPlane / aspectTexture;\n            right = -left;\n            top = 1;\n            bottom = -1;\n        }\n        // 2. 纹理比例 < 投影平面比例\n        else {\n            left = -1;\n            right = 1;\n            top = 1 / aspectPlane * aspectTexture;\n            bottom = -top;\n        }\n        Matrix.orthoM(\n                mProjectionMatrix, 0,\n                left, right, bottom, top,\n                1, -1\n        );\n        Log.e(TAG, \"view size = \" + viewSize + \", data source size = \" + dataSourceSize);\n    }\n\n    @Override\n    public int getPreviewerTextureId() {\n        return mFboHelper.getTexture2DId();\n    }\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/PreviewerRendererWrapper.java",
    "content": "package com.sharry.lib.camera;\n\nimport android.graphics.SurfaceTexture;\n\n/**\n * @author Sharry <a href=\"sharrychoochn@gmail.com\">Contact me.</a>\n * @version 1.0\n * @since 2019-08-19\n */\npublic abstract class PreviewerRendererWrapper implements IPreviewer.Renderer {\n\n    private IPreviewer.Renderer mImpl;\n\n    public PreviewerRendererWrapper(IPreviewer.Renderer impl) {\n        this.mImpl = impl;\n    }\n\n    @Override\n    public void onAttach() {\n        mImpl.onAttach();\n    }\n\n    @Override\n    public void onSizeChanged(int width, int height) {\n        mImpl.onSizeChanged(width, height);\n    }\n\n    @Override\n    public void onDraw() {\n        mImpl.onDraw();\n        onDrawTexture(mImpl.getPreviewerTextureId());\n    }\n\n    @Override\n    public void onDetach() {\n        mImpl.onDetach();\n    }\n\n    @Override\n    public void setDataSource(SurfaceTexture dataSource) {\n        mImpl.setDataSource(dataSource);\n    }\n\n    @Override\n    public int getPreviewerTextureId() {\n        return mImpl.getPreviewerTextureId();\n    }\n\n    @Override\n    public void setRotate(int degrees) {\n        mImpl.setRotate(degrees);\n    }\n\n    @Override\n    public void setScaleType(ScaleType type, boolean landscape, Size dataSourceSize, Size viewSize) {\n        mImpl.setScaleType(type, landscape, dataSourceSize, viewSize);\n    }\n\n    protected abstract void onDrawTexture(int textureId);\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/previewer/com/sharry/lib/camera/ScaleType.java",
    "content": "package com.sharry.lib.camera;\n\npublic enum ScaleType {\n\n    CENTER_CROP,\n    FIT_CENTER\n\n}\n"
  },
  {
    "path": "lib-scamera/src/main/res/raw/camera_fragment_shader.glsl",
    "content": "#extension GL_OES_EGL_image_external : require\n// 设置精度，中等精度\nprecision mediump float;\n// 由顶点着色器输出, 经过栅格化转换之后的纹理坐标\nvarying vec2 vTextureCoordinate;\n// 2D 纹理, uniform 用于 application 向 gl 传值 （扩展纹理）\nuniform samplerExternalOES uTexture;\nvoid main(){\n    gl_FragColor = texture2D(uTexture, vTextureCoordinate);\n}"
  },
  {
    "path": "lib-scamera/src/main/res/raw/camera_vertex_shader.glsl",
    "content": "attribute vec4 aVertexCoordinate;  // 传入参数: 顶点坐标, Java 传入\nattribute vec4 aTextureCoordinate; // 传入参数: 纹理坐标, Java 传入\nuniform mat4 uVertexMatrix;        // 全局参数: 4x4 顶点的裁剪矩阵, Java 传入\nuniform mat4 uTextureMatrix;       // 全局参数: 4x4 矩阵纹理变化矩阵, Java 传入\nvarying vec2 vTextureCoordinate;   // 传出参数: 计算纹理坐标传递给 片元着色器\nvoid main() {\n    // 计算纹理坐标, 传出给片元着色器\n    vTextureCoordinate = (uTextureMatrix * aTextureCoordinate).xy;\n    // 计算顶点坐标, 输出给内建输出变量\n    gl_Position = uVertexMatrix * aVertexCoordinate;\n}"
  },
  {
    "path": "lib-scamera/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2016 The Android Open Source Project\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n         http://www.apache.org/licenses/LICENSE-2.0\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n    <declare-styleable name=\"SCameraView\">\n        <!--\n          Set this to true if you want the CameraView to adjust its bounds to preserve the aspect\n          ratio of its camera preview.\n        -->\n        <attr name=\"android:adjustViewBounds\"/>\n        <!-- Direction the camera faces relative to device screen. -->\n        <attr name=\"facing\" format=\"enum\">\n            <!-- The camera device faces the opposite direction as the device's screen. -->\n            <enum name=\"back\" value=\"0\"/>\n            <!-- The camera device faces the same direction as the device's screen. -->\n            <enum name=\"front\" value=\"1\"/>\n        </attr>\n        <!-- Aspect ratio of camera preview and pictures. -->\n        <attr name=\"aspectRatio\" format=\"string\"/>\n        <!-- Continuous auto focus mode. -->\n        <attr name=\"autoFocus\" format=\"boolean\"/>\n        <!-- The flash mode. -->\n        <attr name=\"flash\" format=\"enum\">\n            <!-- Flash will not be fired. -->\n            <enum name=\"off\" value=\"0\"/>\n            <!--\n              Flash will always be fired during snapshot.\n              The flash may also be fired during preview or auto-focus depending on the driver.\n            -->\n            <enum name=\"on\" value=\"1\"/>\n            <!--\n              Constant emission of light during preview, auto-focus and snapshot.\n              This can also be used for video recording.\n            -->\n            <enum name=\"torch\" value=\"2\"/>\n            <!--\n              Flash will be fired automatically when required.\n              The flash may be fired during preview, auto-focus, or snapshot depending on the\n              driver.\n            -->\n            <enum name=\"auto\" value=\"3\"/>\n            <!--\n              Flash will be fired in red-eye reduction mode.\n            -->\n            <enum name=\"redEye\" value=\"4\"/>\n        </attr>\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "lib-scamera/src/main/res/values/public.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2016 The Android Open Source Project\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n         http://www.apache.org/licenses/LICENSE-2.0\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n    <public name=\"facing\" type=\"attr\"/>\n    <public name=\"aspectRatio\" type=\"attr\"/>\n    <public name=\"autoFocus\" type=\"attr\"/>\n    <public name=\"flash\" type=\"attr\"/>\n\n    <public name=\"Widget.CameraView\" type=\"style\"/>\n</resources>\n"
  },
  {
    "path": "lib-scamera/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2016 The Android Open Source Project\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n         http://www.apache.org/licenses/LICENSE-2.0\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <style name=\"Widget.CameraView\" parent=\"android:Widget\">\n        <item name=\"android:adjustViewBounds\">false</item>\n        <item name=\"facing\">back</item>\n        <item name=\"aspectRatio\">4:3</item>\n        <item name=\"autoFocus\">true</item>\n        <item name=\"flash\">auto</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app'\ninclude ':lib-album'\ninclude ':lib-media-recorder'\ninclude ':lib-scamera'\ninclude ':lib-opengles'\n"
  }
]