[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n.DS_Store\n.idea\n/build\nbuild\n/captures\n.externalNativeBuild\n/gradle\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\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"
  },
  {
    "path": "README.md",
    "content": "[ ![Download](https://api.bintray.com/packages/nfleo/MatisseKotlin/MatisseKotlin/images/download.svg?version=2.1.1) ](https://bintray.com/nfleo/MatisseKotlin/MatisseKotlin/2.1.1/link)\n\n![Image](/image/banner.png)\n\n[Matisse-kotlin地址](https://github.com/NFLeo/Matisse-Kotlin)[包体大小278kb]()\n\n首先感谢：\nMatisse核心功能：[https://github.com/zhihu/Matisse](https://github.com/zhihu/Matisse)\n\n裁剪提供者：Yalantis github地址：[https://github.com/Yalantis/uCrop](https://github.com/Yalantis/uCrop) \n\n完整说明文档：[Android 图片选择库 MatisseKotlin 版](https://www.jianshu.com/p/ca1e7460fa69)\n\n# 版本更新记录\n\n2020-4-2 (v_2.1.1)\n1. 修复裁剪结果无法带回问题\n\n2020-4-2\n1. 修复部分设备 column '_data' does not exist\n\n2020-3-30  注：升级后，旧裁剪无法使用\n1. 裁剪适配Android Q，实现为UCrop\n    裁剪功能 主要暴露两个开关 isCrop(boolean)、isCircleCrop(boolean)\n2. 修复部分华为设备图片无法裁剪问题\n3. 修复预览大概率ANR问题\n\n\n2020-1-18\n1. 裁剪适配Android Q\n2. 去除内部压缩-后期将使用接入Luban压缩\n3. 修改提示方式\n    setNoticeConsumer { context, noticeType, title, message ->\n                             showToast(context, noticeType, title, message)\n                         }\n4. 修改状态栏处理方法\n\n    setStatusBarFuture { params, view ->\n                    // 外部设置状态栏\n                    ImmersionBar.with(params)?.run {\n                        statusBarDarkFont(true)\n                        view?.apply { titleBar(this) }\n                        init()\n                    }\n\n                    // 外部可隐藏Matisse界面中的标题栏\n                    // view?.visibility = if (isDarkStatus) View.VISIBLE else View.GONE\n                }\n5. 注意: 单独调用拍照走裁剪时,也需要创建SelectionCreator\n\n2020-1-17 (v_2.0.5)\n1. 修复Gif图预览崩溃问题\n\n2020-1-14 (v_2.0.5)\n1. 拍照、展示图片适配Android Q\n\n2020-1-3 (2.0.3)\n1. 修复PreviewPageAdapter获取资源数组越界\n\n2019-12-30\n1. 默认关闭内部压缩\n2. 修复MatisseActivity中albumLoad空指针异常\n3. 修复Item创建时，部分cursor为空问题\n4. 修复demo压缩开关出错问题\n\n2019-12-23\n1. 修复Gif图片类型无法正确判断问题\n2. 修复部分设备第一次拍照后，拍照结果不显示问题\n\n2019-12-19\n1. 主题属性命名规范化 见R.style.CustomMatisseStyle\n2. MimeTypeManager类新增ofMotionlessImage()静态图类型\n3. Item列宽(spanSize和gridExceptedSize)添加限制，避免过大/过小\n4. 去除app_name\n5. 消除选中闪烁\n6. 精简压缩库大小 目前release aar 278k\n\n2019-12-10 (2.0.2)\n1. 修复mimeType为空情况\n2. 修复spanSize和gridExceptedSize同时设置冲突\n    注：同时设置时，读取gridExceptedSize值\n\n2019-11-10 (2.0.1)\n1. 修复裁剪结果尺寸异常\n\n2019-11-4 (2.0)\n1. 相机单独提取\n2. 支持默认选中，可传入上次选中的项（通过图片cursor id或uri string对比）\n    注：不支持裁剪带回的图片，裁剪带回的图片无id和uri\n```\n.setLastChoosePicturesIdOrUri(selectedPathIds as ArrayList<String>?)\n```\n3. 修复压缩为空带回崩溃\n\n2019-10-29 (1.2.3)\n1. 修复相册弹窗高度不准确问题\n2. 支持压缩配置，外部添加开关  api:[isInnerCompress]\n3. 完善未选中资源时各按钮点击添加提示\n4. 修复不同设备返回的媒体类型表示不一致（如：JPEG image/jpeg）\n5. 去除[api setStatusIsDark], 外部处理状态栏，见[api setStatusBarFuture]\n\n2019-10-28 (持续更新 待发布)\n1. 支持相机拍照完成后多选\n2. 扩展提示方法，支持使用外部自定义弹窗\n3. 支持外部处理状态栏，去除项目中原[ImmersionBar]库\n```\n.setStatusBarFuture(object : MFunction<BaseActivity> {\n                override fun accept(params: BaseActivity, view: View?) {\n                    // 外部设置状态栏\n                    ImmersionBar.with(params)?.run {\n                        statusBarDarkFont(isDarkStatus)\n                        view?.apply { titleBar(this) }\n                        init()\n                    }\n\n                    // 外部可隐藏Matisse界面中的View\n                    view?.visibility = if (isDarkStatus) View.VISIBLE else View.GONE\n                }\n            })\n```\n4. 按官方方式适配Android Q\n\n2019-10-21 (1.2.2)\n1. 修复方形裁剪图片变形问题\n2. 优化单选/多选刷新问题\n\n2019-10-18 (1.2.0)\n1. 迁移到androidx\n2. 修复并支持图片与视频混合选择\n```\n设置选择单一类型媒体，示例如下\nMatisse.choose(MimeTypeManager.ofAll())\n                .maxSelectable(3)\n或者\nMatisse.choose(MimeTypeManager.ofAll(), true)\n                .maxSelectable(3)\n\n设置选择混合类型媒体，示例如下\nMatisse.choose(MimeTypeManager.ofAll(), false)\n                .maxSelectablePerMediaType(4, 2)\n\n说明：\nmediaTypeExclusive true 单一媒体类型选择\n     读取maxSelectable属性作为最大值\nmediaTypeExclusive false\n     读取maxImageSelectable和maxVideoSelectable属性分别作为最大值\n```\n3. 修改单/多选逻辑\n\t* 单选支持重新选定，不支持计数方式\n\t* 多选不支持重新选定，选满外部给出提示方式，支持计数与选中方式\n4. 提示方式外部实现\n```\nSelectionCreator.setNoticeEvent( { context: Context, noticeType: Int, title: String, message: String\n                                 ->\n                                     // 外部提示，可外部定义样式\n                                     showToast(context, noticeType, title, message)\n                                 }\n                             })\n```\n\n2019-10-16\n1. 完善主题扩展，并提供图片说明\n\n\n# Matisse\n本项目为知乎原项目kotlin改写版本（2018/9月版本），由于项目纯图片选择库与[Matisse](https://github.com/zhihu/Matisse)UI风格有较大差异，为方便个人使用顺手便对[Matisse](https://github.com/zhihu/Matisse)进行Kotlin翻译，主要对原项目进行部分UI层面改写、已发现bug的修改、新功能添加。\n*主要修改内容为：*\n```\n1. 优化相册选择。\n2. 优化单选策略。\n3. 添加圆形与方形裁剪。\n4. 图片选择后压缩，不失真条件下高比率压缩。\n5. 增加主题修改，基本可保证定制成与自身项目风格一致\n6. 支持设置状态栏颜色 需依赖[ImmersionBar](https://github.com/gyf-dev/ImmersionBar)\n    1.2.2之后版本去除内部ImmersionBar处理\n\n\n* 注：裁剪成功后只返回裁剪后图片的绝对路径，不返回Uri，需自行转换\n\n具体调用查看 SelectionCreator.java\n\n关于打包报错问题：\n\n使用：\n1. gradle中添加 implementation 'com.nfleo:MatisseKotlin:2.0.4'\n2. AndroidManifest.xml中添加以下代码\n* 注：注意provider androidx的差别\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths_public\"/>\n        </provider>\n\n3. 6.0+需处理权限\nThe library requires two permissions:\n    - `android.permission.READ_EXTERNAL_STORAGE`\n    - `android.permission.WRITE_EXTERNAL_STORAGE`\n\n4. 为适配7.0，,项目manifest的privider标签下 paths文件中添加\n    文件名称为file_paths_public（名字随意取，但需与AndroidManifest.xml中引用保持一致）\n    <external-path  name=\"my_images\" path=\"Pictures\"/>\n\n\n| Default Style                  | Other Style Preview                  | Preview                          |\n|:------------------------------:|:---------------------------------:|:--------------------------------:|\n|![](image/screenshot_default.jpg) | ![](image/screenshot_other.jpg) | ![](image/screenshot_preview.jpg)|\n\n| Circle Crop                    | Square Crop                       |\n|:------------------------------:|:---------------------------------:|\n|![](image/screenshot_circlecrop.jpg) | ![](image/screenshot_squarecrop.jpg) |\n\n\n#### Simple usage snippet\n------\n\n### 配置主题.\n| Media theme                    | Preview theme                       |\n|:------------------------------:|:---------------------------------:|\n|![](image/media_theme_describe.png) | ![](image/preview_theme_describe.png) |\n\n\n使用套路与原项目一致，只是多增加了一些参数，另外定制时需配置所提供的所有参数。\n使用主题时可直接使用Matisse.Default， 或者在自己项目中另写style继承自Matisse.Default，修改自己所需属性，如下\n```\napp/style.xml\n    <style name=\"CustomMatisseStyle\" parent=\"Matisse.Default\">\n        <item name=\"Media_Back_text\">@string/back</item>\n        <item name=\"Media_Sure_text\">@string/sure</item>\n        <item name=\"Media_Preview_text\">@string/preview</item>\n        <item name=\"Media_Original_text\">@string/original</item>\n        <item name=\"Media_Album_text\">@string/album</item>\n        <item name=\"Media_Camera_text\">@string/camera</item>\n    </style>\n```\n\n```\nmatisse/style.xml\n    <style name=\"Matisse.Default\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!--状态栏相关-->\n        <item name=\"colorPrimary\">@color/primary</item>\n        <item name=\"colorPrimaryDark\">@color/primary_dark</item>\n\n        <!--顶部导航条高度-->\n        <item name=\"Navigation_height\">@dimen/navigation_height</item>\n        <!--顶部导航条背景色-->\n        <item name=\"Navigation_bg\">@color/navigation_bg</item>\n        <!--顶部导航条返回按钮资源图-->\n        <item name=\"Navigation_backRes\">@drawable/icon_arrow_right_white</item>\n        <!--图片预览界面背景色-->\n        <item name=\"Preview_bg\">@color/preview_bg</item>\n\n        <!--底部工具栏背景色-->\n        <item name=\"BottomToolbar_bg\">@color/bottomTool_bg</item>\n        <!--底部工具栏高度-->\n        <item name=\"BottomToolbar_height\">@dimen/bottom_tool_height</item>\n\n        <!--返回按钮文字 当无文字是可设置为空字符串 如下-->\n        <item name=\"Media_Back_text\">@string/button_null</item>\n        <!--返回按钮颜色-->\n        <item name=\"Media_Back_textColor\">@color/color_base</item>\n        <!--返回按钮文字大小-->\n        <item name=\"Media_Back_textSize\">@dimen/text_back</item>\n\n        <item name=\"Media_Sure_text\">@string/button_sure</item>\n        <!--确定按钮颜色-->\n        <item name=\"Media_Sure_textColor\">@color/color_base</item>\n        <!--确认按钮文字大小-->\n        <item name=\"Media_Sure_textSize\">@dimen/text_sure</item>\n\n        <item name=\"Media_Preview_text\">@string/button_preview</item>\n        <!--预览按钮颜色-->\n        <item name=\"Media_Preview_textColor\">@color/text_preview</item>\n        <!--预览按钮文字大小-->\n        <item name=\"Media_Preview_textSize\">@dimen/text_preview</item>\n\n        <item name=\"Media_Original_text\">@string/button_original</item>\n        <!--原图选择控件文字颜色-->\n        <item name=\"Media_Original_textColor\">@color/text_original</item>\n        <!--原图按钮文字大小-->\n        <item name=\"Media_Original_textSize\">@dimen/text_original</item>\n\n        <item name=\"Media_Album_text\">@string/album_name_all</item>\n        <!--查看全部相册文件夹按钮颜色-->\n        <item name=\"Media_Album_textColor\">@color/text_album</item>\n        <!--查看全部相册按钮文字大小-->\n        <item name=\"Media_Album_textSize\">@dimen/text_album</item>\n        <!--相册文件夹内部列表item颜色-->\n        <item name=\"Media_Album_Item_textColor\">@color/text_item_album</item>\n        <!--查看全部相册内item文字大小-->\n        <item name=\"Media_Album_Item_textSize\">@dimen/text_item_album</item>\n\n        <!--列表中可拍照状态下相机item文字颜色-->\n        <item name=\"Media_Camera_textColor\">@color/text_camera</item>\n        <!--列表中可拍照状态下相机item文字大小-->\n        <item name=\"Media_Camera_textSize\">@dimen/text_camera</item>\n\n        <item name=\"Preview_Back_text\">@string/button_back</item>\n        <item name=\"Preview_Confirm_text\">@string/button_sure_default</item>\n\n        <!--选中控件背景色-->\n        <item name=\"Item_checkCircle_bgColor\">@color/selector_base_text</item>\n        <!--选中控件圆环边框颜色-->\n        <item name=\"Item_checkCircle_borderColor\">@color/item_checkCircle_borderColor</item>\n        <!--原图radio控件颜色-->\n        <item name=\"Item_checkRadio\">@color/selector_base_text</item>\n\n        <!--空状态文字颜色-->\n        <item name=\"Media_Empty_textColor\">@color/text_empty</item>\n        <!--空状态文字大小-->\n        <item name=\"Media_Empty_textSize\">@dimen/text_empty</item>\n        <item name=\"Media_Empty_text\">@string/empty_text</item>\n        <!--空状态资源图-->\n        <item name=\"Media_Empty_resource\">@drawable/ic_empty_zhihu</item>\n\n        <!--图片加载占位图-->\n        <!--<item name=\"Item_placeholder\">@drawable/zhihu_item_placeholder</item>-->\n    </style>\n\n如需定制UI样式 按需修改，否则使用上述默认主题\nMatisse.from(this@MainActivity)\n             .setStatusIsDark(true)   // 按需设置状态栏文字颜色\n             .theme(R.style.Matisse_Default)    // 设置成所需主题\n```\n\n### 注意：1.1.1版本支持外部设置设置状态栏颜色，关于状态栏与状态栏底色问题\n##### 项目内部 仅添加了该库(`compileOnly 'com.gyf.barlibrary:barlibrary:2.3.0'`)的编译\n需要根据样式修改状态栏文字颜色，图片预览界面状态栏隐藏功能，需在自己项目中引入该库\n```\nMatisse.from(this@MainActivity)\n             ...\n             .setStatusIsDark(true)   // 设置状态栏文字颜色 true=黑色  false=白色\n             .theme(R.style.Matisse_Default)    // 设置成所需主题\n             ...\n```\n\nStart `MatisseActivity` from current `Activity` or `Fragment`:\n\n```\nkotlin项目调用\nMatisse.from(MainActivity.this)\n        .choose(MimeTypeManager.ofAll(), false)\n        .countable(true)\n        .maxSelectable(9)\n        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))\n        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))\n        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)\n        .thumbnailScale(0.85f)\n        .imageEngine(new GlideEngine())\n        .forResult(REQUEST_CODE_CHOOSE);\n\njava项目调用\nMatisse.Companion.from(MainActivity.this)\n        .choose(MimeTypeManager.Companion.ofAll(), false)\n        .countable(true)\n        .maxSelectable(9)\n        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))\n        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))\n        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)\n        .thumbnailScale(0.85f)\n        .imageEngine(new GlideEngine())\n        .forResult(REQUEST_CODE_CHOOSE);\n\nMatisse.from(SampleActivity.this)\n        .choose(MimeTypeManager.ofAll(), false)      // 展示所有类型文件（图片 视频 gif）\n        .capture(true)                        // 可拍照\n        .countable(true)                      // 记录文件选择顺序\n        .captureStrategy(new CaptureStrategy(true, \"cache path\"))\n        .maxSelectable(1)                     // 最多选择一张\n        .isCrop(true)                         // 开启裁剪\n        .isCircleCrop(true)                   // 设置裁剪类型\n        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))  // 筛选数据源可选大小限制\n        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))\n        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)\n        .thumbnailScale(0.8f)\n        .setStatusIsDark(true)            // 设置状态栏文字颜色 需依赖ImmersionBar库\n        .imageEngine(new GlideEngine())   // 加载库需外部实现\n        .forResult(REQUEST_CODE_CHOOSE);\n```\n\n为方便后期兼容Fresco，图片加载类需外部实现\n**注意：**目前慎用Fresco（尽管提供了栗子）！！！，图片加载兼容了Fresco，***但，图片放大预览并未兼容***\n```\nclass Glide4Engine : ImageEngine {\n\n    override fun cleanMemory(context: Context) {\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            Glide.get(context).clearMemory()\n        }\n    }\n\n    override fun pause(context: Context) {\n        Glide.with(context).pauseRequests()\n    }\n\n    override fun resume(context: Context) {\n        Glide.with(context).resumeRequests()\n    }\n\n    override fun init(context: Context) {\n    }\n\n    override fun loadThumbnail(context: Context, resize: Int, placeholder: Drawable, imageView: ImageView, uri: Uri) {\n        Glide.with(context)\n                .asBitmap() // some .jpeg files are actually gif\n                .load(uri)\n                .apply(RequestOptions()\n                        .override(resize, resize)\n                        .placeholder(placeholder)\n                        .centerCrop())\n                .into(imageView)\n    }\n\n    override fun loadGifThumbnail(context: Context, resize: Int, placeholder: Drawable, imageView: ImageView,\n                                  uri: Uri) {\n        Glide.with(context)\n                .asBitmap() // some .jpeg files are actually gif\n                .load(uri)\n                .apply(RequestOptions()\n                        .override(resize, resize)\n                        .placeholder(placeholder)\n                        .centerCrop())\n                .into(imageView)\n    }\n\n    override fun loadImage(context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri) {\n        Glide.with(context)\n                .load(uri)\n                .apply(RequestOptions()\n                        .override(resizeX, resizeY)\n                        .priority(Priority.HIGH)\n                        .fitCenter())\n                .into(imageView)\n    }\n\n    override fun loadGifImage(context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri) {\n        Glide.with(context)\n                .asGif()\n                .load(uri)\n                .apply(RequestOptions()\n                        .override(resizeX, resizeY)\n                        .priority(Priority.HIGH)\n                        .fitCenter())\n                .into(imageView)\n    }\n}\n```"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion rootProject.ext.compileSdkVersion\n\n    defaultConfig {\n        minSdkVersion rootProject.ext.minSdkVersion\n        targetSdkVersion rootProject.ext.targetSdkVersion\n\n        applicationId \"com.leo.matisse\"\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility 1.8\n        targetCompatibility 1.8\n    }\n}\n\nrepositories {\n    flatDir {\n        dirs 'libs'\n    }\n}\n\n/**\n * @Parcelize 注解方式显示序列化\n * */\nandroidExtensions {\n    experimental = true\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    testImplementation 'junit:junit:4.12'\n    androidTestImplementation 'androidx.test:runner:1.2.0'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n    implementation \"com.github.bumptech.glide:glide:$rootProject.ext.glide\"\n    implementation 'com.google.android.material:material:1.0.0'\n    /*动态权限*/\n    implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar'\n    implementation 'io.reactivex.rxjava2:rxjava:2.2.12'\n    implementation 'com.facebook.fresco:fresco:1.14.1'\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'\n    implementation 'com.gyf.barlibrary:barlibrary:2.3.0'\n\n//    implementation project(':matisse')\n    implementation 'com.nfleo:MatisseKotlin:2.1.1'\n}\n"
  },
  {
    "path": "app/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": "app/src/androidTest/java/com/leo/matisse/ExampleInstrumentedTest.kt",
    "content": "package com.leo.matisse\n\nimport android.support.test.InstrumentationRegistry\nimport android.support.test.runner.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getTargetContext()\n        assertEquals(\"com.leo.matisse\", appContext.packageName)\n    }\n}\n"
  },
  {
    "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    package=\"com.leo.matisse\">\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        <activity android:name=\".ExampleActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n        </activity>\n        <activity android:name=\".MainActivity\" />\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths_public\"/>\n        </provider>\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/ExampleActivity.kt",
    "content": "package com.leo.matisse\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.ActivityInfo\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.CompoundButton\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport com.bumptech.glide.Glide\nimport com.gyf.barlibrary.ImmersionBar\nimport com.matisse.Matisse\nimport com.matisse.MimeType\nimport com.matisse.MimeTypeManager\nimport com.matisse.SelectionCreator\nimport com.matisse.entity.CaptureStrategy\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.IncapableCause\nimport com.matisse.ui.activity.BaseActivity\nimport com.matisse.utils.MediaStoreCompat\nimport com.matisse.utils.Platform\nimport com.matisse.utils.gotoImageCrop\nimport com.matisse.widget.IncapableDialog\nimport com.tbruyelle.rxpermissions2.RxPermissions\nimport kotlinx.android.synthetic.main.activity_example.*\nimport java.util.*\n\nclass ExampleActivity : AppCompatActivity(), View.OnClickListener {\n    private var showType = MimeTypeManager.ofAll()\n    private var showCustomizeType: MutableList<MimeType>? = null\n    private var mediaTypeExclusive = true\n    private var isSingleChoose = false\n    private var isCountable = true\n    private var defaultTheme = R.style.Matisse_Default\n    private var maxCount = 5\n    private var maxImageCount = 1\n    private var maxVideoCount = 1\n    private var isOpenCamera = false\n    private var spanCount = 3\n    private var gridSizePx = 0\n    private var isCrop = false\n    private var isCircleCrop = false\n\n    private var isColumnNum = true\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_example)\n        initListener()\n    }\n\n    private fun initListener() {\n        rg_show.setOnCheckedChangeListener { _, checkedId ->\n            when (checkedId) {\n                R.id.btnAll -> showType = MimeTypeManager.ofAll()\n                R.id.btnVideo -> showType = MimeTypeManager.ofVideo()\n                R.id.btnImage -> showType = MimeTypeManager.ofImage()\n            }\n        }\n\n        rg_media_exclusive.setOnCheckedChangeListener { _, checkedId ->\n            when (checkedId) {\n                R.id.btnMixed -> {\n                    mediaTypeExclusive = false\n                    ev_max_1.visibility = View.VISIBLE\n                    ev_max_2.visibility = if (isSingleChoose) View.GONE else View.VISIBLE\n                    tv_max_1.text = \"图片最大选择数\"\n                }\n                R.id.btnExclusive -> {\n                    mediaTypeExclusive = true\n                    ev_max_1.visibility = View.VISIBLE\n                    ev_max_2.visibility = View.GONE\n                    tv_max_1.text = \"最大可选择数\"\n                }\n            }\n        }\n\n        rg_theme.setOnCheckedChangeListener { _, checkedId ->\n            when (checkedId) {\n                R.id.btn_normal_theme -> defaultTheme = R.style.Matisse_Default\n                R.id.btn_customize_theme -> defaultTheme = R.style.CustomMatisseStyle\n                R.id.btn_jc_theme -> defaultTheme = R.style.JCStyle\n            }\n        }\n\n        rg_column.setOnCheckedChangeListener { _, checkedId ->\n            when (checkedId) {\n                R.id.btn_num_column -> {\n                    isColumnNum = true\n                    ev_column.setText(\"3\")\n                }\n                R.id.btn_size_column -> {\n                    isColumnNum = false\n                    ev_column.setText(\"300\")\n                }\n            }\n        }\n\n        if (showCustomizeType != null && showCustomizeType?.size ?: 0 > 0) {\n            showType =\n                MimeTypeManager.of(showCustomizeType!![0], showCustomizeType?.toTypedArray()!!)\n        }\n\n        switch_choose_type.setOnCheckedChangeListener { _, isChecked ->\n            isSingleChoose = isChecked\n            if (isSingleChoose) {\n                maxCount = 1\n                maxImageCount = 1\n                maxVideoCount = 1\n                ev_max_1.visibility = View.GONE\n                ev_max_2.visibility = View.GONE\n\n                // 单选才支持裁剪\n                ll_crop.visibility = View.VISIBLE\n            } else {\n                if (mediaTypeExclusive) {\n                    tv_max_1.text = \"最大可选择数\"\n                    ev_max_2.visibility = View.GONE\n                    ev_max_1.visibility = View.VISIBLE\n                } else {\n                    tv_max_1.text = \"图片最大选择数\"\n                    ev_max_1.visibility = View.VISIBLE\n                    ev_max_2.visibility = View.VISIBLE\n                }\n\n                ll_crop.visibility = View.GONE\n            }\n        }\n\n        switch_check_type.setOnCheckedChangeListener { _, isChecked -> isCountable = !isChecked }\n\n        switch_capture.setOnCheckedChangeListener { _, isChecked -> isOpenCamera = isChecked }\n\n        switch_crop.setOnCheckedChangeListener { _, isChecked ->\n            isCrop = isChecked\n\n            if (isChecked) {\n                ll_crop_type.visibility = View.VISIBLE\n            } else {\n                ll_crop_type.visibility = View.GONE\n            }\n        }\n\n        switch_crop_type.setOnCheckedChangeListener { _, isChecked -> isCircleCrop = isChecked }\n\n        chb_jpeg.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_png.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_gif.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_bmp.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_webp.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_mpeg.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_mp4.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_quick_time.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_threegpp.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_threegpp2.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_mkv.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_webm.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_ts.setOnCheckedChangeListener(checkedOnCheckedListener)\n        chb_avi.setOnCheckedChangeListener(checkedOnCheckedListener)\n\n        btn_open_matisse.setOnClickListener(this)\n        btn_open_capture.setOnClickListener(this)\n    }\n\n    @SuppressLint(\"CheckResult\")\n    override fun onClick(v: View?) {\n\n        RxPermissions(this@ExampleActivity)\n            .request(\n                Manifest.permission.WRITE_EXTERNAL_STORAGE,\n                Manifest.permission.CAMERA,\n                Manifest.permission.READ_EXTERNAL_STORAGE\n            )\n            .subscribe {\n                if (!it) {\n                    showToast(\n                        this, IncapableCause.TOAST, \"\",\n                        getString(R.string.permission_request_denied)\n                    )\n                    return@subscribe\n                }\n\n                createMatisse()\n\n                when (v) {\n                    btn_open_matisse -> {\n                        openMatisse()\n                    }\n                    btn_open_capture -> {\n                        createMediaStoreCompat()\n                        mediaStoreCompat?.dispatchCaptureIntent(\n                            this, ConstValue.REQUEST_CODE_CAPTURE\n                        )\n                    }\n                }\n            }\n    }\n\n    private fun showToast(context: Context, noticeType: Int, title: String, message: String) {\n        if (noticeType == IncapableCause.TOAST) {\n            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()\n        } else if (noticeType == IncapableCause.DIALOG) {\n            // 外部弹窗，可外部定义样式\n            val incapableDialog = IncapableDialog.newInstance(title, message)\n            incapableDialog.show(\n                (context as BaseActivity).supportFragmentManager, IncapableDialog::class.java.name\n            )\n        } else if(noticeType == IncapableCause.LOADING) {\n\n        }\n\n    }\n\n    private var checkedOnCheckedListener =\n        CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->\n            val mimeType = when (buttonView) {\n                chb_jpeg -> MimeType.JPEG\n                chb_png -> MimeType.PNG\n                chb_gif -> MimeType.GIF\n                chb_bmp -> MimeType.BMP\n                chb_webp -> MimeType.WEBP\n                chb_mpeg -> MimeType.MPEG\n                chb_mp4 -> MimeType.MP4\n                chb_quick_time -> MimeType.QUICKTIME\n                chb_threegpp -> MimeType.THREEGPP\n                chb_threegpp2 -> MimeType.THREEGPP2\n                chb_mkv -> MimeType.MKV\n                chb_webm -> MimeType.WEBM\n                chb_ts -> MimeType.TS\n                chb_avi -> MimeType.AVI\n                else -> null\n            } ?: return@OnCheckedChangeListener\n\n            if (showCustomizeType == null)\n                showCustomizeType = mutableListOf()\n\n            if (isChecked) {\n                showCustomizeType?.add(mimeType)\n            } else {\n                showCustomizeType?.remove(mimeType)\n            }\n        }\n\n    private var mediaStoreCompat: MediaStoreCompat? = null\n    private var selectionCreator: SelectionCreator? = null\n    private var selectedPathIds: List<String>? = null\n\n    private fun createMatisse() {\n\n        setEditText()\n        selectionCreator =\n            Matisse.from(this@ExampleActivity)                              // 绑定Activity/Fragment\n                .choose(\n                    showType,\n                    mediaTypeExclusive\n                )                               // 设置显示类型，单一/混合选择模式\n                .theme(defaultTheme)                                                // 外部设置主题样式\n                .countable(isCountable)                                             // 设置选中计数方式\n                .isCrop(isCrop)                                                     // 设置开启裁剪\n                .isCircleCrop(isCircleCrop)                                                // 裁剪类型，圆形/方形\n                .maxSelectable(maxCount)                                            // 单一选择下 最大选择数量\n                .maxSelectablePerMediaType(\n                    maxImageCount,\n                    maxVideoCount\n                )            // 混合选择下 视频/图片最大选择数量\n                .capture(isOpenCamera)                                              // 是否开启内部拍摄\n                .captureStrategy(                                                   // 拍照设置Strategy\n                    CaptureStrategy(\n                        true,\n                        \"${Platform.getPackageName(this@ExampleActivity)}.fileprovider\"\n                    )\n                )\n                .thumbnailScale(0.6f)                                         // 图片显示压缩比\n                .spanCount(spanCount)                                               // 资源显示列数\n                .gridExpectedSize(gridSizePx)                                       // 资源显示网格列宽度\n                .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)      // 强制屏幕方向\n                .imageEngine(Glide4Engine())                                        // 图片加载实现方式\n                .setLastChoosePicturesIdOrUri(selectedPathIds as ArrayList<String>?)// 预选中\n                .setNoticeConsumer { context, noticeType, title, message ->\n                    showToast(context, noticeType, title, message)\n                }.setStatusBarFuture { params, view ->\n                    // 外部设置状态栏\n                    ImmersionBar.with(params)?.run {\n                        statusBarDarkFont(true)\n                        view?.apply { titleBar(this) }\n                        init()\n                    }\n\n                    // 外部可隐藏Matisse界面中的标题栏\n                    // view?.visibility = if (isDarkStatus) View.VISIBLE else View.GONE\n                }\n    }\n\n    private fun createMediaStoreCompat() {\n        if (mediaStoreCompat != null) return\n\n        val captureStrategy =\n            CaptureStrategy(\n                true,\n                \"${Platform.getPackageName(this@ExampleActivity)}.fileprovider\"\n            )\n        mediaStoreCompat = MediaStoreCompat(this, null)\n        mediaStoreCompat?.setCaptureStrategy(captureStrategy)\n    }\n\n    private fun openMatisse() {\n        selectionCreator?.forResult(ConstValue.REQUEST_CODE_CHOOSE)\n    }\n\n    private fun setEditText() {\n        if (!mediaTypeExclusive) {\n            maxImageCount = formatStrTo0(ev_max_1.text.toString())\n            maxVideoCount = formatStrTo0(ev_max_2.text.toString())\n        } else {\n            if (!isSingleChoose) {\n                maxCount = formatStrTo0(ev_max_1.text.toString())\n            }\n        }\n\n        if (isColumnNum) {\n            spanCount = formatStrTo0(ev_column.text.toString())\n            gridSizePx = 0\n        } else {\n            gridSizePx = formatStrTo0(ev_column.text.toString())\n            spanCount = 0\n        }\n    }\n\n    private fun formatStrTo0(s: String?): Int {\n        if (s == null || s.toString() == \"\") {\n            Toast.makeText(this, \"请保证所有输入非0非空，否则崩溃\", Toast.LENGTH_SHORT).show()\n            return 0\n        }\n        return s.toString().toInt()\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (resultCode != Activity.RESULT_OK) return\n\n        when (requestCode) {\n            ConstValue.REQUEST_CODE_CHOOSE -> doActivityResultForChoose(data)\n            ConstValue.REQUEST_CODE_CAPTURE -> doActivityResultForCapture()\n            ConstValue.REQUEST_CODE_CROP -> doActivityResultForCrop(data)\n        }\n    }\n\n    private fun doActivityResultForChoose(data: Intent?) {\n        if (data == null) return\n        // 获取uri返回值  裁剪结果不返回uri\n        val uriList = Matisse.obtainResult(data)\n        // 获取文件路径返回值\n        selectedPathIds = Matisse.obtainPathIdResult(data)\n\n        uriList?.apply {\n            Glide.with(this@ExampleActivity).load(this[0]).into(iv_image)\n        }\n\n        showPictureResult(uriList, uriList)\n    }\n\n    private fun doActivityResultForCapture() {\n        mediaStoreCompat?.getCurrentPhotoUri()?.apply {\n            if (isCrop) {\n                gotoImageCrop(this@ExampleActivity, arrayListOf(this))\n            } else {\n                showCompressedPath(this)\n            }\n        }\n    }\n\n    private fun doActivityResultForCrop(data: Intent?) {\n        data?.run {\n            Matisse.obtainCropResult(data)?.let {\n                showCompressedPath(it)\n            }\n        }\n    }\n\n    private fun showCompressedPath(path: Uri) {\n        showPictureResult(null, arrayListOf(path))\n        Glide.with(this).load(path).into(iv_image)\n    }\n\n    private fun showPictureResult(\n        uriList: List<Uri>?, strList: List<Uri>?\n    ) {\n        var string = \"uri 路径集合：\\n\"\n\n        uriList?.forEach {\n            string += it.toString() + \"\\n\"\n        }\n\n        string += \"\\npath 路径集合：\\n\"\n\n        strList?.forEach {\n            string += it.toString() + \"\\n\"\n        }\n\n        text.text = \"\\n\\n$string\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/FrescoEngine.kt",
    "content": "package com.leo.matisse\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport androidx.core.view.ViewCompat\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport com.facebook.drawee.backends.pipeline.Fresco\nimport com.facebook.drawee.generic.GenericDraweeHierarchy\nimport com.facebook.drawee.generic.GenericDraweeHierarchyBuilder\nimport com.facebook.drawee.interfaces.DraweeController\nimport com.facebook.drawee.view.DraweeHolder\nimport com.facebook.imagepipeline.common.ResizeOptions\nimport com.facebook.imagepipeline.request.ImageRequestBuilder\nimport com.matisse.engine.ImageEngine\n\n/**\n * Describe :\n * Created by Leo on 2018/10/9 on 15:07.\n */\nclass FrescoEngine : ImageEngine {\n\n    override fun loadThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n        var hierarchy: GenericDraweeHierarchy? = null\n        val hierarchyBuilder =\n            GenericDraweeHierarchyBuilder.newInstance(imageView.context.resources)\n        var draweeHolder: DraweeHolder<*>? =\n            imageView.getTag(R.id.fresco_drawee) as DraweeHolder<*>?\n\n        hierarchyBuilder.placeholderImage = placeholder\n        hierarchyBuilder.failureImage = placeholder\n\n        if (hierarchy == null) {\n            hierarchy = hierarchyBuilder.build()\n        }\n\n        val controllerBuilder =\n            Fresco.newDraweeControllerBuilder().setUri(uri).setAutoPlayAnimations(true)\n\n        val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(uri)\n        imageRequestBuilder.resizeOptions = ResizeOptions(resize, resize)\n\n        val request = imageRequestBuilder.build()\n        controllerBuilder.imageRequest = request\n        val controller: DraweeController\n\n        if (draweeHolder == null) {\n            draweeHolder = DraweeHolder.create(hierarchy, context)\n            controller = controllerBuilder.build()\n        } else {\n            controller = controllerBuilder.setOldController(draweeHolder.controller).build()\n        }\n\n        draweeHolder?.controller = controller\n\n        if (ViewCompat.isAttachedToWindow(imageView)) {\n            draweeHolder?.onAttach()\n        }\n\n        imageView.setTag(R.id.fresco_drawee, draweeHolder)\n        imageView.setImageDrawable(draweeHolder?.topLevelDrawable)\n    }\n\n    override fun loadGifThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n    }\n\n    override fun loadImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n        var hierarchy: GenericDraweeHierarchy? = null\n        val hierarchyBuilder =\n            GenericDraweeHierarchyBuilder.newInstance(imageView.context.resources)\n        var draweeHolder: DraweeHolder<*>? =\n            imageView.getTag(R.id.fresco_drawee) as DraweeHolder<*>?\n\n        if (hierarchy == null) {\n            hierarchy = hierarchyBuilder.build()\n        }\n\n        val controllerBuilder =\n            Fresco.newDraweeControllerBuilder().setUri(uri).setAutoPlayAnimations(true)\n\n        var params: ViewGroup.LayoutParams? = imageView.layoutParams\n        if (params == null) {\n            params = ViewGroup.LayoutParams(resizeX, resizeY)\n        }\n\n        if (params.width == ViewGroup.LayoutParams.WRAP_CONTENT) {\n            params.width = ViewGroup.LayoutParams.MATCH_PARENT\n        }\n        if (params.height == ViewGroup.LayoutParams.WRAP_CONTENT) {\n            params.height = ViewGroup.LayoutParams.MATCH_PARENT\n        }\n\n        imageView.layoutParams = params\n\n        val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(uri)\n        val request = imageRequestBuilder.build()\n        controllerBuilder.imageRequest = request\n        val controller: DraweeController\n\n        if (draweeHolder == null) {\n            draweeHolder = DraweeHolder.create(hierarchy, context)\n            controller = controllerBuilder.build()\n        } else {\n            controller = controllerBuilder.setOldController(draweeHolder.controller).build()\n        }\n\n        // 请求\n        draweeHolder?.controller = controller\n\n        if (ViewCompat.isAttachedToWindow(imageView)) {\n            draweeHolder?.onAttach()\n        }\n\n        imageView.setTag(R.id.fresco_drawee, draweeHolder)\n        imageView.setImageDrawable(draweeHolder?.topLevelDrawable)\n    }\n\n    override fun loadGifImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n    }\n\n    override fun cleanMemory(context: Context) {\n        Fresco.getImagePipeline().clearMemoryCaches()\n    }\n\n    override fun pause(context: Context) {\n        Fresco.getImagePipeline().pause()\n    }\n\n    override fun resume(context: Context) {\n        Fresco.getImagePipeline().resume()\n    }\n\n    override fun init(context: Context) {\n        Fresco.initialize(context)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/Glide4Engine.kt",
    "content": "package com.leo.matisse\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.Looper\nimport android.widget.ImageView\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.Priority\nimport com.bumptech.glide.request.RequestOptions\nimport com.matisse.engine.ImageEngine\n\n/**\n * [ImageEngine] implementation using Glide.\n */\nclass Glide4Engine : ImageEngine {\n\n    override fun cleanMemory(context: Context) {\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            Glide.get(context).clearMemory()\n        }\n    }\n\n    override fun pause(context: Context) {\n        Glide.with(context).pauseRequests()\n    }\n\n    override fun resume(context: Context) {\n        Glide.with(context).resumeRequests()\n    }\n\n    override fun init(context: Context) {\n    }\n\n    override fun loadThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asBitmap() // some .jpeg files are actually gif\n            .load(uri)\n            .apply(\n                RequestOptions()\n                    .override(resize, resize).dontAnimate()\n                    .placeholder(placeholder).centerCrop()\n            )\n            .into(imageView)\n    }\n\n    override fun loadGifThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asBitmap() // some .jpeg files are actually gif\n            .load(uri)\n            .apply(\n                RequestOptions().override(resize, resize).placeholder(placeholder).centerCrop()\n            )\n            .into(imageView)\n    }\n\n    override fun loadImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .load(uri)\n            .apply(\n                RequestOptions()\n                    .override(resizeX, resizeY).priority(Priority.HIGH).fitCenter()\n            )\n            .into(imageView)\n    }\n\n    override fun loadGifImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asGif()\n            .load(uri)\n            .apply(\n                RequestOptions().override(resizeX, resizeY).priority(Priority.HIGH).fitCenter()\n            )\n            .into(imageView)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/GlideEngine.kt",
    "content": "package com.leo.matisse\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.Looper\nimport android.widget.ImageView\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.Priority\nimport com.bumptech.glide.request.RequestOptions\nimport com.matisse.engine.ImageEngine\n\n/**\n * Describe : implementation using Glide.\n * Created by Leo on 2018/9/7 on 10:55.\n */\nclass GlideEngine : ImageEngine {\n\n    override fun cleanMemory(context: Context) {\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            Glide.get(context).clearMemory()\n        }\n    }\n\n    override fun pause(context: Context) {\n        Glide.with(context).pauseRequests()\n    }\n\n    override fun resume(context: Context) {\n        Glide.with(context).resumeRequests()\n    }\n\n    override fun init(context: Context) {\n    }\n\n    override fun loadThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asBitmap()  // some .jpeg files are actually gif\n            .load(uri)\n            .apply(\n                RequestOptions().placeholder(placeholder)\n                    .override(resize, resize)\n                    .centerCrop()\n            )\n            .into(imageView)\n    }\n\n    override fun loadGifThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asBitmap()\n            .load(uri)\n            .apply(\n                RequestOptions().placeholder(placeholder)\n                    .override(resize, resize)\n                    .centerCrop()\n            )\n            .into(imageView)\n    }\n\n    override fun loadImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .load(uri)\n            .apply(\n                RequestOptions().priority(Priority.HIGH)\n                    .fitCenter()\n            )\n            .into(imageView)\n    }\n\n    override fun loadGifImage(\n        context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?\n    ) {\n        Glide.with(context)\n            .asGif()\n            .load(uri)\n            .apply(\n                RequestOptions().priority(Priority.HIGH)\n                    .override(resizeX, resizeY)\n            )\n            .into(imageView)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/ImageSizeFilter.kt",
    "content": "package com.leo.matisse\n\nimport android.content.Context\nimport com.matisse.MimeTypeManager\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.filter.Filter\nimport com.matisse.utils.PhotoMetadataUtils\n\n/**\n * desc：不允许选择大于itemSize字节的图片</br>\n * time: 2019/10/23-10:12</br>\n * author：Leo </br>\n * since V 1.2.2 </br>\n */\nclass ImageSizeFilter(private var itemSize: Long) : Filter() {\n\n    // 设置需过滤的资源类型,此处过滤类型为图片\n    override fun constraintTypes() = MimeTypeManager.ofImage()\n\n    override fun filter(context: Context, item: Item?): IncapableCause? {\n        // 1. 判断当前选中的item是否属于constraintTypes中定义的图片资源\n        if (!needFiltering(context, item)) return null\n\n        if (item?.size ?: 0 > itemSize) {\n            return IncapableCause(\n                IncapableCause.DIALOG, \"需选择小于${PhotoMetadataUtils.getSizeInMB(itemSize)}M的图片\"\n            )\n        }\n\n        return null\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/MainActivity.kt",
    "content": "package com.leo.matisse\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.pm.ActivityInfo\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.AppCompatButton\nimport com.bumptech.glide.Glide\nimport com.matisse.Matisse\nimport com.matisse.MimeTypeManager\nimport com.matisse.entity.CaptureStrategy\nimport com.matisse.entity.ConstValue\nimport com.matisse.utils.Platform\nimport com.tbruyelle.rxpermissions2.RxPermissions\nimport kotlinx.android.synthetic.main.activity_main.*\n\nclass MainActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        findViewById<AppCompatButton>(R.id.btn_media_store).setOnClickListener {\n            RxPermissions(this@MainActivity)\n                .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)\n                .subscribe {\n                    if (!it) {\n                        Toast.makeText(\n                            this@MainActivity, R.string.permission_request_denied, Toast.LENGTH_LONG\n                        ).show()\n                        return@subscribe\n                    }\n                    openMatisse()\n                }\n        }\n    }\n\n    private fun openMatisse() {\n        Matisse.from(this@MainActivity)\n            .choose(MimeTypeManager.ofAll())\n            .countable(false)\n            .capture(true)\n            .isCrop(true)\n            .isCircleCrop(true)\n            .maxSelectable(1)\n            .theme(R.style.JCStyle)\n            .captureStrategy(\n                CaptureStrategy(\n                    true,\n                    \"${Platform.getPackageName(this@MainActivity)}.fileprovider\"\n                )\n            )\n            .thumbnailScale(0.8f)\n            .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)\n            .imageEngine(Glide4Engine())\n            .forResult(ConstValue.REQUEST_CODE_CHOOSE)\n    }\n\n    private fun showToast(value: String) {\n        Toast.makeText(this, value, Toast.LENGTH_SHORT).show()\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (data == null) return\n\n        if (requestCode == ConstValue.REQUEST_CODE_CHOOSE && resultCode == Activity.RESULT_OK) {\n            var string = \"\"\n            val uriList = Matisse.obtainResult(data) ?: return\n\n            uriList.forEach {\n                string += it.toString() + \"\\n\"\n            }\n\n            Glide.with(this).load(uriList[0]).into(iv_image)\n            string = \"\\n\\n$string\"\n\n            text.text = \"\\n\\n$string\"\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/leo/matisse/SizeFilter.kt",
    "content": "package com.leo.matisse\n\nimport android.content.Context\nimport com.matisse.MimeType\nimport com.matisse.MimeTypeManager\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.filter.Filter\nimport com.matisse.utils.PhotoMetadataUtils\n\nclass SizeFilter(private val maxSizeByte: Int) : Filter() {\n\n    override fun constraintTypes(): Set<MimeType> {\n        return MimeTypeManager.ofMotionlessImage()\n    }\n\n    override fun filter(context: Context, item: Item?): IncapableCause? {\n        if (!needFiltering(context, item))\n            return null\n        return if (item?.size ?: 0 > maxSizeByte) {\n            IncapableCause(\n                IncapableCause.TOAST,\n                \"not larger than ${PhotoMetadataUtils.getSizeInMB(maxSizeByte.toLong())} MB\"\n            )\n        } else null\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillColor=\"#26A69A\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-xhdpi/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_example.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fillViewport=\"true\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:padding=\"15dp\">\n\n        <!--显示资源类型-->\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:id=\"@+id/tv_show_resource\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingTop=\"5dp\"\n                android:text=\"显示资源\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <RadioGroup\n                android:id=\"@+id/rg_show\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"15dp\"\n                android:orientation=\"horizontal\"\n                app:layout_constraintStart_toEndOf=\"@+id/tv_show_resource\"\n                app:layout_constraintTop_toTopOf=\"@+id/tv_show_resource\">\n\n                <RadioButton\n                    android:id=\"@+id/btnAll\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"全部资源\" />\n\n                <RadioButton\n                    android:id=\"@+id/btnVideo\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"仅视频\" />\n\n                <RadioButton\n                    android:id=\"@+id/btnImage\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"仅图片\" />\n            </RadioGroup>\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <!--指定组合类型-->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\">\n\n            <TextView\n                android:id=\"@+id/tv_show_customize\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingTop=\"10dp\"\n                android:text=\"自定义显示\"\n                android:textColor=\"@color/black\" />\n\n            <androidx.constraintlayout.widget.ConstraintLayout\n                android:id=\"@+id/view_show_customize\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\">\n\n                <CheckBox\n                    android:id=\"@+id/chb_jpeg\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"JPEG\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_png\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginEnd=\"50dp\"\n                    android:text=\"PNG\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_gif\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"GIF\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_jpeg\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_bmp\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"BMP\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_png\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_webp\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"WEBP\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_gif\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_mpeg\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"MPEG\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_bmp\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_mp4\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"MP4\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_webp\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_quick_time\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"QUICKTIME\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_mpeg\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_threegpp\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"GPP\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_mp4\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_threegpp2\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"GPP2\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_quick_time\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_mkv\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"MKV\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_threegpp\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_webm\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"WEBM\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_threegpp2\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_ts\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"TS\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_jpeg\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_mkv\" />\n\n                <CheckBox\n                    android:id=\"@+id/chb_avi\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"AVI\"\n                    app:layout_constraintStart_toStartOf=\"@+id/chb_png\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/chb_webm\" />\n\n            </androidx.constraintlayout.widget.ConstraintLayout>\n\n        </LinearLayout>\n\n        <!--设置选择类型-->\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:id=\"@+id/tv_media_exclusive\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingTop=\"8dp\"\n                android:text=\"混合/单一\\n是否可同时\\n选择图片和\\n视频\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <RadioGroup\n                android:id=\"@+id/rg_media_exclusive\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                app:layout_constraintStart_toEndOf=\"@+id/tv_media_exclusive\"\n                app:layout_constraintTop_toTopOf=\"@+id/tv_media_exclusive\">\n\n                <RadioButton\n                    android:id=\"@+id/btnMixed\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"混合选择\" />\n\n                <RadioButton\n                    android:id=\"@+id/btnExclusive\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"单一选择\" />\n\n            </RadioGroup>\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <!--单多选、计数-->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_choose_type\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"单/多选\" />\n\n            <androidx.appcompat.widget.SwitchCompat\n                android:id=\"@+id/switch_choose_type\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"30dp\"\n                android:layout_marginStart=\"25dp\"\n                android:checked=\"false\"\n                android:textOff=\"多选\"\n                android:textOn=\"单选\"\n                android:textSize=\"10sp\"\n                app:showText=\"true\" />\n\n            <TextView\n                android:id=\"@+id/tv_check_type\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"50dp\"\n                android:text=\"是否计数\" />\n\n            <androidx.appcompat.widget.SwitchCompat\n                android:id=\"@+id/switch_check_type\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"30dp\"\n                android:layout_marginStart=\"10dp\"\n                android:checked=\"false\"\n                android:textOff=\"是\"\n                android:textOn=\"否\"\n                android:textSize=\"10sp\"\n                app:showText=\"true\" />\n\n        </LinearLayout>\n\n        <!--开启拍照、内部压缩-->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_capture\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"开启拍照\" />\n\n            <androidx.appcompat.widget.SwitchCompat\n                android:id=\"@+id/switch_capture\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"30dp\"\n                android:layout_marginStart=\"15dp\"\n                android:checked=\"false\"\n                android:textOff=\"关\"\n                android:textOn=\"开\"\n                android:textSize=\"10sp\"\n                app:showText=\"true\" />\n\n        </LinearLayout>\n\n        <!--设置主题-->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_theme\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"默认主题\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/tv_capture\" />\n\n            <RadioGroup\n                android:id=\"@+id/rg_theme\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"15dp\"\n                android:orientation=\"horizontal\"\n                app:layout_constraintStart_toStartOf=\"@+id/rg_show\"\n                app:layout_constraintTop_toTopOf=\"@+id/tv_theme\">\n\n                <RadioButton\n                    android:id=\"@+id/btn_normal_theme\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"默认主题\" />\n\n                <RadioButton\n                    android:id=\"@+id/btn_customize_theme\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"自定义主题\" />\n\n                <RadioButton\n                    android:id=\"@+id/btn_jc_theme\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"JC主题\" />\n\n            </RadioGroup>\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_crop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <!--裁剪-->\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/tv_crop\"\n                    style=\"@style/style_title_text\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"开启裁剪\" />\n\n                <androidx.appcompat.widget.SwitchCompat\n                    android:id=\"@+id/switch_crop\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"30dp\"\n                    android:layout_marginStart=\"15dp\"\n                    android:checked=\"false\"\n                    android:textOff=\"关\"\n                    android:textOn=\"开\"\n                    android:textSize=\"10sp\"\n                    app:showText=\"true\" />\n\n            </LinearLayout>\n\n            <!--裁剪类型、保存类型-->\n            <LinearLayout\n                android:id=\"@+id/ll_crop_type\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/tv_crop_type\"\n                    style=\"@style/style_title_text\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"裁剪类型\" />\n\n                <androidx.appcompat.widget.SwitchCompat\n                    android:id=\"@+id/switch_crop_type\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"30dp\"\n                    android:layout_marginStart=\"15dp\"\n                    android:checked=\"false\"\n                    android:textOff=\"方\"\n                    android:textOn=\"圆\"\n                    android:textSize=\"10sp\"\n                    app:showText=\"true\" />\n\n            </LinearLayout>\n\n        </LinearLayout>\n\n        <!--设置列数-->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_column\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"设置列数\" />\n\n            <RadioGroup\n                android:id=\"@+id/rg_column\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"15dp\"\n                android:orientation=\"horizontal\">\n\n                <RadioButton\n                    android:id=\"@+id/btn_num_column\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"列表列数\" />\n\n                <RadioButton\n                    android:id=\"@+id/btn_size_column\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"列表宽度\" />\n\n            </RadioGroup>\n\n        </LinearLayout>\n\n        <EditText\n            android:id=\"@+id/ev_column\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"50dp\"\n            android:inputType=\"number\"\n            android:text=\"3\"\n            android:textSize=\"14sp\" />\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <EditText\n                android:id=\"@+id/ev_max_1\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"50dp\"\n                android:layout_marginTop=\"20dp\"\n                android:inputType=\"number\"\n                android:text=\"3\"\n                android:textSize=\"14sp\"\n                android:visibility=\"gone\"\n                app:layout_constraintEnd_toStartOf=\"@+id/ev_max_2\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/tv_max_1\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"最大可选择数\"\n                app:layout_constraintBottom_toTopOf=\"@+id/ev_max_1\"\n                app:layout_constraintEnd_toEndOf=\"@+id/ev_max_1\"\n                app:layout_constraintStart_toStartOf=\"@+id/ev_max_1\" />\n\n            <TextView\n                android:id=\"@+id/tv_max_2\"\n                style=\"@style/style_title_text\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"视频最大选择数\"\n                app:layout_constraintBottom_toTopOf=\"@+id/ev_max_2\"\n                app:layout_constraintEnd_toEndOf=\"@+id/ev_max_2\"\n                app:layout_constraintStart_toStartOf=\"@+id/ev_max_2\" />\n\n            <EditText\n                android:id=\"@+id/ev_max_2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"50dp\"\n                android:inputType=\"number\"\n                android:text=\"1\"\n                android:textSize=\"14sp\"\n                android:visibility=\"gone\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toEndOf=\"@+id/ev_max_1\"\n                app:layout_constraintTop_toTopOf=\"@+id/ev_max_1\"\n                tools:visibility=\"gone\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <!--操作按钮相册、相机-->\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"80dp\">\n\n            <androidx.appcompat.widget.AppCompatButton\n                android:id=\"@+id/btn_open_matisse\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"start\"\n                android:text=\"打开相册\" />\n\n            <androidx.appcompat.widget.AppCompatButton\n                android:id=\"@+id/btn_open_capture\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"end\"\n                android:layout_marginStart=\"50dp\"\n                android:text=\"打开相机\" />\n\n        </LinearLayout>\n\n        <!--结果显示-->\n        <ImageView\n            android:id=\"@+id/iv_image\"\n            android:layout_width=\"150dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:adjustViewBounds=\"true\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/btn_open_matisse\" />\n\n        <TextView\n            android:id=\"@+id/text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"15dp\"\n            android:lineSpacingExtra=\"3dp\"\n            android:textColor=\"@color/color_title_text\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/iv_image\" />\n\n    </LinearLayout>\n</androidx.core.widget.NestedScrollView>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.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:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <ImageView\n        android:id=\"@+id/iv_image\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"150dp\"\n        android:adjustViewBounds=\"true\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <androidx.appcompat.widget.AppCompatButton\n        android:id=\"@+id/btn_media_store\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:foreground=\"?selectableItemBackground\"\n        android:gravity=\"center\"\n        android:text=\"媒体库\"\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\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"15dp\"\n        android:lineSpacingExtra=\"3dp\"\n        android:textColor=\"@color/color_title_text\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "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=\"@drawable/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=\"@drawable/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=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n\n    <color name=\"preview_bottom_size\">#61FFFFFF</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/colors_dracula.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  Copyright 2017 Zhihu Inc.\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<resources>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"fresco_drawee\" type=\"id\"></item>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MatisseKotlin</string>\n    <string name=\"permission_request_denied\">请求读写权限失败!</string>\n\n    <string name=\"back\">Media.Back</string>\n    <string name=\"sure\">Sure</string>\n    <string name=\"preview\">Media.Preview</string>\n    <string name=\"original\">Media.Original</string>\n    <string name=\"camera\">Media.Camera</string>\n    <string name=\"album\">Media.Album</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    <style name=\"style_title_text\">\n        <item name=\"android:textColor\">#000000</item>\n        <item name=\"android:textSize\">16sp</item>\n        <item name=\"android:textStyle\">bold</item>\n    </style>\n\n    <style name=\"CustomMatisseStyle\" parent=\"Matisse.Default\">\n        <item name=\"Media_Back_text\">@string/back</item>\n        <item name=\"Media_Sure_text\">@string/sure</item>\n        <item name=\"Media_Preview_text\">@string/preview</item>\n        <item name=\"Media_Original_text\">@string/original</item>\n        <item name=\"Media_Album_text\">@string/album</item>\n        <item name=\"Media_Camera_text\">@string/camera</item>\n        <item name=\"Media_Sure_textColor\">#00ff00</item>\n        <item name=\"Media_Album_textColor\">#0000ff</item>\n        <item name=\"Media_Preview_textColor\">#ff0000</item>\n        <item name=\"Item_placeholder\">@drawable/ic_launcher_foreground</item>\n    </style>\n\n    <style name=\"JCStyle\" parent=\"Matisse.Default\">\n        <item name=\"colorPrimary\">#EB5148</item>\n        <item name=\"colorPrimaryDark\">#EB5148</item>\n        <item name=\"Navigation_bg\">#EB5148</item>\n        <item name=\"Navigation_backRes\">@mipmap/ic_navbar48_chervon_white</item>\n        <item name=\"Media_Sure_text\">@string/sure</item>\n        <item name=\"Media_Sure_textColor\">@color/selector_white_text</item>\n        <item name=\"Media_Preview_text\">@string/preview</item>\n        <item name=\"Media_Album_text\">@string/album</item>\n        <item name=\"Media_Camera_text\">@string/camera</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/file_paths_public.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"my_images\"\n        path=\"Pictures\" />\n</paths>"
  },
  {
    "path": "app/src/test/java/com/leo/matisse/ExampleUnitTest.kt",
    "content": "package com.leo.matisse\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    ext.kotlin_version = '1.3.50'\n\n    repositories {\n        google()\n        jcenter()\n        mavenCentral()\n    }\n    dependencies {\n        classpath \"com.android.tools.build:gradle:3.4.1\"\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath 'com.novoda:bintray-release:0.9.1'\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n        mavenCentral()\n        maven { url \"https://jitpack.io\" }\n    }\n}\n\next {\n    compileSdkVersion = 29\n\n    minSdkVersion = 19\n    targetSdkVersion = 29\n\n    appcompat = '1.0.0'\n    material = '1.0.0'\n    recyclerview = '1.0.0'\n    glide = '4.7.1'\n    constraintlayout = '1.1.3'\n}\n\ntasks.withType(Javadoc) {\n    options.addStringOption('Xdoclint:none', '-quiet')\n    options.addStringOption('encoding', 'UTF-8')\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\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# 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 sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\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\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\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\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\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\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\" -a \"$nonstop\" = \"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# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\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\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\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\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 Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_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=%*\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": "matisse/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\napply plugin: 'com.novoda.bintray-release'\n\nProperties properties = new Properties()\nproperties.load(project.rootProject.file('local.properties').newDataInputStream())\n\nString localBintrayUser = properties.getProperty(\"bintray.user\")\nString localBintrayApikey = properties.getProperty(\"bintray.apikey\")\n\npublish {\n    bintrayUser = localBintrayUser   //bintray.com用户名\n    bintrayKey = localBintrayApikey  //bintray.com apikey\n    dryRun = false\n    repoName = 'MatisseKotlin'\n    userOrg = 'nfleo'//bintray.com用户名\n    groupId = 'com.nfleo'//jcenter上的路径\n    artifactId = 'MatisseKotlin'//项目名称\n    publishVersion = '2.1.1'//版本号\n    desc = 'this is for MatisseKotlin'\n    website = 'https://github.com/NFLeo/Matisse-Kotlin'\n}\n\nandroid {\n    compileSdkVersion rootProject.ext.compileSdkVersion\n\n    defaultConfig {\n        minSdkVersion rootProject.ext.minSdkVersion\n        targetSdkVersion rootProject.ext.targetSdkVersion\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n\n        vectorDrawables.useSupportLibrary = true\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    lintOptions {\n        abortOnError false\n    }\n\n    compileOptions {\n        /* Java8支持 */\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8\n    }\n}\n\nandroidExtensions {\n    experimental = true\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    testImplementation 'junit:junit:4.12'\n    androidTestImplementation 'androidx.test:runner:1.2.0'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    /*ImageViewTouch*/\n    implementation \"it.sephiroth.android.library.imagezoom:library:1.0.4\"\n    implementation \"androidx.appcompat:appcompat:$rootProject.ext.appcompat\"\n    implementation \"com.google.android.material:material:$rootProject.ext.material\"\n    implementation \"androidx.recyclerview:recyclerview:$rootProject.ext.recyclerview\"\n    implementation \"androidx.constraintlayout:constraintlayout:$rootProject.ext.constraintlayout\"\n}\n\ntasks.withType(JavaCompile) {\n    options.encoding = \"UTF-8\"\n}"
  },
  {
    "path": "matisse/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": "matisse/src/androidTest/java/com/matisse/ExampleInstrumentedTest.java",
    "content": "package com.matisse;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"com.matisse.test\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.matisse\">\n\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.CAMERA\" />\n\n    <application>\n        <activity android:name=\".ui.activity.SelectedPreviewActivity\" />\n        <activity android:name=\".ui.activity.AlbumPreviewActivity\" />\n        <activity android:name=\".ui.activity.matisse.MatisseActivity\" />\n        <activity android:name=\".ucrop.UCropActivity\" />\n        <activity android:name=\".ucrop.PictureMultiCuttingActivity\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/Matisse.kt",
    "content": "package com.matisse\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.fragment.app.Fragment\nimport com.matisse.entity.ConstValue\nimport java.lang.ref.WeakReference\n\n/**\n * Entry for Matisse's media selection.\n */\nclass Matisse(activity: Activity?, fragment: Fragment? = null) {\n\n    companion object {\n\n        /**\n         * Start Matisse from an Activity.\n         * This Activity's [Activity.onActivityResult] will be called when user\n         * finishes selecting.\n         *\n         * @param activity Activity instance.\n         * @return Matisse instance.\n         */\n        fun from(activity: Activity?): Matisse {\n            return Matisse(activity)\n        }\n\n        /**\n         * Start Matisse from a Fragment.\n         *\n         * This Fragment's [Fragment.onActivityResult] will be called when user\n         * finishes selecting.\n         *\n         * @param fragment Fragment instance.\n         * @return Matisse instance.\n         */\n        fun from(fragment: Fragment): Matisse {\n            return Matisse(fragment)\n        }\n\n        /**\n         * Obtain user selected media' [Uri] list in the starting Activity or Fragment.\n         *\n         * @param data Intent passed by [Activity.onActivityResult] or\n         * [Fragment.onActivityResult].\n         * @return User selected media' [Uri] list.\n         */\n        fun obtainResult(data: Intent): List<Uri>? {\n            return data.getParcelableArrayListExtra(ConstValue.EXTRA_RESULT_SELECTION)\n        }\n\n        /**\n         * Obtain user selected media path id list in the starting Activity or Fragment.\n         *\n         * @param data Intent passed by [Activity.onActivityResult] or\n         * [Fragment.onActivityResult].\n         * @return User selected media path id list.\n         */\n        fun obtainPathIdResult(data: Intent): List<String>? {\n            return data.getStringArrayListExtra(ConstValue.EXTRA_RESULT_SELECTION_ID)\n        }\n\n        /**\n         * 直接获取裁剪结果\n         */\n        fun obtainCropResult(data: Intent?): Uri? {\n            return data?.getParcelableExtra(ConstValue.EXTRA_RESULT_CROP_BACK_BUNDLE)\n        }\n\n        /**\n         * Obtain state whether user decide to use selected media in original\n         *\n         * @param data Intent passed by [Activity.onActivityResult] or\n         * [Fragment.onActivityResult].\n         * @return Whether use original photo\n         */\n        fun obtainOriginalState(data: Intent) =\n            data.getBooleanExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, false)\n    }\n\n    private val mContext = WeakReference(activity)\n    private val mFragment: WeakReference<Fragment>?\n\n    internal val activity: Activity?\n        get() = mContext.get()\n\n    internal val fragment: Fragment?\n        get() = mFragment?.get()\n\n    private constructor(fragment: Fragment) : this(fragment.activity, fragment)\n\n    init {\n        mFragment = WeakReference<Fragment>(fragment)\n    }\n\n    /**\n     * MIME types the selection constrains on.\n     * Types not included in the set will still be shown in the grid but can't be chosen.\n     *\n     * @param mimeTypes MIME types set user can choose from.\n     * @return [SelectionCreator] to build select specifications.\n     * @see MimeType\n     *\n     * @see SelectionCreator\n     */\n    fun choose(mimeTypes: Set<MimeType>): SelectionCreator {\n        return this.choose(mimeTypes, true)\n    }\n\n    /**\n     * MIME types the selection constrains on.\n     * Types not included in the set will still be shown in the grid but can't be chosen.\n     *\n     * @param mimeTypes          MIME types set user can choose from.\n     * @param mediaTypeExclusive Whether can choose images and videos at the same time during one single choosing\n     * process. true corresponds to not being able to choose images and videos at the same\n     * time, and false corresponds to being able to do this.\n     * @return [SelectionCreator] to build select specifications.\n     * @see MimeType\n     *\n     * @see SelectionCreator\n     */\n    fun choose(mimeTypes: Set<MimeType>, mediaTypeExclusive: Boolean) =\n        SelectionCreator(this, mimeTypes, mediaTypeExclusive)\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/MimeType.kt",
    "content": "package com.matisse\n\n/**\n * Describe : MIME Type enumeration to restrict selectable media on the selection activity.\n * Matisse only supports images and videos.\n * Created by Leo on 2018/8/29 on 14:55.\n */\nenum class MimeType {\n\n    // ============== images ==============\n\n    JPG {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"jpg\", \"jpeg\")\n        override fun getKey() = \"image/jpg\"\n    },\n\n    JPEG {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"jpg\", \"jpeg\")\n        override fun getKey() = \"image/jpeg\"\n    },\n\n    PNG {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"png\")\n        override fun getKey() = \"image/png\"\n    },\n\n    GIF {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"gif\")\n        override fun getKey() = \"image/gif\"\n    },\n\n    BMP {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"bmp\")\n        override fun getKey() = \"image/x-ms-bmp\"\n    },\n\n    WEBP {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"webp\")\n        override fun getKey() = \"image/webp\"\n    },\n\n    // ============== videos ==============\n    MPEG {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"mpg\")\n        override fun getKey() = \"video/mpeg\"\n    },\n\n    MP4 {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"m4v\", \"mp4\")\n        override fun getKey() = \"video/mp4\"\n    },\n\n    QUICKTIME {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"mov\")\n        override fun getKey() = \"video/quicktime\"\n    },\n\n    THREEGPP {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"3gp\", \"3gpp\")\n        override fun getKey() = \"video/3gpp\"\n    },\n\n    THREEGPP2 {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"3g2\", \"3gpp2\")\n        override fun getKey() = \"video/3gpp2\"\n    },\n\n    MKV {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"mkv\")\n        override fun getKey() = \"video/x-matroska\"\n    },\n\n    WEBM {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"webm\")\n        override fun getKey() = \"video/webm\"\n    },\n\n    TS {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"ts\")\n        override fun getKey() = \"video/mp2ts\"\n    },\n\n    AVI {\n        override fun getValue() = MimeTypeManager.arraySetOf(\"avi\")\n        override fun getKey() = \"video/avi\"\n    };\n\n    abstract fun getValue(): Set<String>\n    abstract fun getKey(): String\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/MimeTypeManager.kt",
    "content": "package com.matisse\n\nimport android.content.Context\nimport android.net.Uri\nimport android.text.TextUtils\nimport android.webkit.MimeTypeMap\nimport androidx.collection.ArraySet\nimport com.matisse.utils.PhotoMetadataUtils\nimport com.matisse.utils.getRealFilePath\nimport java.util.*\n\n/**\n * Describe : Define MediaType\n * Created by Leo on 2018/8/29 on 15:02.\n */\nclass MimeTypeManager {\n\n    companion object {\n        fun ofAll(): EnumSet<MimeType> = EnumSet.allOf(MimeType::class.java)\n\n        fun of(first: MimeType, others: Array<MimeType>): EnumSet<MimeType> =\n            EnumSet.of(first, *others)\n\n        fun ofImage(): EnumSet<MimeType> = EnumSet.of(\n            MimeType.JPEG, MimeType.JPG, MimeType.PNG, MimeType.GIF, MimeType.BMP, MimeType.WEBP\n        )\n\n        // 静态图\n        fun ofMotionlessImage(): EnumSet<MimeType> = EnumSet.of(\n            MimeType.JPEG, MimeType.JPG, MimeType.PNG, MimeType.BMP\n        )\n\n        fun ofVideo(): EnumSet<MimeType> = EnumSet.of(\n            MimeType.MPEG, MimeType.MP4, MimeType.QUICKTIME, MimeType.THREEGPP, MimeType.THREEGPP2,\n            MimeType.MKV, MimeType.WEBM, MimeType.TS, MimeType.AVI\n        )\n\n        fun isImage(mimeType: String?) =\n            isMotionlessImage(mimeType)\n                    || MimeType.GIF.getKey().contains(lowerCaseMimeType(mimeType))\n                    || MimeType.WEBP.getKey().contains(lowerCaseMimeType(mimeType))\n\n        private fun isMotionlessImage(mimeType: String?) =\n            MimeType.JPEG.getKey().contains(lowerCaseMimeType(mimeType))\n                    || MimeType.JPG.getKey().contains(lowerCaseMimeType(mimeType))\n                    || MimeType.PNG.getKey().contains(lowerCaseMimeType(mimeType))\n                    || MimeType.BMP.getKey().contains(lowerCaseMimeType(mimeType))\n\n        fun isVideo(mimeType: String) = MimeType.MPEG.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.MP4.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.QUICKTIME.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.THREEGPP.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.THREEGPP2.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.MKV.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.WEBM.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.TS.getKey().contains(lowerCaseMimeType(mimeType))\n                || MimeType.AVI.getKey().contains(lowerCaseMimeType(mimeType))\n\n        fun isGif(mimeType: String) = MimeType.GIF.getKey().contains(lowerCaseMimeType(mimeType))\n\n        fun arraySetOf(vararg suffixes: String) = ArraySet(mutableListOf(*suffixes))\n\n        fun checkType(context: Context, uri: Uri?, mExtensions: Set<String>): Boolean {\n            val map = MimeTypeMap.getSingleton()\n            if (uri == null) return false\n\n            val type = map.getExtensionFromMimeType(context.contentResolver.getType(uri))\n            var path: String? = null\n            // lazy load the path and prevent resolve for multiple times\n            var pathParsed = false\n            mExtensions.forEach {\n                if (it == type) return true\n\n                if (!pathParsed) {\n                    // we only resolve the path for one time\n                    path = getRealFilePath(context, uri)\n                    if (!TextUtils.isEmpty(path)) path = path?.toLowerCase(Locale.US)\n                    pathParsed = true\n                }\n                if (path != null && path?.endsWith(it) == true) return true\n            }\n            return false\n        }\n\n        private fun lowerCaseMimeType(mimeType: String?) = mimeType?.toLowerCase() ?: \"\"\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/SelectionCreator.kt",
    "content": "package com.matisse\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.ActivityInfo.*\nimport android.os.Build\nimport android.view.View\nimport androidx.annotation.IntDef\nimport androidx.annotation.RequiresApi\nimport androidx.annotation.StyleRes\nimport com.matisse.engine.ImageEngine\nimport com.matisse.entity.CaptureStrategy\nimport com.matisse.filter.Filter\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.listener.OnCheckedListener\nimport com.matisse.listener.OnSelectedListener\nimport com.matisse.ui.activity.BaseActivity\nimport com.matisse.ui.activity.matisse.MatisseActivity\nimport java.io.File\n\n/**\n * Fluent API for building media select specification.\n * Constructs a new specification builder on the context.\n *\n * @param matisse   a requester context wrapper.\n * @param mimeTypes MIME type set to select.\n */\nclass SelectionCreator(\n    private val matisse: Matisse, mimeTypes: Set<MimeType>, mediaTypeExclusive: Boolean\n) {\n    private val selectionSpec: SelectionSpec = SelectionSpec.getCleanInstance()\n\n    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)\n    @IntDef(\n        SCREEN_ORIENTATION_UNSPECIFIED, SCREEN_ORIENTATION_LANDSCAPE,\n        SCREEN_ORIENTATION_PORTRAIT, SCREEN_ORIENTATION_USER, SCREEN_ORIENTATION_BEHIND,\n        SCREEN_ORIENTATION_SENSOR, SCREEN_ORIENTATION_NOSENSOR, SCREEN_ORIENTATION_SENSOR_LANDSCAPE,\n        SCREEN_ORIENTATION_SENSOR_PORTRAIT, SCREEN_ORIENTATION_REVERSE_LANDSCAPE,\n        SCREEN_ORIENTATION_REVERSE_PORTRAIT, SCREEN_ORIENTATION_FULL_SENSOR,\n        SCREEN_ORIENTATION_USER_LANDSCAPE, SCREEN_ORIENTATION_USER_PORTRAIT,\n        SCREEN_ORIENTATION_FULL_USER, SCREEN_ORIENTATION_LOCKED\n    )\n    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)\n    internal annotation class ScreenOrientation\n\n    init {\n        selectionSpec.run {\n            this.mimeTypeSet = mimeTypes\n            this.mediaTypeExclusive = mediaTypeExclusive\n            this.orientation = SCREEN_ORIENTATION_UNSPECIFIED\n        }\n    }\n\n    /**\n     * Theme for media selecting Activity.\n     *\n     * There are two built-in themes:\n     * you can define a custom theme derived from the above ones or other themes.\n     *\n     * @param themeId theme resource id. Default value is R.style.Matisse_Zhihu.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun theme(@StyleRes themeId: Int) = this.apply { selectionSpec.themeId = themeId }\n\n    /**\n     * Show a auto-increased number or a check mark when user select media.\n     *\n     * @param countable true for a auto-increased number from 1, false for a check mark. Default\n     * value is false.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun countable(countable: Boolean) = this.apply { selectionSpec.countable = countable }\n\n    /**\n     * Maximum selectable count.\n     * mediaTypeExclusive true\n     *      use maxSelectable\n     * mediaTypeExclusive false\n     *      use maxImageSelectable and maxVideoSelectable\n     * @param maxSelectable Maximum selectable count. Default value is 1.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun maxSelectable(maxSelectable: Int) = this.apply {\n        if (!selectionSpec.mediaTypeExclusive) return this\n        require(maxSelectable >= 1) { \"maxSelectable must be greater than or equal to one\" }\n        check(!(selectionSpec.maxImageSelectable > 0 || selectionSpec.maxVideoSelectable > 0)) {\n            \"already set maxImageSelectable and maxVideoSelectable\"\n        }\n        selectionSpec.maxSelectable = maxSelectable\n    }\n\n    /**\n     * Only useful when [SelectionSpec.mediaTypeExclusive] set true and you want to set different maximum\n     * selectable files for image and video media types.\n     *\n     * @param maxImageSelectable Maximum selectable count for image.\n     * @param maxVideoSelectable Maximum selectable count for video.\n     * @return\n     */\n    fun maxSelectablePerMediaType(maxImageSelectable: Int, maxVideoSelectable: Int) = this.apply {\n        if (selectionSpec.mediaTypeExclusive) return this\n        require(!(maxImageSelectable < 1 || maxVideoSelectable < 1)) {\n            \"mediaTypeExclusive must be false and max selectable must be greater than or equal to one\"\n        }\n        selectionSpec.maxSelectable = -1\n        selectionSpec.maxImageSelectable = maxImageSelectable\n        selectionSpec.maxVideoSelectable = maxVideoSelectable\n    }\n\n    /**\n     * Add filter to filter each selecting item.\n     *\n     * @param filter [Filter]\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun addFilter(filter: Filter) = apply {\n        if (selectionSpec.filters == null) selectionSpec.filters = mutableListOf()\n        selectionSpec.filters?.add(filter)\n    }\n\n    /**\n     * Determines whether the photo capturing is enabled or not on the media grid view.\n     * If this value is set true, photo capturing entry will appear only on All Media's page.\n     *\n     * @param enable Whether to enable capturing or not. Default value is false;\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun capture(enable: Boolean) = this.apply { selectionSpec.capture = enable }\n\n    /**\n     * Show a original photo check options.Let users decide whether use original photo after select\n     *\n     * @param enable Whether to enable original photo or not\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun originalEnable(enable: Boolean) = this.apply { selectionSpec.originalable = enable }\n\n    /**\n     * Maximum original size,the unit is MB. Only useful when {link@originalEnable} set true\n     *\n     * @param size Maximum original size. Default value is Integer.MAX_VALUE\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun maxOriginalSize(size: Int) = this.apply { selectionSpec.originalMaxSize = size }\n\n    /**\n     * Capture strategy provided for the location to save photos including internal and external\n     * storage and also a authority for [androidx.core.content.FileProvider].\n     *\n     * @param captureStrategy [CaptureStrategy], needed only when capturing is enabled.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun captureStrategy(captureStrategy: CaptureStrategy) = this.apply {\n        selectionSpec.captureStrategy = captureStrategy\n    }\n\n    /**\n     * Set the desired orientation of this activity.\n     *\n     * @param orientation An orientation constant as used in [ScreenOrientation].\n     * Default value is [android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT].\n     * @return [SelectionCreator] for fluent API.\n     * @see Activity.setRequestedOrientation\n     */\n    fun restrictOrientation(@ScreenOrientation orientation: Int) = this.apply {\n        selectionSpec.orientation = orientation\n    }\n\n    /**\n     * Set a fixed span count for the media grid. Same for different screen orientations.\n     * This will be ignored when [.gridExpectedSize] is set.\n     * [get gridExpectedSize first]\n     * @param spanCount Requested span count.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun spanCount(spanCount: Int) = this.apply {\n        if (selectionSpec.gridExpectedSize > 0) return this\n        selectionSpec.spanCount = spanCount\n    }\n\n    /**\n     * Set expected size for media grid to adapt to different screen sizes. This won't necessarily\n     * be applied cause the media grid should fill the view container. The measured media grid's\n     * size will be as close to this value as possible.\n     *\n     * @param sizePx Expected media grid size in pixel.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun gridExpectedSize(sizePx: Int) = this.apply { selectionSpec.gridExpectedSize = sizePx }\n\n    /**\n     * Photo thumbnail's scale compared to the View's size. It should be a float value in (0.0,1.0].\n     *\n     * @param scale Thumbnail's scale in (0.0, 1.0]. Default value is 0.5.\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun thumbnailScale(scale: Float) = this.apply {\n        require(!(scale <= 0f || scale > 1f)) { \"Thumbnail scale must be between (0.0, 1.0]\" }\n        selectionSpec.thumbnailScale = scale\n    }\n\n    /**\n     * Provide an image engine.\n     * There are two built-in image engines:\n     * And you can implement your own image engine.\n     *\n     * @param imageEngine [ImageEngine]\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun imageEngine(imageEngine: ImageEngine) = this.apply {\n        selectionSpec.imageEngine = imageEngine\n        selectionSpec.imageEngine?.init(matisse.activity?.applicationContext!!)\n    }\n\n    /**\n     * Whether to support crop\n     * If this value is set true, it will support function crop.\n     * @param crop Whether to support crop or not. Default value is false;\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun isCrop(crop: Boolean) = this.apply { selectionSpec.isCrop = crop }\n\n    /**\n     * isCircleCrop\n     * default is RECTANGLE CROP\n     */\n    fun isCircleCrop(isCircle: Boolean) = this.apply {\n        selectionSpec.isCircleCrop = isCircle\n    }\n\n    /**\n     * provide file to save image after crop\n     */\n    fun cropCacheFolder(cropCacheFolder: File) = this.apply {\n        selectionSpec.cropCacheFolder = cropCacheFolder\n    }\n\n    /**\n     * Set listener for callback immediately when user select or unselect something.\n     *\n     * It's a redundant API with [Matisse.obtainResult],\n     * we only suggest you to use this API when you need to do something immediately.\n     *\n     * @param listener [OnSelectedListener]\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun setOnSelectedListener(listener: OnSelectedListener?) = this.apply {\n        selectionSpec.onSelectedListener = listener\n    }\n\n    /**\n     * Set listener for callback immediately when user check or uncheck original.\n     *\n     * @param listener [OnSelectedListener]\n     * @return [SelectionCreator] for fluent API.\n     */\n    fun setOnCheckedListener(listener: OnCheckedListener?) = this.apply {\n        selectionSpec.onCheckedListener = listener\n    }\n\n    /**\n     * set notice type for matisse\n     */\n    fun setNoticeConsumer(\n        noticeConsumer: ((context: Context, noticeType: Int, title: String, message: String) -> Unit)?\n    ) = this.apply {\n        selectionSpec.noticeEvent = noticeConsumer\n    }\n\n    /**\n     * set Status Bar\n     */\n    fun setStatusBarFuture(statusBarFunction: ((params: BaseActivity, view: View?) -> Unit)?) =\n        this.apply {\n            selectionSpec.statusBarFuture = statusBarFunction\n        }\n\n    /**\n     * set last choose pictures ids\n     * id is cursor id. not support crop picture\n     * 预选中上次带回的图片\n     * 注：暂时无法保持预选中图片的顺序\n     */\n    fun setLastChoosePicturesIdOrUri(list: ArrayList<String>?) = this.apply {\n        selectionSpec.lastChoosePictureIdsOrUris = list\n    }\n\n    /**\n     * Start to select media and wait for result.\n     *\n     * @param requestCode Identity of the request Activity or Fragment.\n     */\n    fun forResult(requestCode: Int) {\n        val activity = matisse.activity ?: return\n\n        val intent = Intent(activity, MatisseActivity::class.java)\n\n        val fragment = matisse.fragment\n        if (fragment != null) {\n            fragment.startActivityForResult(intent, requestCode)\n        } else {\n            activity.startActivityForResult(intent, requestCode)\n        }\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/engine/ImageEngine.kt",
    "content": "package com.matisse.engine\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.widget.ImageView\n\n/**\n * Describe : Image loader interface. There are predefined\n * Created by Leo on 2018/9/6 on 17:01.\n */\ninterface ImageEngine {\n\n    /**\n     * Load thumbnail of a static image resource\n     *\n     * @param context context\n     * @param resize Desired size of the origin image\n     * @param placeholder Placeholder drawable when image is not loaded yet\n     * @param imageView ImageView widget\n     * @param uri Uri of the loaded image\n     */\n    fun loadThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?,\n        imageView: ImageView, uri: Uri?\n    )\n\n    /**\n     * Load thumbnail of a static image resource\n     *\n     * @param context context\n     * @param resize Desired size of the origin image\n     * @param placeholder Placeholder drawable when image is not loaded yet\n     * @param imageView ImageView widget\n     * @param uri Uri of the loaded image\n     */\n    fun loadGifThumbnail(\n        context: Context, resize: Int, placeholder: Drawable?,\n        imageView: ImageView, uri: Uri?\n    )\n\n    /**\n     * Load a gif image resource\n     *\n     * @param context context\n     * @param imageView ImageView widget\n     * @param uri Uri of the loaded image\n     */\n    fun loadImage(context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?)\n\n    /**\n     * Load a gif image resource\n     *\n     * @param context context\n     * @param resizeX Desired x-size of the origin image\n     * @param resizeY Desired y-size of the origin image\n     * @param imageView ImageView widget\n     * @param uri Uri of the loaded image\n     */\n    fun loadGifImage(context: Context, resizeX: Int, resizeY: Int, imageView: ImageView, uri: Uri?)\n\n    fun cleanMemory(context: Context)\n\n    fun pause(context: Context)\n\n    fun resume(context: Context)\n\n    // 在application的onCreate中初始化\n    fun init(context: Context)\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/entity/Album.kt",
    "content": "package com.matisse.entity\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Parcel\nimport android.os.Parcelable\nimport com.matisse.R\nimport com.matisse.loader.AlbumLoader\n\nclass Album() : Parcelable {\n    private var id = \"\"\n    private var coverUri: Uri? = null\n    private var displayName = \"\"\n    private var count: Long = 0\n    private var isCheck = false\n\n    constructor(parcel: Parcel) : this() {\n        id = parcel.readString() ?: \"\"\n        coverUri = parcel.readParcelable(Uri::class.java.classLoader)\n        displayName = parcel.readString() ?: \"\"\n        count = parcel.readLong()\n        isCheck = parcel.readByte() != 0.toByte()\n    }\n\n    constructor(mCoverUri: Uri?, mDisplayName: String, mCount: Long) :\n            this(\"-1\", mCoverUri, mDisplayName, mCount)\n\n    constructor(mDisplayName: String, mCount: Long) :\n            this(System.currentTimeMillis().toString(), mDisplayName, mCount, false)\n\n    constructor(mId: String, mCoverUri: Uri?, mDisplayName: String, mCount: Long) :\n            this() {\n        this.id = mId\n        this.coverUri = mCoverUri\n        this.displayName = mDisplayName\n        this.count = mCount\n        this.isCheck = false\n    }\n\n    constructor(\n        mId: String, mDisplayName: String, mCount: Long, mIsCheck: Boolean = false\n    ) : this() {\n        this.id = mId\n        this.displayName = mDisplayName\n        this.count = mCount\n        this.isCheck = mIsCheck\n    }\n\n    fun getId() = id\n\n    fun getCoverPath() = coverUri\n\n    fun setCoverPath(path: Uri?) {\n        path?.apply { coverUri = this }\n    }\n\n    fun getCount() = count\n\n    fun addCaptureCount() {\n        count++\n    }\n\n    fun getDisplayName(context: Context): String {\n        return if (isAll()) {\n            context.getString(R.string.album_name_all)\n        } else displayName\n    }\n\n    fun isAll() = ALBUM_ID_ALL == id\n\n    fun isEmpty() = count == 0L\n\n    fun isChecked() = isCheck\n\n    override fun writeToParcel(parcel: Parcel, flags: Int) {\n        parcel.writeString(id)\n        parcel.writeParcelable(coverUri, 0)\n        parcel.writeString(displayName)\n        parcel.writeLong(count)\n        parcel.writeByte(if (isCheck) 1 else 0)\n    }\n\n    override fun describeContents() = 0\n\n    companion object CREATOR : Parcelable.Creator<Album> {\n\n        const val ALBUM_ID_ALL = (-1).toString()\n        const val ALBUM_NAME_ALL = \"All\"\n\n        override fun createFromParcel(parcel: Parcel): Album {\n            return Album(parcel)\n        }\n\n        override fun newArray(size: Int): Array<Album?> {\n            return arrayOfNulls(size)\n        }\n\n        fun valueOf(cursor: Cursor) = Album(\n            cursor.getString(cursor.getColumnIndex(AlbumLoader.BUCKET_ID)),\n            Uri.parse(cursor.getString(cursor.getColumnIndex(AlbumLoader.COLUMN_URI)) ?: \"\"),\n            cursor.getString(cursor.getColumnIndex(AlbumLoader.BUCKET_DISPLAY_NAME)),\n            cursor.getLong(cursor.getColumnIndex(AlbumLoader.COLUMN_COUNT))\n        )\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/entity/CaptureStrategy.kt",
    "content": "package com.matisse.entity\n\ndata class CaptureStrategy(var isPublic: Boolean, var authority: String, var directory: String = \"\")"
  },
  {
    "path": "matisse/src/main/java/com/matisse/entity/ConstValue.kt",
    "content": "package com.matisse.entity\n\nimport com.matisse.ucrop.UCrop\n\nobject ConstValue {\n    const val EXTRA_RESULT_SELECTION = \"extra_result_selection\"\n    const val EXTRA_RESULT_SELECTION_ID = \"extra_result_selection_id\"\n    const val EXTRA_RESULT_ORIGINAL_ENABLE = \"extra_result_original_enable\"\n    const val EXTRA_ALBUM = \"extra_album\"\n    const val EXTRA_ITEM = \"extra_item\"\n\n    const val CHECK_STATE = \"checkState\"\n\n    const val FOLDER_CHECK_POSITION = \"folder_check_position\"\n\n    const val EXTRA_DEFAULT_BUNDLE = \"extra_default_bundle\"\n    const val EXTRA_RESULT_BUNDLE = \"extra_result_bundle\"\n    const val EXTRA_RESULT_CROP_BACK_BUNDLE = UCrop.EXTRA_OUTPUT_URI\n    const val EXTRA_RESULT_APPLY = \"extra_result_apply\"\n\n    const val STATE_SELECTION = \"state_selection\"\n    const val STATE_COLLECTION_TYPE = \"state_collection_type\"\n\n    const val REQUEST_CODE_PREVIEW = 23\n    const val REQUEST_CODE_CAPTURE = 24\n    const val REQUEST_CODE_CROP = 69            // 对应UCrop中的key\n    const val REQUEST_CODE_CROP_ERROR = 96      // 对应UCrop中的key\n    const val REQUEST_CODE_CHOOSE = 26\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/entity/IncapableCause.kt",
    "content": "package com.matisse.entity\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.annotation.IntDef\nimport androidx.fragment.app.FragmentActivity\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.widget.IncapableDialog\n\nclass IncapableCause {\n\n    companion object {\n        const val TOAST = 0x0001\n        const val DIALOG = 0x0002\n        const val LOADING = 0x0003\n        const val NONE = 0x0004\n\n        fun handleCause(context: Context, cause: IncapableCause?) {\n            if (cause?.noticeEvent != null) {\n                cause.noticeEvent?.invoke(\n                    context, cause.form, cause.title ?: \"\", cause.message ?: \"\"\n                )\n                return\n            }\n\n            when (cause?.form) {\n                DIALOG -> {\n                    IncapableDialog.newInstance(cause.title, cause.message)\n                        .show(\n                            (context as FragmentActivity).supportFragmentManager,\n                            IncapableDialog::class.java.name\n                        )\n                }\n\n                TOAST -> {\n                    Toast.makeText(context, cause.message, Toast.LENGTH_SHORT).show()\n                }\n\n                LOADING -> {\n                    // TODO Leo 2019-12-24 complete loading\n                }\n            }\n        }\n    }\n\n    @Retention(AnnotationRetention.SOURCE)\n    @IntDef(TOAST, DIALOG, LOADING, NONE)\n    annotation class Form\n\n    var form = TOAST\n    var title: String? = null\n    var message: String? = null\n    var dismissLoading: Boolean? = null\n    var noticeEvent: ((\n        context: Context, noticeType: Int, title: String, msg: String\n    ) -> Unit)? = null\n\n    constructor(message: String) : this(TOAST, message)\n    constructor(@Form form: Int, message: String) : this(form, \"\", message)\n    constructor(@Form form: Int, title: String, message: String) : this(form, title, message, true)\n    constructor(@Form form: Int, title: String, message: String, dismissLoading: Boolean) {\n        this.form = form\n        this.title = title\n        this.message = message\n        this.dismissLoading = dismissLoading\n\n        this.noticeEvent = SelectionSpec.getInstance().noticeEvent\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/entity/Item.kt",
    "content": "package com.matisse.entity\n\nimport android.content.ContentUris\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Parcelable\nimport android.provider.MediaStore\nimport com.matisse.MimeTypeManager\nimport kotlinx.android.parcel.IgnoredOnParcel\nimport kotlinx.android.parcel.Parcelize\n\n@Parcelize\nclass Item(\n    var id: Long, private var mimeType: String, var size: Long = 0,\n    var duration: Long = 0, var positionInList: Int = -1\n) : Parcelable {\n\n    companion object {\n        const val ITEM_ID_CAPTURE: Long = -1\n        const val ITEM_DISPLAY_NAME_CAPTURE = \"Capture\"\n\n        // * 注：资源文件size单位为字节byte\n        fun valueOf(cursor: Cursor?, positionInList: Int = -1) = cursor?.let {\n            Item(\n                it.getLong(it.getColumnIndex(MediaStore.Files.FileColumns._ID)),\n                it.getString(it.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),\n                it.getLong(it.getColumnIndex(MediaStore.MediaColumns.SIZE)),\n                it.getLong(it.getColumnIndex(\"duration\")), positionInList\n            )\n        }\n    }\n\n    @IgnoredOnParcel\n    private var uri: Uri\n\n    init {\n        val contentUri = when {\n            isImage() -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n            isVideo() -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI\n            else -> MediaStore.Files.getContentUri(\"external\")\n        }\n        uri = ContentUris.withAppendedId(contentUri, id)\n    }\n\n    fun isImage() = MimeTypeManager.isImage(mimeType)\n\n    fun isGif() = MimeTypeManager.isGif(mimeType)\n\n    fun isVideo() = MimeTypeManager.isVideo(mimeType)\n\n    fun getContentUri() = uri\n\n    fun isCapture() = id == ITEM_ID_CAPTURE\n\n    override fun describeContents() = 0\n\n    override fun equals(other: Any?): Boolean {\n        if (other !is Item) return false\n\n        val otherItem = other as Item?\n        return ((id == otherItem?.id && (mimeType == otherItem.mimeType))\n                && (uri == otherItem.uri) && size == otherItem.size && duration == otherItem.duration)\n    }\n\n    override fun hashCode(): Int {\n        var result = 1\n        result = 31 * result + mimeType.hashCode()\n        result = 31 * result + uri.hashCode()\n        result = 31 * result + size.toString().hashCode()\n        result = 31 * result + duration.toString().hashCode()\n        return result\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/filter/Filter.kt",
    "content": "package com.matisse.filter\n\nimport android.content.Context\nimport com.matisse.MimeType\nimport com.matisse.MimeTypeManager\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\n\n/**\n * Describe : Filter for choosing a {@link Item}. You can add multiple Filters through\n * {@link SelectionCreator #addFilter(Filter)}.\n * Created by Leo on 2018/9/4 on 16:12.\n */\nabstract class Filter {\n    companion object {\n\n        // Convenient constant for a minimum value\n        const val MIN = 0\n\n        // Convenient constant for a maximum value\n        const val MAX = Int.MAX_VALUE\n\n        // Convenient constant for 1024\n        const val K = 1024\n    }\n\n    // Against what mime types this filter applies\n    abstract fun constraintTypes(): Set<MimeType>\n\n    /**\n     * Invoked for filtering each item\n     *\n     * @return null if selectable, {@link IncapableCause} if not selectable.\n     */\n    abstract fun filter(context: Context, item: Item?): IncapableCause?\n\n    // Whether an {@link Item} need filtering\n    open fun needFiltering(context: Context, item: Item?): Boolean {\n        constraintTypes().forEach {\n            if (MimeTypeManager.checkType(context, item?.getContentUri(), it.getValue())\n            ) return true\n        }\n\n        return false\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/internal/entity/SelectionSpec.kt",
    "content": "package com.matisse.internal.entity\n\nimport android.content.Context\nimport android.content.pm.ActivityInfo\nimport android.view.View\nimport androidx.annotation.StyleRes\nimport com.matisse.MimeType\nimport com.matisse.MimeTypeManager\nimport com.matisse.R\nimport com.matisse.engine.ImageEngine\nimport com.matisse.entity.CaptureStrategy\nimport com.matisse.entity.Item\nimport com.matisse.filter.Filter\nimport com.matisse.listener.OnCheckedListener\nimport com.matisse.listener.OnSelectedListener\nimport com.matisse.ui.activity.BaseActivity\nimport java.io.File\n\n/**\n * Describe : Builder to get config values\n * Created by Leo on 2018/8/29 on 14:54.\n */\nclass SelectionSpec {\n    var mimeTypeSet: Set<MimeType>? = null\n    var mediaTypeExclusive = false                      // 设置单种/多种媒体资源选择 默认支持多种\n    var filters: MutableList<Filter>? = null\n    var maxSelectable = 1\n    var maxImageSelectable = 0\n    var maxVideoSelectable = 0\n    var thumbnailScale = 0.5f\n    var countable = false\n    var capture = false\n    var gridExpectedSize = 0\n    var spanCount = 3\n    var captureStrategy: CaptureStrategy? = null\n    @StyleRes\n    var themeId = R.style.Matisse_Default\n    var orientation = 0\n    var originalable = false\n    var originalMaxSize = 0\n    var imageEngine: ImageEngine? = null\n    var onSelectedListener: OnSelectedListener? = null\n    var onCheckedListener: OnCheckedListener? = null\n\n    var isCrop = false                              // 裁剪\n    var isCircleCrop = false                        // 裁剪框的形状\n    var cropCacheFolder: File? = null               // 裁剪后文件保存路径\n\n    var hasInited = false                           // 是否初始化完成\n\n    // 库内提示具体回调\n    var noticeEvent: ((\n        context: Context, noticeType: Int, title: String, msg: String\n    ) -> Unit)? = null\n\n    // 状态栏处理回调\n    var statusBarFuture: ((params: BaseActivity, view: View?) -> Unit)? = null\n\n    var lastChoosePictureIdsOrUris: ArrayList<String>? = null   // 上次选中的图片Id\n\n    class InstanceHolder {\n        companion object {\n            val INSTANCE: SelectionSpec = SelectionSpec()\n        }\n    }\n\n    companion object {\n\n        fun getInstance() = InstanceHolder.INSTANCE\n\n        fun getCleanInstance(): SelectionSpec {\n            val selectionSpec = getInstance()\n            selectionSpec.reset()\n            return selectionSpec\n        }\n    }\n\n    private fun reset() {\n        mimeTypeSet = null\n        mediaTypeExclusive = false\n        themeId = R.style.Matisse_Default\n        orientation = 0\n        countable = false\n        maxSelectable = 1\n        maxImageSelectable = 0\n        maxVideoSelectable = 0\n        filters = null\n        capture = false\n        captureStrategy = null\n        spanCount = 3\n        gridExpectedSize = 0\n        thumbnailScale = 0.5f\n\n        imageEngine = null\n        hasInited = true\n\n        // crop\n        isCrop = false\n        isCircleCrop = false\n\n        // return original setting\n        originalable = false\n        originalMaxSize = Integer.MAX_VALUE\n\n        noticeEvent = null\n        statusBarFuture = null\n\n        lastChoosePictureIdsOrUris = null\n    }\n\n    // 是否可计数\n    fun isCountable() = countable && !isSingleChoose()\n\n    // 是否可单选\n    fun isSingleChoose() =\n        maxSelectable == 1 || (maxImageSelectable == 1 && maxVideoSelectable == 1)\n\n    // 是否可裁剪\n    fun openCrop() = isCrop && isSingleChoose()\n\n    fun isSupportCrop(item: Item?) = item != null && item.isImage() && !item.isGif()\n\n    // 是否单一资源选择方式\n    fun isMediaTypeExclusive() =\n        mediaTypeExclusive && (maxImageSelectable + maxVideoSelectable == 0)\n\n    fun onlyShowImages() =\n        if (mimeTypeSet != null) MimeTypeManager.ofImage().containsAll(mimeTypeSet!!) else false\n\n    fun onlyShowVideos() =\n        if (mimeTypeSet != null) MimeTypeManager.ofVideo().containsAll(mimeTypeSet!!) else false\n\n    fun singleSelectionModeEnabled() = !countable && isSingleChoose()\n\n    fun needOrientationRestriction() = orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/listener/OnCheckedListener.kt",
    "content": "package com.matisse.listener\n\n/**\n * Created by Lijianyou on 2018-09-07.\n * @author  Lijianyou\n */\ninterface OnCheckedListener {\n    fun onCheck(isChecked: Boolean)\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/listener/OnSelectedListener.kt",
    "content": "package com.matisse.listener\n\nimport android.net.Uri\n\n/**\n * Created by Lijianyou on 2018-09-07.\n * @author  Lijianyou\n */\ninterface OnSelectedListener {\n    /**\n     * @param uriList the selected item [Uri] list.\n     * @param pathList the selected item file path list.\n     */\n    fun onSelected(uriList: List<Uri>, pathList: List<String>)\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/loader/AlbumLoader.kt",
    "content": "package com.matisse.loader\n\nimport android.content.ContentUris\nimport android.content.Context\nimport android.database.Cursor\nimport android.database.MatrixCursor\nimport android.database.MergeCursor\nimport android.net.Uri\nimport android.provider.MediaStore\nimport androidx.loader.content.CursorLoader\nimport com.matisse.MimeTypeManager\nimport com.matisse.entity.Album\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.Platform.beforeAndroidTen\nimport java.util.*\n\n/**\n * Describe : Load all albums(group by bucket_id) into a single cursor\n * Created by Leo on 2018/8/29 on 14:28.\n */\nclass AlbumLoader(context: Context, selection: String, selectionArgs: Array<out String>) :\n    CursorLoader(\n        context, QUERY_URI, if (beforeAndroidTen()) PROJECTION else PROJECTION_29,\n        selection, selectionArgs, BUCKET_ORDER_BY\n    ) {\n\n    companion object {\n        const val COLUMN_COUNT = \"count\"\n        private val QUERY_URI = MediaStore.Files.getContentUri(\"external\")\n        const val BUCKET_ID = \"bucket_id\"\n        const val BUCKET_DISPLAY_NAME = \"bucket_display_name\"\n        private const val BUCKET_ORDER_BY = \"datetaken DESC\"\n\n        const val COLUMN_URI = \"uri\"\n\n        val COLUMNS = arrayOf(\n            MediaStore.Files.FileColumns._ID, BUCKET_ID, BUCKET_DISPLAY_NAME,\n            MediaStore.MediaColumns.MIME_TYPE, COLUMN_URI, COLUMN_COUNT\n        )\n\n        val PROJECTION = arrayOf(\n            MediaStore.Files.FileColumns._ID, BUCKET_ID, BUCKET_DISPLAY_NAME,\n            MediaStore.MediaColumns.MIME_TYPE, \"COUNT(*) AS $COLUMN_COUNT\"\n        )\n\n        private val PROJECTION_29 = arrayOf(\n            MediaStore.Files.FileColumns._ID, BUCKET_ID,\n            BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE\n        )\n\n        private const val SELECTION = \"(\" + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=? \" +\n                \"OR \" + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=?) \" +\n                \"AND \" + MediaStore.MediaColumns.SIZE + \">0) GROUP BY (\" + BUCKET_ID\n\n        private const val SELECTION_29 = (\n                \"(\" + MediaStore.Files.FileColumns.MEDIA_TYPE\n                        + \"=? OR \" + MediaStore.Files.FileColumns.MEDIA_TYPE\n                        + \"=?) AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        private val SELECTION_ARGS = arrayOf(\n            MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),\n            MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()\n        )\n\n        private const val SELECTION_FOR_SINGLE_MEDIA_TYPE =\n            MediaStore.Files.FileColumns.MEDIA_TYPE + \"=? AND \" +\n                    MediaStore.MediaColumns.SIZE + \">0) GROUP BY (\" + BUCKET_ID\n\n        private const val SELECTION_FOR_SINGLE_MEDIA_TYPE_29 = (\n                MediaStore.Files.FileColumns.MEDIA_TYPE\n                        + \"=? AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        private fun getSelectionArgsForSingleMediaType(mediaType: Int) =\n            arrayOf(mediaType.toString())\n\n        fun newInstance(context: Context): CursorLoader {\n            var selection = if (beforeAndroidTen())\n                SELECTION_FOR_SINGLE_MEDIA_TYPE\n            else\n                SELECTION_FOR_SINGLE_MEDIA_TYPE_29\n            val selectionArgs: Array<String>\n\n            when {\n                SelectionSpec.getInstance().onlyShowImages() -> selectionArgs =\n                    getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)\n                SelectionSpec.getInstance().onlyShowVideos() -> selectionArgs =\n                    getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)\n                else -> {\n                    selection = if (beforeAndroidTen()) SELECTION else SELECTION_29\n                    selectionArgs = SELECTION_ARGS\n                }\n            }\n\n            return AlbumLoader(context, selection, selectionArgs)\n        }\n    }\n\n    override fun loadInBackground(): Cursor? {\n        val albums = super.loadInBackground()\n        val allAlbum = MatrixCursor(COLUMNS)\n        return if (beforeAndroidTen()) loadBelowAndroidQ(albums, allAlbum)\n        else loadAboveAndroidQ(albums, allAlbum)\n    }\n\n    private fun loadBelowAndroidQ(albums: Cursor?, allAlbum: MatrixCursor): MergeCursor {\n        var totalCount = 0\n        var allAlbumCoverUri: Uri? = null\n        val otherAlbums = MatrixCursor(COLUMNS)\n        albums?.apply {\n            while (moveToNext()) {\n                val fileId = getLong(getColumnIndex(MediaStore.Files.FileColumns._ID))\n                val bucketId = getLong(getColumnIndex(BUCKET_ID))\n                val bucketDisplayName = getString(getColumnIndex(BUCKET_DISPLAY_NAME))\n                val mimeType = getString(getColumnIndex(MediaStore.MediaColumns.MIME_TYPE))\n                val uri = getUri(albums)\n                val count = getInt(getColumnIndex(COLUMN_COUNT))\n\n                otherAlbums.addRow(\n                    arrayOf(\n                        fileId, bucketId, bucketDisplayName,\n                        mimeType, uri.toString(), count.toString()\n                    )\n                )\n                totalCount += count\n            }\n            if (albums.moveToFirst()) allAlbumCoverUri = getUri(albums)\n\n        }\n\n        allAlbumAddRow(allAlbumCoverUri, totalCount, allAlbum)\n        return MergeCursor(arrayOf<Cursor>(allAlbum, otherAlbums))\n    }\n\n    private fun loadAboveAndroidQ(albums: Cursor?, allAlbum: MatrixCursor): MergeCursor {\n        var totalCount = 0\n        var allAlbumCoverUri: Uri? = null\n        val otherAlbums = MatrixCursor(COLUMNS)\n\n        // Pseudo GROUP BY\n        val countMap = hashMapOf<Long, Long>()\n        albums?.apply {\n            while (moveToNext()) {\n                val bucketId = getLong(getColumnIndex(BUCKET_ID))\n\n                var count: Long? = countMap[bucketId]\n                if (count == null) count = 1L else count++\n\n                countMap[bucketId] = count\n            }\n\n            if (moveToFirst()) {\n                allAlbumCoverUri = getUri(this)\n\n                val done = HashSet<Long>()\n\n                do {\n                    val bucketId = getLong(getColumnIndex(BUCKET_ID))\n\n                    if (done.contains(bucketId)) continue\n\n                    val fileId = getLong(getColumnIndex(MediaStore.Files.FileColumns._ID))\n                    val bucketDisplayName = getString(getColumnIndex(BUCKET_DISPLAY_NAME))\n                    val mimeType = getString(getColumnIndex(MediaStore.MediaColumns.MIME_TYPE))\n                    val uri = getUri(this)\n                    val count = countMap[bucketId]\n\n                    otherAlbums.addRow(\n                        arrayOf<String>(\n                            fileId.toString(), bucketId.toString(), bucketDisplayName,\n                            mimeType, uri.toString(), count.toString()\n                        )\n                    )\n                    done.add(bucketId)\n\n                    totalCount += count?.toInt() ?: 0\n                } while (albums.moveToNext())\n            }\n        }\n\n        allAlbumAddRow(allAlbumCoverUri, totalCount, allAlbum)\n        return MergeCursor(arrayOf<Cursor>(allAlbum, otherAlbums))\n    }\n\n    private fun allAlbumAddRow(allAlbumCoverUri: Uri?, totalCount: Int, allAlbum: MatrixCursor) {\n        val row: Array<String?> = arrayOf(\n            Album.ALBUM_ID_ALL, Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL,\n            null, allAlbumCoverUri?.toString(), totalCount.toString()\n        )\n        allAlbum.addRow(row)\n    }\n\n    private fun getUri(cursor: Cursor): Uri {\n        val id = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID))\n        val mimeType =\n            cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)) ?: \"\"\n        val contentUri = when {\n            MimeTypeManager.isImage(mimeType) -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n            MimeTypeManager.isVideo(mimeType) -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI\n            else -> MediaStore.Files.getContentUri(\"external\")\n        }\n\n        return ContentUris.withAppendedId(contentUri, id)\n    }\n\n    override fun onContentChanged() {\n        // FIXME a dirty way to fix loading multiple times\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/loader/AlbumMediaLoader.kt",
    "content": "package com.matisse.loader\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.database.MatrixCursor\nimport android.database.MergeCursor\nimport android.provider.MediaStore\nimport androidx.loader.content.CursorLoader\nimport com.matisse.entity.Album\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.MediaStoreCompat\n\n/**\n * Load images and videos into a single cursor.\n * Created by Leo on 2018/9/4 on 19:53.\n */\nclass AlbumMediaLoader(\n    context: Context, selection: String, selectionArgs: Array<out String>, capture: Boolean\n) : CursorLoader(context, QUERY_URI, PROJECTION, selection, selectionArgs, ORDER_BY) {\n\n    private var enableCapture = false\n\n    init {\n        enableCapture = capture\n    }\n\n    companion object {\n        private val QUERY_URI = MediaStore.Files.getContentUri(\"external\")\n\n        val PROJECTION = arrayOf(\n            MediaStore.Files.FileColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME,\n            MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.SIZE, \"duration\"\n        )\n\n        private const val SELECTION_ALL = (\"(\" + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=? OR \"\n                + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=?) AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        private val SELECTION_ALL_ARGS = arrayOf(\n            MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),\n            MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()\n        )\n        // ===========================================================\n\n        // === params for album ALL && showSingleMediaType: true ===\n        private const val SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE = (\n                MediaStore.Files.FileColumns.MEDIA_TYPE + \"=?\"\n                        + \" AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        // === params for ordinary album && showSingleMediaType: false ===\n        private const val SELECTION_ALBUM = (\n                \"(\" + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=?\"\n                        + \" OR \"\n                        + MediaStore.Files.FileColumns.MEDIA_TYPE + \"=?)\"\n                        + \" AND \"\n                        + \" bucket_id=?\"\n                        + \" AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        private fun getSelectionAlbumArgs(albumId: String): Array<String> {\n            return arrayOf(\n                MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),\n                MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString(), albumId\n            )\n        }\n\n        // ===============================================================\n\n        // === params for ordinary album && showSingleMediaType: true ===\n        private const val SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE = (\n                MediaStore.Files.FileColumns.MEDIA_TYPE\n                        + \"=? AND bucket_id=? AND \" + MediaStore.MediaColumns.SIZE + \">0\")\n\n        // ===============================================================\n\n        private const val ORDER_BY = MediaStore.Images.Media.DATE_TAKEN + \" DESC\"\n\n        fun newInstance(context: Context, album: Album, capture: Boolean): CursorLoader {\n            val selection: String\n            val selectionArgs: Array<String>\n            val enableCapture: Boolean\n\n            if (album.isAll()) {\n                when {\n                    SelectionSpec.getInstance().onlyShowImages() -> {\n                        selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE\n                        selectionArgs =\n                            arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString())\n                    }\n                    SelectionSpec.getInstance().onlyShowVideos() -> {\n                        selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE\n                        selectionArgs =\n                            arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString())\n                    }\n                    else -> {\n                        selection = SELECTION_ALL\n                        selectionArgs = SELECTION_ALL_ARGS\n                    }\n                }\n                enableCapture = capture\n            } else {\n                when {\n                    SelectionSpec.getInstance().onlyShowImages() -> {\n                        selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE\n                        selectionArgs = arrayOf(\n                            MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), album.getId()\n                        )\n                    }\n                    SelectionSpec.getInstance().onlyShowVideos() -> {\n                        selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE\n                        selectionArgs = arrayOf(\n                            MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString(), album.getId()\n                        )\n                    }\n                    else -> {\n                        selection = SELECTION_ALBUM\n                        selectionArgs = getSelectionAlbumArgs(album.getId())\n                    }\n                }\n                enableCapture = false\n            }\n            return AlbumMediaLoader(context, selection, selectionArgs, enableCapture)\n        }\n    }\n\n    override fun loadInBackground(): Cursor? {\n        val result = super.loadInBackground()\n        if (!enableCapture || !MediaStoreCompat.hasCameraFeature(context)) {\n            return result\n        }\n        val dummy = MatrixCursor(PROJECTION)\n        dummy.addRow(arrayOf(Item.ITEM_ID_CAPTURE, Item.ITEM_DISPLAY_NAME_CAPTURE, \"\", 0, 0))\n        return MergeCursor(arrayOf(dummy, result!!))\n    }\n\n    override fun onContentChanged() {\n        // FIXME a dirty way to fix loading multiple times\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/model/AlbumCallbacks.kt",
    "content": "package com.matisse.model\n\nimport android.database.Cursor\n\ninterface AlbumCallbacks {\n    fun onAlbumStart()\n    fun onAlbumLoad(cursor: Cursor)\n    fun onAlbumReset()\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/model/AlbumCollection.kt",
    "content": "package com.matisse.model\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.os.Bundle\nimport androidx.fragment.app.FragmentActivity\nimport androidx.loader.app.LoaderManager\nimport androidx.loader.content.Loader\nimport com.matisse.loader.AlbumLoader\nimport java.lang.ref.WeakReference\n\nclass AlbumCollection : LoaderManager.LoaderCallbacks<Cursor> {\n    companion object {\n        const val LOADER_ID = 1\n        const val STATE_CURRENT_SELECTION = \"state_current_selection\"\n    }\n\n    private var context: WeakReference<Context>? = null\n    private var loaderManager: LoaderManager? = null\n    private var callbacks: AlbumCallbacks? = null\n    private var currentSelection = 0\n    private var loadFinished = false\n\n\n    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {\n        val context = context?.get()\n        loadFinished = false\n        return AlbumLoader.newInstance(context!!)\n    }\n\n    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {\n        if (context?.get() == null || data == null) return\n\n        if (!loadFinished) {\n            loadFinished = true\n            callbacks?.onAlbumLoad(data)\n        }\n    }\n\n    override fun onLoaderReset(loader: Loader<Cursor>) {\n        if (context?.get() == null) return\n\n        callbacks?.onAlbumReset()\n    }\n\n    fun onCreate(activity: FragmentActivity, callbacks: AlbumCallbacks) {\n        context = WeakReference(activity)\n        loaderManager = LoaderManager.getInstance(activity)\n        this.callbacks = callbacks\n    }\n\n    fun onRestoreInstanceState(saveInstanceState: Bundle) {\n        currentSelection = saveInstanceState.getInt(STATE_CURRENT_SELECTION)\n    }\n\n    fun onSaveInstanceState(outState: Bundle?) {\n        outState?.putInt(STATE_CURRENT_SELECTION, currentSelection)\n    }\n\n    fun onDestroy() {\n        loaderManager?.destroyLoader(LOADER_ID)\n        if (callbacks != null) callbacks = null\n    }\n\n    @Synchronized\n    fun loadAlbums() {\n        loadFinished = false\n        loaderManager?.initLoader(LOADER_ID, null, this)\n    }\n\n    fun getCurrentSelection() = currentSelection\n\n    fun setStateCurrentSelection(currentSelection: Int) {\n        this.currentSelection = currentSelection\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/model/AlbumMediaCollection.kt",
    "content": "package com.matisse.model\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.os.Bundle\nimport androidx.fragment.app.FragmentActivity\nimport androidx.loader.app.LoaderManager\nimport androidx.loader.content.Loader\nimport com.matisse.entity.Album\nimport com.matisse.loader.AlbumMediaLoader\nimport java.lang.ref.WeakReference\n\nclass AlbumMediaCollection : LoaderManager.LoaderCallbacks<Cursor> {\n\n    companion object {\n        const val LOADER_ID = 2\n        const val ARGS_ALBUM = \"args_album\"\n        const val ARGS_ENABLE_CAPTURE = \"args_enable_capture\"\n    }\n\n    private var context: WeakReference<Context>? = null\n    private var loaderManager: LoaderManager? = null\n    private var callbacks: AlbumCallbacks? = null\n\n\n    fun onCreate(context: FragmentActivity, callbacks: AlbumCallbacks) {\n        this.context = WeakReference(context)\n        loaderManager = LoaderManager.getInstance(context)\n        this.callbacks = callbacks\n    }\n\n    fun onDestroy() {\n        loaderManager?.destroyLoader(LOADER_ID)\n        if (callbacks != null) callbacks = null\n    }\n\n    fun load(target: Album) {\n        load(target, false)\n    }\n\n    fun load(target: Album, enableCapture: Boolean) {\n        val args = Bundle()\n        args.putParcelable(ARGS_ALBUM, target)\n        args.putBoolean(ARGS_ENABLE_CAPTURE, enableCapture)\n        loaderManager?.initLoader(LOADER_ID, args, this)\n    }\n\n    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {\n        val content = context?.get()\n\n        val album = args?.getParcelable<Album>(ARGS_ALBUM)\n        return AlbumMediaLoader.newInstance(\n            content!!, album!!, album.isAll()\n                    && args.getBoolean(ARGS_ENABLE_CAPTURE, false)\n        )\n    }\n\n    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {\n        if (context?.get() == null) return\n\n        callbacks?.onAlbumLoad(data!!)\n    }\n\n    override fun onLoaderReset(loader: Loader<Cursor>) {\n        if (context?.get() == null) return\n\n        callbacks?.onAlbumReset()\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/model/SelectedItemCollection.kt",
    "content": "package com.matisse.model\n\nimport android.content.Context\nimport android.content.res.Resources\nimport android.net.Uri\nimport android.os.Bundle\nimport com.matisse.R\nimport com.matisse.entity.ConstValue.STATE_COLLECTION_TYPE\nimport com.matisse.entity.ConstValue.STATE_SELECTION\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.PhotoMetadataUtils\nimport com.matisse.utils.getPath\nimport com.matisse.widget.CheckView\nimport java.util.*\nimport kotlin.collections.ArrayList\n\nclass SelectedItemCollection(private var context: Context) {\n\n    companion object {\n        /**\n         * Empty collection\n         */\n        const val COLLECTION_UNDEFINED = 0x00\n        /**\n         * Collection only with images\n         */\n        const val COLLECTION_IMAGE = 0x01\n        /**\n         * Collection only with videos\n         */\n        const val COLLECTION_VIDEO = 0x02\n        /**\n         * Collection with images and videos.\n         */\n        const val COLLECTION_MIXED = COLLECTION_IMAGE or COLLECTION_VIDEO\n    }\n\n    private lateinit var items: LinkedHashSet<Item>\n    private var imageItems: LinkedHashSet<Item>? = null\n    private var videoItems: LinkedHashSet<Item>? = null\n    private var collectionType = COLLECTION_UNDEFINED\n    private val spec: SelectionSpec = SelectionSpec.getInstance()\n\n\n    fun onCreate(bundle: Bundle?) {\n        if (bundle == null) {\n            items = linkedSetOf()\n        } else {\n            val saved = bundle.getParcelableArrayList<Item>(STATE_SELECTION)\n            items = LinkedHashSet(saved!!)\n            initImageOrVideoItems()\n            collectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED)\n        }\n    }\n\n    /**\n     * 根据混合选择模式，初始化图片与视频集合\n     */\n    private fun initImageOrVideoItems() {\n        if (spec.isMediaTypeExclusive()) return\n        items.forEach {\n            addImageOrVideoItem(it)\n        }\n    }\n\n    fun onSaveInstanceState(outState: Bundle?) {\n        outState?.putParcelableArrayList(STATE_SELECTION, ArrayList(items))\n        outState?.putInt(STATE_COLLECTION_TYPE, collectionType)\n    }\n\n    fun getDataWithBundle() = Bundle().run {\n        putParcelableArrayList(STATE_SELECTION, ArrayList(items))\n        putInt(STATE_COLLECTION_TYPE, collectionType)\n        this\n    }\n\n    fun setDefaultSelection(uris: List<Item>) {\n        items.addAll(uris)\n    }\n\n    private fun resetType() {\n        if (items.size == 0) {\n            collectionType = COLLECTION_UNDEFINED\n        } else {\n            if (collectionType == COLLECTION_MIXED) refineCollectionType()\n        }\n    }\n\n    fun overwrite(items: ArrayList<Item>, collectionType: Int) {\n        this.collectionType = if (items.size == 0) COLLECTION_UNDEFINED else collectionType\n\n        this.items.clear()\n        this.items.addAll(items)\n    }\n\n    fun asList() = ArrayList(items)\n\n    fun asListOfUri(): List<Uri> {\n        val uris = arrayListOf<Uri>()\n        for (item in items) {\n            uris.add(item.getContentUri())\n        }\n        return uris\n    }\n\n    fun asListOfString(): List<String> {\n        val paths = ArrayList<String>()\n        items.forEach {\n            val path = getPath(context, it.getContentUri())\n            if (path != null) paths.add(path)\n        }\n\n        return paths\n    }\n\n    fun isAcceptable(item: Item?): IncapableCause? {\n        if (maxSelectableReached(item)) {\n            val maxSelectable = currentMaxSelectable(item)\n            val maxSelectableTips = currentMaxSelectableTips(item)\n\n            val cause = try {\n                context.getString(maxSelectableTips, maxSelectable)\n            } catch (e: Resources.NotFoundException) {\n                context.getString(maxSelectableTips, maxSelectable)\n            } catch (e: NoClassDefFoundError) {\n                context.getString(maxSelectableTips, maxSelectable)\n            }\n\n            return IncapableCause(cause)\n        } else if (typeConflict(item)) {\n            return IncapableCause(context.getString(R.string.error_type_conflict))\n        }\n\n        return PhotoMetadataUtils.isAcceptable(context, item)\n    }\n\n    private fun currentMaxSelectableTips(item: Item?): Int {\n        if (!spec.isMediaTypeExclusive()) {\n            if (item?.isImage() == true) {\n                return R.string.error_over_count_of_image\n            } else if (item?.isVideo() == true) {\n                return R.string.error_over_count_of_video\n            }\n        }\n\n        return R.string.error_over_count\n    }\n\n    fun maxSelectableReached(item: Item?): Boolean {\n        if (!spec.isMediaTypeExclusive()) {\n            if (item?.isImage() == true) {\n                return spec.maxImageSelectable == imageItems?.size\n            } else if (item?.isVideo() == true) {\n                return spec.maxVideoSelectable == videoItems?.size\n            }\n        }\n        return spec.maxSelectable == items.size\n    }\n\n    // depends\n    private fun currentMaxSelectable(item: Item?): Int {\n        if (!spec.isMediaTypeExclusive()) {\n            if (item?.isImage() == true) {\n                return spec.maxImageSelectable\n            } else if (item?.isVideo() == true) {\n                return spec.maxVideoSelectable\n            }\n        }\n\n        return spec.maxSelectable\n    }\n\n    fun getCollectionType() = collectionType\n\n    fun isEmpty() = items.isEmpty()\n\n    fun isSelected(item: Item?) = items.contains(item)\n\n    fun count() = items.size\n\n    fun items() = items.toList()\n\n    /**\n     * 注：\n     * 此处取的是item在选中集合中的序号，\n     * 所以不需区分混合选择或单独选择\n     */\n    fun checkedNumOf(item: Item?): Int {\n        val index = ArrayList(items).indexOf(item)\n        return if (index == -1) CheckView.UNCHECKED else index + 1\n    }\n\n    /**\n     * 根据item集合数据设置collectionType\n     */\n    private fun refineCollectionType() {\n        val hasImage = imageItems != null && imageItems?.size ?: 0 > 0\n        val hasVideo = videoItems != null && videoItems?.size ?: 0 > 0\n\n        collectionType = if (hasImage && hasVideo) {\n            COLLECTION_MIXED\n        } else if (hasImage) {\n            COLLECTION_IMAGE\n        } else if (hasVideo) {\n            COLLECTION_VIDEO\n        } else {\n            COLLECTION_UNDEFINED\n        }\n    }\n\n    /**\n     * Determine whether there will be conflict media types. A user can only select images and videos at the same time\n     * while [SelectionSpec.mediaTypeExclusive] is set to false.\n     */\n    private fun typeConflict(item: Item?) =\n        spec.isMediaTypeExclusive()\n                && ((item?.isImage() == true && (collectionType == COLLECTION_VIDEO || collectionType == COLLECTION_MIXED))\n                || (item?.isVideo() == true && (collectionType == COLLECTION_IMAGE || collectionType == COLLECTION_MIXED)))\n\n    fun add(item: Item?): Boolean {\n        if (typeConflict(item)) {\n            throw IllegalArgumentException(\"Can't select images and videos at the same time.\")\n        }\n        if (item == null) return false\n\n        val added = items.add(item)\n        addImageOrVideoItem(item)\n        if (added) {\n            when (collectionType) {\n                COLLECTION_UNDEFINED -> {\n                    if (item.isImage()) {\n                        collectionType = COLLECTION_IMAGE\n                    } else if (item.isVideo()) {\n                        collectionType = COLLECTION_VIDEO\n                    }\n                }\n\n                COLLECTION_IMAGE, COLLECTION_VIDEO -> {\n                    if ((item.isImage() && collectionType == COLLECTION_VIDEO)\n                        || item.isVideo() && collectionType == COLLECTION_IMAGE\n                    ) {\n                        collectionType = COLLECTION_MIXED\n                    }\n                }\n            }\n        }\n\n        return added\n    }\n\n    private fun addImageOrVideoItem(item: Item) {\n        if (item.isImage()) {\n            if (imageItems == null)\n                imageItems = linkedSetOf()\n\n            imageItems?.add(item)\n        } else if (item.isVideo()) {\n            if (videoItems == null)\n                videoItems = linkedSetOf()\n\n            videoItems?.add(item)\n        }\n    }\n\n    private fun removeImageOrVideoItem(item: Item) {\n        if (item.isImage()) {\n            imageItems?.remove(item)\n        } else if (item.isVideo()) {\n            videoItems?.remove(item)\n        }\n    }\n\n    fun remove(item: Item?): Boolean {\n        if (item == null) return false\n        val removed = items.remove(item)\n        removeImageOrVideoItem(item)\n        if (removed) resetType()\n        return removed\n    }\n\n    fun removeAll() {\n        items.clear()\n        imageItems?.clear()\n        videoItems?.clear()\n        resetType()\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/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.matisse.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": "matisse/src/main/java/com/matisse/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.matisse.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                if (scaleFactor >= 0) {\n                    mListener.onScale(scaleFactor,\n                            detector.getFocusX(), detector.getFocusY());\n                }\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": "matisse/src/main/java/com/matisse/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.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnMatrixChangedListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnOutsidePhotoTapListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnPhotoTapListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnScaleChangedListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnSingleFlingListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnViewDragListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/photoview/OnViewTapListener.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/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.matisse.photoview;\n\nimport android.content.Context;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\n\nimport androidx.appcompat.widget.AppCompatImageView;\n\n/**\n * A zoomable {@link AppCompatImageView}. 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 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    public void setRotationTo(float rotationDegree) {\n        attacher.setRotationTo(rotationDegree);\n    }\n\n    public void setRotationBy(float rotationDegree) {\n        attacher.setRotationBy(rotationDegree);\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    @SuppressWarnings(\"UnusedReturnValue\")\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": "matisse/src/main/java/com/matisse/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.matisse.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 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 HORIZONTAL_EDGE_NONE = -1;\n    private static final int HORIZONTAL_EDGE_LEFT = 0;\n    private static final int HORIZONTAL_EDGE_RIGHT = 1;\n    private static final int HORIZONTAL_EDGE_BOTH = 2;\n    private static final int VERTICAL_EDGE_NONE = -1;\n    private static final int VERTICAL_EDGE_TOP = 0;\n    private static final int VERTICAL_EDGE_BOTTOM = 1;\n    private static final int VERTICAL_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 mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;\n    private int mVerticalScrollEdge = VERTICAL_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            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 start taking\n             * over the touch event.\n             *\n             * First we check whether this function is enabled. We never want the\n             * parent to take 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 take over).\n             */\n            ViewParent parent = mImageView.getParent();\n            if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {\n                if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH\n                        || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)\n                        || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f)\n                        || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f)\n                        || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -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) {\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        if (imageView.isInEditMode()) {\n            return;\n        }\n        mBaseRotation = 0.0f;\n        // Create Gesture Detectors...\n        mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);\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                    if (e1.getPointerCount() > SINGLE_TOUCH\n                            || e2.getPointerCount() > SINGLE_TOUCH) {\n                        return false;\n                    }\n                    return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);\n                }\n                return false;\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                final float x = e.getX(), y = e.getY();\n                if (mViewTapListener != null) {\n                    mViewTapListener.onViewTap(mImageView, x, y);\n                }\n                if (displayRect != null) {\n                    // Check to see if the user tapped on the photo\n                    if (displayRect.contains(x, y)) {\n                        float xResult = (x - displayRect.left)\n                                / displayRect.width();\n                        float yResult = (y - displayRect.top)\n                                / displayRect.height();\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                    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                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        if (mImageView.getDrawable() == null) {\n            return false;\n        }\n        mSuppMatrix.set(finalMatrix);\n        checkAndDisplayMatrix();\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\n                (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\n            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        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                    // If we're flinging, and the user presses down, cancel\n                    // fling\n                    cancelFling();\n                    break;\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            // Try the Scale/Drag detector\n            if (mScaleDragDetector != null) {\n                boolean wasScaling = mScaleDragDetector.isScaling();\n                boolean wasDragging = mScaleDragDetector.isDragging();\n                handled = mScaleDragDetector.onTouchEvent(ev);\n                boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();\n                boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();\n                mBlockParentIntercept = didntScale && didntDrag;\n            }\n            // Check to see if the user double tapped\n            if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {\n                handled = true;\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        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        // 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        final float viewWidth = getImageViewWidth(mImageView);\n        final float viewHeight = getImageViewHeight(mImageView);\n        final int drawableWidth = drawable.getIntrinsicWidth();\n        final int drawableHeight = drawable.getIntrinsicHeight();\n        mBaseMatrix.reset();\n        final float widthScale = viewWidth / drawableWidth;\n        final float heightScale = viewHeight / drawableHeight;\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            if ((int) mBaseRotation % 180 != 0) {\n                mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);\n            }\n            switch (mScaleType) {\n                case FIT_CENTER:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);\n                    break;\n                case FIT_START:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);\n                    break;\n                case FIT_END:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);\n                    break;\n                case FIT_XY:\n                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);\n                    break;\n                default:\n                    break;\n            }\n        }\n        resetMatrix();\n    }\n\n    private boolean checkMatrixBounds() {\n        final RectF rect = getDisplayRect(getDrawMatrix());\n        if (rect == null) {\n            return false;\n        }\n        final float height = rect.height(), width = rect.width();\n        float deltaX = 0, deltaY = 0;\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            mVerticalScrollEdge = VERTICAL_EDGE_BOTH;\n        } else if (rect.top > 0) {\n            mVerticalScrollEdge = VERTICAL_EDGE_TOP;\n            deltaY = -rect.top;\n        } else if (rect.bottom < viewHeight) {\n            mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM;\n            deltaY = viewHeight - rect.bottom;\n        } else {\n            mVerticalScrollEdge = VERTICAL_EDGE_NONE;\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            mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;\n        } else if (rect.left > 0) {\n            mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT;\n            deltaX = -rect.left;\n        } else if (rect.right < viewWidth) {\n            deltaX = viewWidth - rect.right;\n            mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT;\n        } else {\n            mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE;\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            float t = interpolate();\n            float scale = mZoomStart + t * (mZoomEnd - mZoomStart);\n            float deltaScale = scale / getScale();\n            onGestureListener.onScale(deltaScale, mFocalX, mFocalY);\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            final int startX = Math.round(-rect.left);\n            final int minX, maxX, minY, maxY;\n            if (viewWidth < rect.width()) {\n                minX = 0;\n                maxX = Math.round(rect.width() - viewWidth);\n            } else {\n                minX = maxX = startX;\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            mCurrentX = startX;\n            mCurrentY = startY;\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            if (mScroller.computeScrollOffset()) {\n                final int newX = mScroller.getCurrX();\n                final int newY = mScroller.getCurrY();\n                mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);\n                checkAndDisplayMatrix();\n                mCurrentX = newX;\n                mCurrentY = newY;\n                // Post On animation\n                Compat.postOnAnimation(mImageView, this);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/photoview/Util.java",
    "content": "package com.matisse.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": "matisse/src/main/java/com/matisse/ucrop/PictureMultiCuttingActivity.java",
    "content": "package com.matisse.ucrop;\n\nimport android.annotation.TargetApi;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.PorterDuff;\nimport android.graphics.drawable.Animatable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Environment;\nimport android.os.ParcelFileDescriptor;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.view.WindowManager;\nimport android.view.animation.AccelerateInterpolator;\nimport android.widget.FrameLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.core.content.ContextCompat;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.matisse.R;\nimport com.matisse.ucrop.callback.BitmapCropCallback;\nimport com.matisse.ucrop.immersion.CropImmersiveManage;\nimport com.matisse.ucrop.model.AspectRatio;\nimport com.matisse.ucrop.model.CutInfo;\nimport com.matisse.ucrop.util.FileUtils;\nimport com.matisse.ucrop.util.VersionUtils;\nimport com.matisse.ucrop.view.CropImageView;\nimport com.matisse.ucrop.view.GestureCropImageView;\nimport com.matisse.ucrop.view.OverlayView;\nimport com.matisse.ucrop.view.TransformImageView;\nimport com.matisse.ucrop.view.UCropView;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\n\n@SuppressWarnings(\"ConstantConditions\")\npublic class PictureMultiCuttingActivity extends AppCompatActivity {\n\n    public static final int DEFAULT_COMPRESS_QUALITY = 90;\n    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;\n\n    public static final int NONE = 0;\n    public static final int SCALE = 1;\n    public static final int ROTATE = 2;\n    public static final int ALL = 3;\n\n    @IntDef({NONE, SCALE, ROTATE, ALL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface GestureTypes {\n\n    }\n\n    private static final String TAG = \"UCropActivity\";\n\n    private static final int TABS_COUNT = 3;\n    private static final int SCALE_WIDGET_SENSITIVITY_COEFFICIENT = 15000;\n    private static final int ROTATE_WIDGET_SENSITIVITY_COEFFICIENT = 42;\n    private RecyclerView mRecyclerView;\n    private PicturePhotoGalleryAdapter adapter;\n    private String mToolbarTitle;\n    private ArrayList<CutInfo> list;\n    // Enables dynamic coloring\n    private int mToolbarColor;\n    private int mStatusBarColor;\n    private int mActiveWidgetColor;\n    private int mToolbarWidgetColor;\n    @ColorInt\n    private int mRootViewBackgroundColor;\n    @DrawableRes\n    private int mToolbarCancelDrawable;\n    @DrawableRes\n    private int mToolbarCropDrawable;\n    private int mLogoColor;\n\n    private boolean mShowLoader = true;\n    private boolean circleDimmedLayer;\n    private UCropView mUCropView;\n    private GestureCropImageView mGestureCropImageView;\n    private OverlayView mOverlayView;\n    private List<ViewGroup> mCropAspectRatioViews = new ArrayList<>();\n    private TextView mTextViewRotateAngle, mTextViewScalePercent;\n    private View mBlockingView;\n    private RelativeLayout uCropMultiplePhotoBox;\n    private Bitmap.CompressFormat mCompressFormat = DEFAULT_COMPRESS_FORMAT;\n    private int mCompressQuality = DEFAULT_COMPRESS_QUALITY;\n    private int[] mAllowedGestures = new int[]{SCALE, ROTATE, ALL};\n    /**\n     * 是否可拖动裁剪框\n     */\n    private boolean isDragFrame;\n\n    /**\n     * 图片是否可拖动或旋转\n     */\n    private boolean scaleEnabled, rotateEnabled, openWhiteStatusBar;\n\n    private int cutIndex;\n\n    /**\n     * 是否使用沉浸式，子类复写该方法来确定是否采用沉浸式\n     *\n     * @return 是否沉浸式，默认true\n     */\n    @Override\n    public boolean isImmersive() {\n        return true;\n    }\n\n\n    /**\n     * 具体沉浸的样式，可以根据需要自行修改状态栏和导航栏的颜色\n     */\n    public void immersive() {\n        CropImmersiveManage.immersiveAboveAPI23(this\n                , mStatusBarColor\n                , mToolbarColor\n                , openWhiteStatusBar);\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        final Intent intent = getIntent();\n        getIntentData(intent);\n        if (isImmersive()) {\n            immersive();\n        }\n        setContentView(R.layout.ucrop_picture_activity_multi_cutting);\n        uCropMultiplePhotoBox = findViewById(R.id.ucrop_mulit_photobox);\n        initLoadCutData();\n        addPhotoRecyclerView();\n        setupViews(intent);\n        setInitialState();\n        addBlockingView();\n        setImageData(intent);\n\n    }\n\n    /**\n     * 装载裁剪数据\n     */\n    private void initLoadCutData() {\n        list = (ArrayList<CutInfo>) getIntent().getSerializableExtra(UCropMulti.Options.EXTRA_CUT_CROP);\n        // Crop cut list\n        if (list == null || list.size() == 0) {\n            closeActivity();\n            return;\n        }\n    }\n\n    /**\n     * 动态添加多图裁剪底部预览图片列表\n     */\n    private void addPhotoRecyclerView() {\n        mRecyclerView = new RecyclerView(this);\n        mRecyclerView.setId(R.id.id_recycler);\n        mRecyclerView.setBackgroundColor(ContextCompat.getColor(this, R.color.ucrop_color_widget_background));\n        RelativeLayout.LayoutParams lp =\n                new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, dip2px(80));\n        mRecyclerView.setLayoutParams(lp);\n        LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);\n        mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);\n        mRecyclerView.setLayoutManager(mLayoutManager);\n        resetCutDataStatus();\n        list.get(cutIndex).setCut(true);\n        adapter = new PicturePhotoGalleryAdapter(this, list);\n        mRecyclerView.setAdapter(adapter);\n        adapter.setOnItemClickListener((position, view) -> {\n            if (cutIndex == position) {\n                return;\n            }\n            cutIndex = position;\n            resetCutData();\n        });\n\n        uCropMultiplePhotoBox.addView(mRecyclerView);\n        changeLayoutParams();\n        FrameLayout uCropFrame = findViewById(R.id.ucrop_frame);\n        ((RelativeLayout.LayoutParams) uCropFrame.getLayoutParams())\n                .addRule(RelativeLayout.ABOVE, R.id.id_recycler);\n    }\n\n    /**\n     * 切换裁剪图片\n     */\n    private void refreshPhotoRecyclerData() {\n        resetCutDataStatus();\n        list.get(cutIndex).setCut(true);\n        adapter.notifyDataSetChanged();\n\n        uCropMultiplePhotoBox.addView(mRecyclerView);\n        changeLayoutParams();\n        FrameLayout uCropFrame = findViewById(R.id.ucrop_frame);\n        ((RelativeLayout.LayoutParams) uCropFrame.getLayoutParams())\n                .addRule(RelativeLayout.ABOVE, R.id.id_recycler);\n    }\n\n    /**\n     * 重置数据裁剪状态\n     */\n    private void resetCutDataStatus() {\n        int size = list.size();\n        for (int i = 0; i < size; i++) {\n            CutInfo cutInfo = list.get(i);\n            cutInfo.setCut(false);\n        }\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(final Menu menu) {\n        getMenuInflater().inflate(R.menu.ucrop_menu_activity, menu);\n        // Change crop & loader menu icons color to match the rest of the UI colors\n        MenuItem menuItemLoader = menu.findItem(R.id.menu_loader);\n        Drawable menuItemLoaderIcon = menuItemLoader.getIcon();\n        if (menuItemLoaderIcon != null) {\n            try {\n                menuItemLoaderIcon.mutate();\n                menuItemLoaderIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);\n                menuItemLoader.setIcon(menuItemLoaderIcon);\n            } catch (IllegalStateException e) {\n                Log.i(TAG, String.format(\"%s - %s\", e.getMessage(), getString(R.string.ucrop_mutate_exception_hint)));\n            }\n            ((Animatable) menuItemLoader.getIcon()).start();\n        }\n\n        MenuItem menuItemCrop = menu.findItem(R.id.menu_crop);\n        Drawable menuItemCropIcon = ContextCompat.getDrawable(this, mToolbarCropDrawable);\n        if (menuItemCropIcon != null) {\n            menuItemCropIcon.mutate();\n            menuItemCropIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);\n            menuItemCrop.setIcon(menuItemCropIcon);\n        }\n        return true;\n    }\n\n    @Override\n    public boolean onPrepareOptionsMenu(Menu menu) {\n        menu.findItem(R.id.menu_crop).setVisible(!mShowLoader);\n        menu.findItem(R.id.menu_loader).setVisible(mShowLoader);\n        return super.onPrepareOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if (item.getItemId() == R.id.menu_crop) {\n            cropAndSaveImage();\n        } else if (item.getItemId() == android.R.id.home) {\n            onBackPressed();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    public void onBackPressed() {\n        super.onBackPressed();\n        exitAnimation();\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n        if (mGestureCropImageView != null) {\n            mGestureCropImageView.cancelAllAnimations();\n        }\n    }\n\n    /**\n     * This method extracts all data from the incoming intent and setups views properly.\n     */\n    private void setImageData(@NonNull Intent intent) {\n        Uri inputUri = intent.getParcelableExtra(UCropMulti.EXTRA_INPUT_URI);\n        Uri outputUri = intent.getParcelableExtra(UCropMulti.EXTRA_OUTPUT_URI);\n        processOptions(intent);\n\n        if (inputUri != null && outputUri != null) {\n            try {\n                ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(inputUri, \"r\");\n                FileInputStream inputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());\n                String suffix = FileUtils.extSuffix(inputStream);\n                boolean isGif = FileUtils.isGifForSuffix(suffix);\n                mGestureCropImageView.setRotateEnabled(isGif ? false : rotateEnabled);\n                mGestureCropImageView.setScaleEnabled(isGif ? false : scaleEnabled);\n                mGestureCropImageView.setImageUri(inputUri, outputUri);\n            } catch (Exception e) {\n                setResultError(e);\n                closeActivity();\n            }\n        } else {\n            setResultError(new NullPointerException(getString(R.string.ucrop_error_input_data_is_absent)));\n            closeActivity();\n        }\n    }\n\n    /**\n     * This method extracts {@link UCrop.Options #optionsBundle} from incoming intent\n     * and setups Activity, {@link OverlayView} and {@link CropImageView} properly.\n     */\n    @SuppressWarnings(\"deprecation\")\n    private void processOptions(@NonNull Intent intent) {\n        // Bitmap compression options\n        String compressionFormatName = intent.getStringExtra(UCropMulti.Options.EXTRA_COMPRESSION_FORMAT_NAME);\n        Bitmap.CompressFormat compressFormat = null;\n        if (!TextUtils.isEmpty(compressionFormatName)) {\n            compressFormat = Bitmap.CompressFormat.valueOf(compressionFormatName);\n        }\n        mCompressFormat = (compressFormat == null) ? DEFAULT_COMPRESS_FORMAT : compressFormat;\n\n        mCompressQuality = intent.getIntExtra(UCrop.Options.EXTRA_COMPRESSION_QUALITY, PictureMultiCuttingActivity.DEFAULT_COMPRESS_QUALITY);\n\n        // Gestures options\n        int[] allowedGestures = intent.getIntArrayExtra(UCropMulti.Options.EXTRA_ALLOWED_GESTURES);\n        if (allowedGestures != null && allowedGestures.length == TABS_COUNT) {\n            mAllowedGestures = allowedGestures;\n        }\n\n        // Crop image view options\n        mGestureCropImageView.setMaxBitmapSize(intent.getIntExtra(UCropMulti.Options.EXTRA_MAX_BITMAP_SIZE, CropImageView.DEFAULT_MAX_BITMAP_SIZE));\n        mGestureCropImageView.setMaxScaleMultiplier(intent.getFloatExtra(UCropMulti.Options.EXTRA_MAX_SCALE_MULTIPLIER, CropImageView.DEFAULT_MAX_SCALE_MULTIPLIER));\n        mGestureCropImageView.setImageToWrapCropBoundsAnimDuration(intent.getIntExtra(UCropMulti.Options.EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, CropImageView.DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION));\n\n        // Overlay view options\n        mOverlayView.setDragFrame(isDragFrame);\n        mOverlayView.setFreestyleCropEnabled(intent.getBooleanExtra(UCropMulti.Options.EXTRA_FREE_STYLE_CROP, false));\n        circleDimmedLayer = intent.getBooleanExtra(UCropMulti.Options.EXTRA_CIRCLE_DIMMED_LAYER, OverlayView.DEFAULT_CIRCLE_DIMMED_LAYER);\n        mOverlayView.setDimmedColor(intent.getIntExtra(UCropMulti.Options.EXTRA_DIMMED_LAYER_COLOR, getResources().getColor(R.color.ucrop_color_default_dimmed)));\n        mOverlayView.setCircleDimmedLayer(circleDimmedLayer);\n\n        mOverlayView.setShowCropFrame(intent.getBooleanExtra(UCropMulti.Options.EXTRA_SHOW_CROP_FRAME, OverlayView.DEFAULT_SHOW_CROP_FRAME));\n        mOverlayView.setCropFrameColor(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_FRAME_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_frame)));\n        mOverlayView.setCropFrameStrokeWidth(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_FRAME_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width)));\n\n        mOverlayView.setShowCropGrid(intent.getBooleanExtra(UCropMulti.Options.EXTRA_SHOW_CROP_GRID, OverlayView.DEFAULT_SHOW_CROP_GRID));\n        mOverlayView.setCropGridRowCount(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_GRID_ROW_COUNT, OverlayView.DEFAULT_CROP_GRID_ROW_COUNT));\n        mOverlayView.setCropGridColumnCount(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_GRID_COLUMN_COUNT, OverlayView.DEFAULT_CROP_GRID_COLUMN_COUNT));\n        mOverlayView.setCropGridColor(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_GRID_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_grid)));\n        mOverlayView.setCropGridStrokeWidth(intent.getIntExtra(UCropMulti.Options.EXTRA_CROP_GRID_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width)));\n\n        // Aspect ratio options\n        float aspectRatioX = intent.getFloatExtra(UCropMulti.EXTRA_ASPECT_RATIO_X, 0);\n        float aspectRatioY = intent.getFloatExtra(UCropMulti.EXTRA_ASPECT_RATIO_Y, 0);\n\n        int aspectRationSelectedByDefault = intent.getIntExtra(UCropMulti.Options.EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, 0);\n        ArrayList<AspectRatio> aspectRatioList = intent.getParcelableArrayListExtra(UCropMulti.Options.EXTRA_ASPECT_RATIO_OPTIONS);\n\n        if (aspectRatioX > 0 && aspectRatioY > 0) {\n            mGestureCropImageView.setTargetAspectRatio(aspectRatioX / aspectRatioY);\n        } else if (aspectRatioList != null && aspectRationSelectedByDefault < aspectRatioList.size()) {\n            mGestureCropImageView.setTargetAspectRatio(aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioX() /\n                    aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioY());\n        } else {\n            mGestureCropImageView.setTargetAspectRatio(CropImageView.SOURCE_IMAGE_ASPECT_RATIO);\n        }\n\n        // Result bitmap max size options\n        int maxSizeX = intent.getIntExtra(UCropMulti.EXTRA_MAX_SIZE_X, 0);\n        int maxSizeY = intent.getIntExtra(UCropMulti.EXTRA_MAX_SIZE_Y, 0);\n\n        if (maxSizeX > 0 && maxSizeY > 0) {\n            mGestureCropImageView.setMaxResultImageSizeX(maxSizeX);\n            mGestureCropImageView.setMaxResultImageSizeY(maxSizeY);\n        }\n    }\n\n    private void getIntentData(@NonNull Intent intent) {\n        openWhiteStatusBar = intent.getBooleanExtra(UCrop.Options.EXTRA_UCROP_WIDGET_CROP_OPEN_WHITE_STATUSBAR, false);\n        mStatusBarColor = intent.getIntExtra(UCropMulti.Options.EXTRA_STATUS_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_statusbar));\n        mToolbarColor = intent.getIntExtra(UCropMulti.Options.EXTRA_TOOL_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar));\n        if (mToolbarColor == 0) {\n            mToolbarColor = ContextCompat.getColor(this, R.color.ucrop_color_toolbar);\n        }\n        if (mStatusBarColor == 0) {\n            mStatusBarColor = ContextCompat.getColor(this, R.color.ucrop_color_statusbar);\n        }\n    }\n\n    private void setupViews(@NonNull Intent intent) {\n        scaleEnabled = intent.getBooleanExtra(UCropMulti.Options.EXTRA_SCALE, false);\n        rotateEnabled = intent.getBooleanExtra(UCropMulti.Options.EXTRA_ROTATE, false);\n        // 是否可拖动裁剪框\n        isDragFrame = intent.getBooleanExtra(UCrop.Options.EXTRA_DRAG_CROP_FRAME, true);\n        mActiveWidgetColor = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_COLOR_WIDGET_ACTIVE, ContextCompat.getColor(this, R.color.ucrop_color_widget_active));\n        mToolbarWidgetColor = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar_widget));\n        if (mToolbarWidgetColor == 0) {\n            mToolbarWidgetColor = ContextCompat.getColor(this, R.color.ucrop_color_toolbar_widget);\n        }\n        mToolbarCancelDrawable = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, R.drawable.ucrop_ic_cross);\n        mToolbarCropDrawable = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_WIDGET_CROP_DRAWABLE, R.drawable.ucrop_ic_done);\n        mToolbarTitle = intent.getStringExtra(UCropMulti.Options.EXTRA_UCROP_TITLE_TEXT_TOOLBAR);\n        mToolbarTitle = mToolbarTitle != null ? mToolbarTitle : getResources().getString(R.string.ucrop_label_edit_photo);\n        mLogoColor = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_LOGO_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_default_logo));\n        mRootViewBackgroundColor = intent.getIntExtra(UCropMulti.Options.EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_crop_background));\n        setNavBarColor();\n        setupAppBar();\n        initiateRootViews();\n\n        changeLayoutParams();\n    }\n\n    /**\n     * set NavBar Color\n     */\n    private void setNavBarColor() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            int navBarColor = getIntent().getIntExtra(UCropMulti.EXTRA_NAV_BAR_COLOR, 0);\n            if (navBarColor != 0) {\n                getWindow().setNavigationBarColor(navBarColor);\n            }\n        }\n    }\n\n    /**\n     * Configures and styles both status bar and toolbar.\n     */\n    private void setupAppBar() {\n        setStatusBarColor(mStatusBarColor);\n\n        final Toolbar toolbar = findViewById(R.id.toolbar);\n\n        // Set all of the Toolbar coloring\n        toolbar.setBackgroundColor(mToolbarColor);\n        toolbar.setTitleTextColor(mToolbarWidgetColor);\n\n        final TextView toolbarTitle = toolbar.findViewById(R.id.toolbar_title);\n        toolbarTitle.setTextColor(mToolbarWidgetColor);\n        toolbarTitle.setText(mToolbarTitle);\n\n        // Color buttons inside the Toolbar\n        Drawable stateButtonDrawable = ContextCompat.getDrawable(this, mToolbarCancelDrawable).mutate();\n        stateButtonDrawable.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);\n        toolbar.setNavigationIcon(stateButtonDrawable);\n        setSupportActionBar(toolbar);\n        final ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayShowTitleEnabled(false);\n        }\n    }\n\n    private void initiateRootViews() {\n        mUCropView = findViewById(R.id.ucrop);\n        mGestureCropImageView = mUCropView.getCropImageView();\n        mOverlayView = mUCropView.getOverlayView();\n        mGestureCropImageView.setTransformImageListener(mImageListener);\n\n//        ((ImageView) findViewById(R.id.image_view_logo)).setColorFilter(mLogoColor, PorterDuff.Mode.SRC_ATOP);\n//\n//        findViewById(R.id.ucrop_frame).setBackgroundColor(mRootViewBackgroundColor);\n    }\n\n    private TransformImageView.TransformImageListener mImageListener = new TransformImageView.TransformImageListener() {\n        @Override\n        public void onRotate(float currentAngle) {\n            setAngleText(currentAngle);\n        }\n\n        @Override\n        public void onScale(float currentScale) {\n            setScaleText(currentScale);\n        }\n\n        @Override\n        public void onLoadComplete() {\n            mUCropView.animate().alpha(1).setDuration(300).setInterpolator(new AccelerateInterpolator());\n            mBlockingView.setClickable(false);\n            mShowLoader = false;\n            supportInvalidateOptionsMenu();\n        }\n\n        @Override\n        public void onLoadFailure(@NonNull Exception e) {\n            setResultError(e);\n            closeActivity();\n        }\n\n    };\n\n    /**\n     * Sets status-bar color for L devices.\n     *\n     * @param color - status-bar color\n     */\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    private void setStatusBarColor(@ColorInt int color) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            final Window window = getWindow();\n            if (window != null) {\n                window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n                window.setStatusBarColor(color);\n            }\n        }\n    }\n\n    private void setAngleText(float angle) {\n        if (mTextViewRotateAngle != null) {\n            mTextViewRotateAngle.setText(String.format(Locale.getDefault(), \"%.1f°\", angle));\n        }\n    }\n\n    private void setScaleText(float scale) {\n        if (mTextViewScalePercent != null) {\n            mTextViewScalePercent.setText(String.format(Locale.getDefault(), \"%d%%\", (int) (scale * 100)));\n        }\n    }\n\n    private void resetRotation() {\n        mGestureCropImageView.postRotate(-mGestureCropImageView.getCurrentAngle());\n        mGestureCropImageView.setImageToWrapCropBounds();\n    }\n\n    private void rotateByAngle(int angle) {\n        mGestureCropImageView.postRotate(angle);\n        mGestureCropImageView.setImageToWrapCropBounds();\n    }\n\n    private void setInitialState() {\n        setAllowedGestures(0);\n    }\n\n    private void setAllowedGestures(int tab) {\n        //mGestureCropImageView.setScaleEnabled(mAllowedGestures[tab] == ALL || mAllowedGestures[tab] == SCALE);\n        //mGestureCropImageView.setRotateEnabled(mAllowedGestures[tab] == ALL || mAllowedGestures[tab] == ROTATE);\n    }\n\n    /**\n     * Adds view that covers everything below the Toolbar.\n     * When it's clickable - user won't be able to click/touch anything below the Toolbar.\n     * Need to block user input while loading and cropping an image.\n     */\n    private void addBlockingView() {\n        if (mBlockingView == null) {\n            mBlockingView = new View(this);\n            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n            lp.addRule(RelativeLayout.BELOW, R.id.toolbar);\n            mBlockingView.setLayoutParams(lp);\n            mBlockingView.setClickable(true);\n        }\n        uCropMultiplePhotoBox.addView(mBlockingView);\n    }\n\n    protected void cropAndSaveImage() {\n        mBlockingView.setClickable(true);\n        mShowLoader = true;\n        supportInvalidateOptionsMenu();\n        mGestureCropImageView.cropAndSaveImage(mCompressFormat, mCompressQuality, false, new BitmapCropCallback() {\n\n            @Override\n            public void onBitmapCropped(@NonNull Uri resultUri, int offsetX, int offsetY, int imageWidth, int imageHeight) {\n                setResultUri(resultUri, mGestureCropImageView.getTargetAspectRatio(), offsetX, offsetY, imageWidth, imageHeight);\n            }\n\n            @Override\n            public void onCropFailure(@NonNull Throwable t) {\n                setResultError(t);\n                closeActivity();\n            }\n        });\n    }\n\n    protected void setResultUri(Uri uri, float resultAspectRatio, int offsetX, int offsetY, int imageWidth, int imageHeight) {\n        try {\n//            CutInfo info = list.get(cutIndex);\n//            info.setCutPath(uri.getPath());\n//            info.setCut(true);\n//            info.setResultAspectRatio(resultAspectRatio);\n//            info.setOffsetX(offsetX);\n//            info.setOffsetY(offsetY);\n//            info.setImageWidth(imageWidth);\n//            info.setImageHeight(imageHeight);\n            cutIndex++;\n            if (cutIndex >= list.size()) {\n                setResult(RESULT_OK, new Intent()\n                        .putExtra(UCropMulti.EXTRA_OUTPUT_URI_LIST, list)\n                );\n                closeActivity();\n            } else {\n                resetCutData();\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n\n    }\n\n    /**\n     * 重置裁剪参数\n     */\n    protected void resetCutData() {\n        uCropMultiplePhotoBox.removeView(mRecyclerView);\n        setContentView(R.layout.ucrop_picture_activity_multi_cutting);\n        uCropMultiplePhotoBox = findViewById(R.id.ucrop_mulit_photobox);\n        Intent intent = getIntent();\n        Bundle extras = intent.getExtras();\n        boolean isAndroidQ = VersionUtils.isAndroidQ();\n        String path = list.get(cutIndex).getPath();\n        boolean isHttp = FileUtils.isHttp(path);\n        String imgType = getLastImgType(isAndroidQ ? FileUtils.getPath(this, Uri.parse(path)) : path);\n        Uri uri = isHttp || isAndroidQ ? Uri.parse(path) : Uri.fromFile(new File(path));\n        extras.putParcelable(UCropMulti.EXTRA_INPUT_URI, uri);\n\n        File file = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ?\n                getExternalFilesDir(Environment.DIRECTORY_PICTURES) : getCacheDir();\n        extras.putParcelable(UCropMulti.EXTRA_OUTPUT_URI,\n                Uri.fromFile(new File(file, FileUtils.getCreateFileName(\"IMG_\") + imgType)));\n        intent.putExtras(extras);\n        refreshPhotoRecyclerData();\n        setupViews(intent);\n        setImageData(intent);\n        // 预览图 一页5个,裁剪到第6个的时候滚动到最新位置，不然预览图片看不到\n        if (cutIndex >= 5) {\n            mRecyclerView.scrollToPosition(cutIndex);\n        }\n        changeLayoutParams();\n    }\n\n    private void changeLayoutParams() {\n        if (mRecyclerView.getLayoutParams() == null) {\n            return;\n        }\n        ((RelativeLayout.LayoutParams) mRecyclerView.getLayoutParams())\n                .addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);\n\n        ((RelativeLayout.LayoutParams) mRecyclerView.getLayoutParams())\n                .addRule(RelativeLayout.ABOVE, 0);\n    }\n\n    /**\n     * 获取图片后缀\n     *\n     * @param path\n     * @return\n     */\n    public static String getLastImgType(String path) {\n        try {\n            int index = path.lastIndexOf(\".\");\n            if (index > 0) {\n                String imageType = path.substring(index);\n                switch (imageType) {\n                    case \".png\":\n                    case \".PNG\":\n                    case \".jpg\":\n                    case \".jpeg\":\n                    case \".JPEG\":\n                    case \".WEBP\":\n                    case \".bmp\":\n                    case \".BMP\":\n                    case \".webp\":\n                    case \".gif\":\n                    case \".GIF\":\n                        return imageType;\n                    default:\n                        return \".png\";\n                }\n            } else {\n                return \".png\";\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n            return \".png\";\n        }\n    }\n\n    protected void setResultError(Throwable throwable) {\n        setResult(UCropMulti.RESULT_ERROR, new Intent().putExtra(UCropMulti.EXTRA_ERROR, throwable));\n    }\n\n    /**\n     * exit activity\n     */\n    protected void closeActivity() {\n        finish();\n        exitAnimation();\n    }\n\n    protected void exitAnimation() {\n        int exitAnimation = getIntent().getIntExtra(UCropMulti.EXTRA_WINDOW_EXIT_ANIMATION, 0);\n        overridePendingTransition(R.anim.ucrop_anim_fade_in, exitAnimation != 0 ? exitAnimation : R.anim.ucrop_close);\n    }\n\n    public int dip2px(float dpValue) {\n        return (int) (0.5f + dpValue * getResources().getDisplayMetrics().density);\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/PicturePhotoGalleryAdapter.java",
    "content": "/*\n * Copyright (C) 2014 pengjianbo(pengjianbosoft@gmail.com), Inc.\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.matisse.ucrop;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\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 com.matisse.R;\nimport com.matisse.ucrop.callback.BitmapLoadShowCallback;\nimport com.matisse.ucrop.model.CutInfo;\nimport com.matisse.ucrop.util.BitmapLoadUtils;\nimport com.matisse.ucrop.util.VersionUtils;\nimport com.matisse.ucrop.util.FileUtils;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * @author：luck\n * @date：2016-12-31 22:22\n * @describe：图片列表\n */\n\n\npublic class PicturePhotoGalleryAdapter extends RecyclerView.Adapter<PicturePhotoGalleryAdapter.ViewHolder> {\n    private final int maxImageWidth = 200;\n    private final int maxImageHeight = 220;\n    private Context context;\n    private List<CutInfo> list;\n    private LayoutInflater mInflater;\n    private boolean isAndroidQ;\n\n    public PicturePhotoGalleryAdapter(Context context, List<CutInfo> list) {\n        mInflater = LayoutInflater.from(context);\n        this.context = context;\n        this.list = list;\n        this.isAndroidQ = VersionUtils.isAndroidQ();\n    }\n\n    public void setData(List<CutInfo> list) {\n        this.list = list;\n        notifyDataSetChanged();\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {\n        View view = mInflater.inflate(R.layout.ucrop_picture_gf_adapter_edit_list,\n                parent, false);\n        return new ViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(ViewHolder holder, int position) {\n        String path = \"\";\n        CutInfo photoInfo = list.get(position);\n        if (photoInfo != null) {\n            path = photoInfo.getPath();\n        }\n        if (photoInfo.isCut()) {\n            holder.iv_dot.setVisibility(View.VISIBLE);\n            holder.iv_dot.setImageResource(R.drawable.ucrop_oval_true);\n        } else {\n            holder.iv_dot.setVisibility(View.INVISIBLE);\n        }\n\n        Uri uri = isAndroidQ ? Uri.parse(path) : Uri.fromFile(new File(path));\n        holder.tvGif.setVisibility(FileUtils.isGif(photoInfo.getMimeType()) ? View.VISIBLE : View.GONE);\n        BitmapLoadUtils.decodeBitmapInBackground(context, uri, maxImageWidth,\n                maxImageHeight,\n                new BitmapLoadShowCallback() {\n\n                    @Override\n                    public void onBitmapLoaded(@NonNull Bitmap bitmap) {\n                        if (holder.mIvPhoto != null) {\n                            holder.mIvPhoto.setImageBitmap(bitmap);\n                        }\n                    }\n\n                    @Override\n                    public void onFailure(@NonNull Exception bitmapWorkerException) {\n                        if (holder.mIvPhoto != null) {\n                            holder.mIvPhoto.setImageResource(R.color.ucrop_color_ba3);\n                        }\n                    }\n                });\n\n        holder.itemView.setOnClickListener(v -> {\n            if (listener != null) {\n                listener.onItemClick(holder.getAdapterPosition(), v);\n            }\n        });\n    }\n\n\n    @Override\n    public int getItemCount() {\n        return list != null ? list.size() : 0;\n    }\n\n\n    public static class ViewHolder extends RecyclerView.ViewHolder {\n        ImageView mIvPhoto;\n        ImageView iv_dot;\n        TextView tvGif;\n\n        public ViewHolder(View view) {\n            super(view);\n            mIvPhoto = view.findViewById(R.id.iv_photo);\n            iv_dot = view.findViewById(R.id.iv_dot);\n            tvGif = view.findViewById(R.id.tv_gif);\n        }\n    }\n\n    private OnItemClickListener listener;\n\n    public void setOnItemClickListener(OnItemClickListener listener) {\n        this.listener = listener;\n    }\n\n    public interface OnItemClickListener {\n        void onItemClick(int position, View view);\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/UCrop.java",
    "content": "package com.matisse.ucrop;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Parcelable;\n\nimport androidx.annotation.AnimRes;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.FloatRange;\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\n\nimport com.matisse.BuildConfig;\nimport com.matisse.R;\nimport com.matisse.ucrop.model.AspectRatio;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Locale;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n * <p/>\n * Builder class to ease Intent setup.\n */\npublic class UCrop {\n\n    public static final int REQUEST_CROP = 69;\n    public static final int RESULT_ERROR = 96;\n\n    private static final String EXTRA_PREFIX = BuildConfig.APPLICATION_ID;\n\n    public static final String EXTRA_INPUT_URI = EXTRA_PREFIX + \".InputUri\";\n    public static final String EXTRA_OUTPUT_URI = EXTRA_PREFIX + \".OutputUri\";\n    public static final String EXTRA_OUTPUT_CROP_ASPECT_RATIO = EXTRA_PREFIX + \".CropAspectRatio\";\n    public static final String EXTRA_OUTPUT_IMAGE_WIDTH = EXTRA_PREFIX + \".ImageWidth\";\n    public static final String EXTRA_OUTPUT_IMAGE_HEIGHT = EXTRA_PREFIX + \".ImageHeight\";\n    public static final String EXTRA_OUTPUT_OFFSET_X = EXTRA_PREFIX + \".OffsetX\";\n    public static final String EXTRA_OUTPUT_OFFSET_Y = EXTRA_PREFIX + \".OffsetY\";\n    public static final String EXTRA_ERROR = EXTRA_PREFIX + \".Error\";\n\n    public static final String EXTRA_ASPECT_RATIO_X = EXTRA_PREFIX + \".AspectRatioX\";\n    public static final String EXTRA_ASPECT_RATIO_Y = EXTRA_PREFIX + \".AspectRatioY\";\n\n    public static final String EXTRA_MAX_SIZE_X = EXTRA_PREFIX + \".MaxSizeX\";\n    public static final String EXTRA_MAX_SIZE_Y = EXTRA_PREFIX + \".MaxSizeY\";\n\n    public static final String EXTRA_WINDOW_EXIT_ANIMATION = EXTRA_PREFIX + \".WindowAnimation\";\n\n    public static final String EXTRA_NAV_BAR_COLOR = EXTRA_PREFIX + \".navBarColor\";\n\n    private Intent mCropIntent;\n    private Bundle mCropOptionsBundle;\n\n    /**\n     * This method creates new Intent builder and sets both source and destination image URIs.\n     *\n     * @param source      Uri for image to crop\n     * @param destination Uri for saving the cropped image\n     */\n    public static UCrop of(@NonNull Uri source, @NonNull Uri destination) {\n        return new UCrop(source, destination);\n    }\n\n    private UCrop(@NonNull Uri source, @NonNull Uri destination) {\n        mCropIntent = new Intent();\n        mCropOptionsBundle = new Bundle();\n        mCropOptionsBundle.putParcelable(EXTRA_INPUT_URI, source);\n        mCropOptionsBundle.putParcelable(EXTRA_OUTPUT_URI, destination);\n    }\n\n    /**\n     * Set an aspect ratio for crop bounds.\n     * User won't see the menu with other ratios options.\n     *\n     * @param x aspect ratio X\n     * @param y aspect ratio Y\n     */\n    public UCrop withAspectRatio(float x, float y) {\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);\n        return this;\n    }\n\n    /**\n     * Set an aspect ratio for crop bounds that is evaluated from source image width and height.\n     * User won't see the menu with other ratios options.\n     */\n    public UCrop useSourceImageAspectRatio() {\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);\n        return this;\n    }\n\n    /**\n     * Set maximum size for result cropped image.\n     *\n     * @param width  max cropped image width\n     * @param height max cropped image height\n     */\n    public UCrop withMaxResultSize(@IntRange(from = 100) int width, @IntRange(from = 100) int height) {\n        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_X, width);\n        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_Y, height);\n        return this;\n    }\n\n    public UCrop withOptions(@NonNull Options options) {\n        mCropOptionsBundle.putAll(options.getOptionBundle());\n        return this;\n    }\n\n    /**\n     * Send the crop Intent from animation an Activity\n     *\n     * @param activity Activity to receive result\n     */\n    public void startAnimation(@NonNull Activity activity, @AnimRes int activityCropEnterAnimation) {\n        if (activityCropEnterAnimation != 0) {\n            start(activity, REQUEST_CROP, activityCropEnterAnimation);\n        } else {\n            start(activity, REQUEST_CROP);\n        }\n    }\n\n    /**\n     * Send the crop Intent from an Activity with a custom request code or animation\n     *\n     * @param activity    Activity to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Activity activity, int requestCode, @AnimRes int activityCropEnterAnimation) {\n        activity.startActivityForResult(getIntent(activity), requestCode);\n        activity.overridePendingTransition(activityCropEnterAnimation, R.anim.ucrop_anim_fade_in);\n    }\n\n    /**\n     * Send the crop Intent from an Activity\n     *\n     * @param activity Activity to receive result\n     */\n    public void start(@NonNull Activity activity) {\n        start(activity, REQUEST_CROP);\n    }\n\n    /**\n     * Send the crop Intent from an Activity with a custom request code\n     *\n     * @param activity    Activity to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Activity activity, int requestCode) {\n        activity.startActivityForResult(getIntent(activity), requestCode);\n    }\n\n    /**\n     * Send the crop Intent from a Fragment\n     *\n     * @param fragment Fragment to receive result\n     */\n    public void start(@NonNull Context context, @NonNull Fragment fragment) {\n        start(context, fragment, REQUEST_CROP);\n    }\n\n    /**\n     * Send the crop Intent with a custom request code\n     *\n     * @param fragment    Fragment to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Context context, @NonNull Fragment fragment, int requestCode) {\n        fragment.startActivityForResult(getIntent(context), requestCode);\n    }\n\n    /**\n     * Get Intent to start {@link UCropActivity}\n     *\n     * @return Intent for {@link UCropActivity}\n     */\n    public Intent getIntent(@NonNull Context context) {\n        mCropIntent.setClass(context, UCropActivity.class);\n        mCropIntent.putExtras(mCropOptionsBundle);\n        return mCropIntent;\n    }\n\n    /**\n     * Retrieve cropped image Uri from the result Intent\n     *\n     * @param intent crop result intent\n     */\n    @Nullable\n    public static Uri getOutput(@NonNull Intent intent) {\n        return intent.getParcelableExtra(EXTRA_OUTPUT_URI);\n    }\n\n    /**\n     * Retrieve the width of the cropped image\n     *\n     * @param intent crop result intent\n     */\n    public static int getOutputImageWidth(@NonNull Intent intent) {\n        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_WIDTH, -1);\n    }\n\n    /**\n     * Retrieve the height of the cropped image\n     *\n     * @param intent crop result intent\n     */\n    public static int getOutputImageHeight(@NonNull Intent intent) {\n        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_HEIGHT, -1);\n    }\n\n    /**\n     * Retrieve cropped image aspect ratio from the result Intent\n     *\n     * @param intent crop result intent\n     * @return aspect ratio as a floating point value (x:y) - so it will be 1 for 1:1 or 4/3 for 4:3\n     */\n    public static float getOutputCropAspectRatio(@NonNull Intent intent) {\n        return intent.getFloatExtra(EXTRA_OUTPUT_CROP_ASPECT_RATIO, 1);\n    }\n\n    /**\n     * Method retrieves error from the result intent.\n     *\n     * @param result crop result Intent\n     * @return Throwable that could happen while image processing\n     */\n    @Nullable\n    public static Throwable getError(@NonNull Intent result) {\n        return (Throwable) result.getSerializableExtra(EXTRA_ERROR);\n    }\n\n\n    /**\n     * Class that helps to setup advanced configs that are not commonly used.\n     * Use it with method {@link #withOptions(Options)}\n     */\n    public static class Options {\n\n        public static final String EXTRA_COMPRESSION_FORMAT_NAME = EXTRA_PREFIX + \".CompressionFormatName\";\n        public static final String EXTRA_COMPRESSION_QUALITY = EXTRA_PREFIX + \".CompressionQuality\";\n\n        public static final String EXTRA_ALLOWED_GESTURES = EXTRA_PREFIX + \".AllowedGestures\";\n\n        public static final String EXTRA_MAX_BITMAP_SIZE = EXTRA_PREFIX + \".MaxBitmapSize\";\n        public static final String EXTRA_MAX_SCALE_MULTIPLIER = EXTRA_PREFIX + \".MaxScaleMultiplier\";\n        public static final String EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = EXTRA_PREFIX + \".ImageToCropBoundsAnimDuration\";\n\n        public static final String EXTRA_DIMMED_LAYER_COLOR = EXTRA_PREFIX + \".DimmedLayerColor\";\n        public static final String EXTRA_CIRCLE_DIMMED_LAYER = EXTRA_PREFIX + \".CircleDimmedLayer\";\n\n        public static final String EXTRA_SHOW_CROP_FRAME = EXTRA_PREFIX + \".ShowCropFrame\";\n        public static final String EXTRA_CROP_FRAME_COLOR = EXTRA_PREFIX + \".CropFrameColor\";\n        public static final String EXTRA_CROP_FRAME_STROKE_WIDTH = EXTRA_PREFIX + \".CropFrameStrokeWidth\";\n\n        public static final String EXTRA_SHOW_CROP_GRID = EXTRA_PREFIX + \".ShowCropGrid\";\n        public static final String EXTRA_CROP_GRID_ROW_COUNT = EXTRA_PREFIX + \".CropGridRowCount\";\n        public static final String EXTRA_CROP_GRID_COLUMN_COUNT = EXTRA_PREFIX + \".CropGridColumnCount\";\n        public static final String EXTRA_CROP_GRID_COLOR = EXTRA_PREFIX + \".CropGridColor\";\n        public static final String EXTRA_CROP_GRID_STROKE_WIDTH = EXTRA_PREFIX + \".CropGridStrokeWidth\";\n\n        public static final String EXTRA_TOOL_BAR_COLOR = EXTRA_PREFIX + \".ToolbarColor\";\n        public static final String EXTRA_STATUS_BAR_COLOR = EXTRA_PREFIX + \".StatusBarColor\";\n        public static final String EXTRA_UCROP_COLOR_WIDGET_ACTIVE = EXTRA_PREFIX + \".UcropColorWidgetActive\";\n\n        public static final String EXTRA_UCROP_WIDGET_COLOR_TOOLBAR = EXTRA_PREFIX + \".UcropToolbarWidgetColor\";\n        public static final String EXTRA_UCROP_TITLE_TEXT_TOOLBAR = EXTRA_PREFIX + \".UcropToolbarTitleText\";\n        public static final String EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE = EXTRA_PREFIX + \".UcropToolbarCancelDrawable\";\n        public static final String EXTRA_UCROP_WIDGET_CROP_DRAWABLE = EXTRA_PREFIX + \".UcropToolbarCropDrawable\";\n\n        public static final String EXTRA_UCROP_WIDGET_CROP_OPEN_WHITE_STATUSBAR = EXTRA_PREFIX + \".openWhiteStatusBar\";\n\n        public static final String EXTRA_UCROP_LOGO_COLOR = EXTRA_PREFIX + \".UcropLogoColor\";\n\n        public static final String EXTRA_FREE_STYLE_CROP = EXTRA_PREFIX + \".FreeStyleCrop\";\n\n        public static final String EXTRA_CUT_CROP = EXTRA_PREFIX + \".cuts\";\n\n        public static final String EXTRA_FREE_STATUS_FONT = EXTRA_PREFIX + \".StatusFont\";\n\n        public static final String EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT = EXTRA_PREFIX + \".AspectRatioSelectedByDefault\";\n        public static final String EXTRA_ASPECT_RATIO_OPTIONS = EXTRA_PREFIX + \".AspectRatioOptions\";\n\n        public static final String EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR = EXTRA_PREFIX + \".UcropRootViewBackgroundColor\";\n\n        public static final String EXTRA_DRAG_CROP_FRAME = EXTRA_PREFIX + \".DragCropFrame\";\n\n        private final Bundle mOptionBundle;\n\n        public Options() {\n            mOptionBundle = new Bundle();\n        }\n\n        @NonNull\n        public Bundle getOptionBundle() {\n            return mOptionBundle;\n        }\n\n        /**\n         * Set one of {@link Bitmap.CompressFormat} that will be used to save resulting Bitmap.\n         */\n        public Options setCompressionFormat(@NonNull Bitmap.CompressFormat format) {\n            mOptionBundle.putString(EXTRA_COMPRESSION_FORMAT_NAME, format.name());\n            return this;\n        }\n\n        /**\n         * Set compression quality [0-100] that will be used to save resulting Bitmap.\n         */\n        public Options setCompressionQuality(@IntRange(from = 0) int compressQuality) {\n            mOptionBundle.putInt(EXTRA_COMPRESSION_QUALITY, compressQuality);\n            return this;\n        }\n\n        /**\n         * Choose what set of gestures will be enabled on each tab - if any.\n         */\n        public Options setAllowedGestures(@UCropActivity.GestureTypes int tabScale,\n                                       @UCropActivity.GestureTypes int tabRotate,\n                                       @UCropActivity.GestureTypes int tabAspectRatio) {\n            mOptionBundle.putIntArray(EXTRA_ALLOWED_GESTURES, new int[]{tabScale, tabRotate, tabAspectRatio});\n            return this;\n        }\n\n        /**\n         * This method sets multiplier that is used to calculate max image scale from min image scale.\n         *\n         * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale\n         */\n        public Options setMaxScaleMultiplier(@FloatRange(from = 1.0, fromInclusive = false) float maxScaleMultiplier) {\n            mOptionBundle.putFloat(EXTRA_MAX_SCALE_MULTIPLIER, maxScaleMultiplier);\n            return this;\n        }\n\n        /**\n         * This method sets animation duration for image to wrap the crop bounds\n         *\n         * @param durationMillis - duration in milliseconds\n         */\n        public Options setImageToCropBoundsAnimDuration(@IntRange(from = 100) int durationMillis) {\n            mOptionBundle.putInt(EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, durationMillis);\n            return this;\n        }\n\n        /**\n         * Setter for max size for both width and height of bitmap that will be decoded from an input Uri and used in the view.\n         *\n         * @param maxBitmapSize - size in pixels\n         */\n        public Options setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize) {\n            mOptionBundle.putInt(EXTRA_MAX_BITMAP_SIZE, maxBitmapSize);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of dimmed area around the crop bounds\n         */\n        public Options setDimmedLayerColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_DIMMED_LAYER_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param isCircle - set it to true if you want dimmed layer to have an circle inside\n         */\n        public Options setCircleDimmedLayer(boolean isCircle) {\n            mOptionBundle.putBoolean(EXTRA_CIRCLE_DIMMED_LAYER, isCircle);\n            return this;\n        }\n\n        /**\n         * @param show - set to true if you want to see a crop frame rectangle on top of an image\n         */\n        public Options setShowCropFrame(boolean show) {\n            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_FRAME, show);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of crop frame\n         */\n        public Options setCropFrameColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_CROP_FRAME_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param width - desired width of crop frame line in pixels\n         */\n        public Options setCropFrameStrokeWidth(@IntRange(from = 0) int width) {\n            mOptionBundle.putInt(EXTRA_CROP_FRAME_STROKE_WIDTH, width);\n            return this;\n        }\n\n        /**\n         * @param show - set to true if you want to see a crop grid/guidelines on top of an image\n         */\n        public Options setShowCropGrid(boolean show) {\n            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_GRID, show);\n            return this;\n        }\n\n        /**\n         * @param isDragFrame - 是否可拖动裁剪框\n         */\n        public Options setDragFrameEnabled(boolean isDragFrame) {\n            mOptionBundle.putBoolean(EXTRA_DRAG_CROP_FRAME, isDragFrame);\n            return this;\n        }\n\n        /**\n         * @param count - crop grid rows count.\n         */\n        public Options setCropGridRowCount(@IntRange(from = 0) int count) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_ROW_COUNT, count);\n            return this;\n        }\n\n        /**\n         * @param count - crop grid columns count.\n         */\n        public Options setCropGridColumnCount(@IntRange(from = 0) int count) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_COLUMN_COUNT, count);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of crop grid/guidelines\n         */\n        public Options setCropGridColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param width - desired width of crop grid lines in pixels\n         */\n        public Options setCropGridStrokeWidth(@IntRange(from = 0) int width) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_STROKE_WIDTH, width);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the toolbar\n         */\n        public Options setToolbarColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_TOOL_BAR_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the statusbar\n         */\n        public Options setStatusBarColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_STATUS_BAR_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the active and selected widget (default is orange) and progress wheel middle line\n         */\n        public Options setActiveWidgetColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_COLOR_WIDGET_ACTIVE, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of Toolbar text and buttons (default is darker orange)\n         */\n        public Options setToolbarWidgetColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, color);\n            return this;\n        }\n\n        /**\n         * @param openWhiteStatusBar - Change the status bar font color\n         */\n        public Options isOpenWhiteStatusBar(boolean openWhiteStatusBar) {\n            mOptionBundle.putBoolean(EXTRA_UCROP_WIDGET_CROP_OPEN_WHITE_STATUSBAR, openWhiteStatusBar);\n            return this;\n        }\n\n        /**\n         * @param text - desired text for Toolbar title\n         */\n        public Options setToolbarTitle(@Nullable String text) {\n            mOptionBundle.putString(EXTRA_UCROP_TITLE_TEXT_TOOLBAR, text);\n            return this;\n        }\n\n        /**\n         * @param drawable - desired drawable for the Toolbar left cancel icon\n         */\n        public Options setToolbarCancelDrawable(@DrawableRes int drawable) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, drawable);\n            return this;\n        }\n\n        /**\n         * @param drawable - desired drawable for the Toolbar right crop icon\n         */\n        public Options setToolbarCropDrawable(@DrawableRes int drawable) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CROP_DRAWABLE, drawable);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of logo fill (default is darker grey)\n         */\n        public Options setLogoColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_LOGO_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param -set cuts path\n         */\n        public Options setCutListData(ArrayList<String> list) {\n            mOptionBundle.putStringArrayList(EXTRA_CUT_CROP, list);\n            return this;\n        }\n\n        /**\n         * @param enabled - set to true to let user resize crop bounds (disabled by default)\n         */\n        public Options setFreeStyleCropEnabled(boolean enabled) {\n            mOptionBundle.putBoolean(EXTRA_FREE_STYLE_CROP, enabled);\n            return this;\n        }\n\n        /**\n         * @param statusFont - Set status bar black\n         */\n        public Options setStatusFont(boolean statusFont) {\n            mOptionBundle.putBoolean(EXTRA_FREE_STATUS_FONT, statusFont);\n            return this;\n        }\n\n        /**\n         * Pass an ordered list of desired aspect ratios that should be available for a user.\n         *\n         * @param selectedByDefault - index of aspect ratio option that is selected by default (starts with 0).\n         * @param aspectRatio       - list of aspect ratio options that are available to user\n         */\n        public Options setAspectRatioOptions(int selectedByDefault, AspectRatio... aspectRatio) {\n            if (selectedByDefault > aspectRatio.length) {\n                throw new IllegalArgumentException(String.format(Locale.US,\n                        \"Index [selectedByDefault = %d] cannot be higher than aspect ratio options count [count = %d].\",\n                        selectedByDefault, aspectRatio.length));\n            }\n            mOptionBundle.putInt(EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, selectedByDefault);\n            mOptionBundle.putParcelableArrayList(EXTRA_ASPECT_RATIO_OPTIONS, new ArrayList<Parcelable>(Arrays.asList(aspectRatio)));\n            return this;\n        }\n\n        /**\n         * @param color - desired background color that should be applied to the root view\n         */\n        public Options setRootViewBackgroundColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, color);\n            return this;\n        }\n\n        /**\n         * Set an aspect ratio for crop bounds.\n         * User won't see the menu with other ratios options.\n         *\n         * @param x aspect ratio X\n         * @param y aspect ratio Y\n         */\n        public Options withAspectRatio(float x, float y) {\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);\n            return this;\n        }\n\n        /**\n         * Set an aspect ratio for crop bounds that is evaluated from source image width and height.\n         * User won't see the menu with other ratios options.\n         */\n        public Options useSourceImageAspectRatio() {\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);\n            return this;\n        }\n\n        /**\n         * Set maximum size for result cropped image.\n         *\n         * @param width  max cropped image width\n         * @param height max cropped image height\n         */\n        public Options withMaxResultSize(int width, int height) {\n            mOptionBundle.putInt(EXTRA_MAX_SIZE_X, width);\n            mOptionBundle.putInt(EXTRA_MAX_SIZE_Y, height);\n            return this;\n        }\n\n        /**\n         * @param activityCropExitAnimation activity exit animation\n         */\n        public Options setCropExitAnimation(@AnimRes int activityCropExitAnimation) {\n            mOptionBundle.putInt(EXTRA_WINDOW_EXIT_ANIMATION, activityCropExitAnimation);\n            return this;\n        }\n\n        /**\n         * @param navBarColor set NavBar Color\n         */\n        public Options setNavBarColor(@ColorInt int navBarColor) {\n            mOptionBundle.putInt(EXTRA_NAV_BAR_COLOR, navBarColor);\n            return this;\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/UCropActivity.java",
    "content": "package com.matisse.ucrop;\n\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.ParcelFileDescriptor;\nimport android.text.TextUtils;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AccelerateInterpolator;\nimport android.widget.LinearLayout;\nimport android.widget.RelativeLayout;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport com.matisse.R;\nimport com.matisse.ucrop.callback.BitmapCropCallback;\nimport com.matisse.ucrop.model.AspectRatio;\nimport com.matisse.ucrop.util.FileUtils;\nimport com.matisse.ucrop.view.CropImageView;\nimport com.matisse.ucrop.view.GestureCropImageView;\nimport com.matisse.ucrop.view.OverlayView;\nimport com.matisse.ucrop.view.TransformImageView;\nimport com.matisse.ucrop.view.UCropView;\nimport com.matisse.ui.activity.BaseActivity;\nimport com.matisse.utils.UIUtils;\n\nimport java.io.FileInputStream;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\n\n@SuppressWarnings(\"ConstantConditions\")\npublic class UCropActivity extends BaseActivity implements View.OnClickListener {\n\n    public static final int DEFAULT_COMPRESS_QUALITY = 100;\n    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;\n\n    public static final int NONE = 0;\n    public static final int SCALE = 1;\n    public static final int ROTATE = 2;\n    public static final int ALL = 3;\n\n    @IntDef({NONE, SCALE, ROTATE, ALL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface GestureTypes {\n    }\n\n    private UCropView mUCropView;\n    private GestureCropImageView mGestureCropImageView;\n    private OverlayView mOverlayView;\n    private View mBlockingView;\n\n    private Bitmap.CompressFormat mCompressFormat = DEFAULT_COMPRESS_FORMAT;\n    private int mCompressQuality = DEFAULT_COMPRESS_QUALITY;\n    private boolean mIsCircleCrop = false;\n\n    @Override\n    public int getResourceLayoutId() {\n        return R.layout.ucrop_activity_photobox;\n    }\n\n    @Override\n    public void configActivity() {\n        super.configActivity();\n        getSpec().getStatusBarFuture().invoke(this, findViewById(R.id.toolbar));\n    }\n\n    @Override\n    public void setViewData() {\n        final Intent intent = getIntent();\n        setupViews();\n        setImageData(intent);\n        addBlockingView();\n    }\n\n    @Override\n    public void initListener() {\n        UIUtils.setOnClickListener(this\n                , findViewById(R.id.button_complete), findViewById(R.id.button_back));\n    }\n\n    @Override\n    public void onClick(View view) {\n        int id = view.getId();\n        if (id == R.id.button_complete) {\n            cropAndSaveImage();\n        } else if (id == R.id.button_back) {\n            onBackPressed();\n        }\n    }\n\n    @Override\n    public void onBackPressed() {\n        super.onBackPressed();\n//        exitAnimation();\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n        if (mGestureCropImageView != null) {\n            mGestureCropImageView.cancelAllAnimations();\n        }\n    }\n\n    /**\n     * This method extracts all data from the incoming intent and setups views properly.\n     */\n    private void setImageData(@NonNull Intent intent) {\n        Uri inputUri = intent.getParcelableExtra(UCrop.EXTRA_INPUT_URI);\n        Uri outputUri = intent.getParcelableExtra(UCrop.EXTRA_OUTPUT_URI);\n        processOptions(intent);\n\n        if (inputUri != null && outputUri != null) {\n            try {\n                ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(inputUri, \"r\");\n                FileInputStream inputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());\n                String suffix = FileUtils.extSuffix(inputStream);\n                boolean isGif = FileUtils.isGifForSuffix(suffix);\n                mGestureCropImageView.setRotateEnabled(!isGif);\n                mGestureCropImageView.setScaleEnabled(!isGif);\n                mGestureCropImageView.setImageUri(inputUri, outputUri);\n            } catch (Exception e) {\n                setResultError(e);\n                closeActivity();\n            }\n        } else {\n            setResultError(new NullPointerException(getString(R.string.ucrop_error_input_data_is_absent)));\n            closeActivity();\n        }\n    }\n\n    /**\n     * This method extracts {@link UCrop.Options #optionsBundle} from incoming intent\n     * and setups Activity, {@link OverlayView} and {@link CropImageView} properly.\n     */\n    private void processOptions(@NonNull Intent intent) {\n        // Bitmap compression options\n        String compressionFormatName = intent.getStringExtra(UCrop.Options.EXTRA_COMPRESSION_FORMAT_NAME);\n        Bitmap.CompressFormat compressFormat = null;\n        if (!TextUtils.isEmpty(compressionFormatName)) {\n            compressFormat = Bitmap.CompressFormat.valueOf(compressionFormatName);\n        }\n        mCompressFormat = (compressFormat == null) ? DEFAULT_COMPRESS_FORMAT : compressFormat;\n\n        mCompressQuality = intent.getIntExtra(UCrop.Options.EXTRA_COMPRESSION_QUALITY, UCropActivity.DEFAULT_COMPRESS_QUALITY);\n\n        // Crop image view options\n        mGestureCropImageView.setMaxBitmapSize(intent.getIntExtra(UCrop.Options.EXTRA_MAX_BITMAP_SIZE, CropImageView.DEFAULT_MAX_BITMAP_SIZE));\n        mGestureCropImageView.setMaxScaleMultiplier(intent.getFloatExtra(UCrop.Options.EXTRA_MAX_SCALE_MULTIPLIER, CropImageView.DEFAULT_MAX_SCALE_MULTIPLIER));\n        mGestureCropImageView.setImageToWrapCropBoundsAnimDuration(intent.getIntExtra(UCrop.Options.EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, CropImageView.DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION));\n\n        // Overlay view options\n        mOverlayView.setFreestyleCropEnabled(intent.getBooleanExtra(UCrop.Options.EXTRA_FREE_STYLE_CROP, false));\n\n        mOverlayView.setDragFrame(true);\n\n        mOverlayView.setDimmedColor(intent.getIntExtra(UCrop.Options.EXTRA_DIMMED_LAYER_COLOR, getResources().getColor(R.color.ucrop_color_default_dimmed)));\n\n        mIsCircleCrop = intent.getBooleanExtra(UCrop.Options.EXTRA_CIRCLE_DIMMED_LAYER, OverlayView.DEFAULT_CIRCLE_DIMMED_LAYER);\n        mOverlayView.setCircleDimmedLayer(mIsCircleCrop);\n\n        mOverlayView.setShowCropFrame(intent.getBooleanExtra(UCrop.Options.EXTRA_SHOW_CROP_FRAME, OverlayView.DEFAULT_SHOW_CROP_FRAME));\n        mOverlayView.setCropFrameColor(intent.getIntExtra(UCrop.Options.EXTRA_CROP_FRAME_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_frame)));\n        mOverlayView.setCropFrameStrokeWidth(intent.getIntExtra(UCrop.Options.EXTRA_CROP_FRAME_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width)));\n\n        mOverlayView.setShowCropGrid(intent.getBooleanExtra(UCrop.Options.EXTRA_SHOW_CROP_GRID, OverlayView.DEFAULT_SHOW_CROP_GRID));\n        mOverlayView.setCropGridRowCount(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_ROW_COUNT, OverlayView.DEFAULT_CROP_GRID_ROW_COUNT));\n        mOverlayView.setCropGridColumnCount(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_COLUMN_COUNT, OverlayView.DEFAULT_CROP_GRID_COLUMN_COUNT));\n        mOverlayView.setCropGridColor(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_grid)));\n        mOverlayView.setCropGridStrokeWidth(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width)));\n\n        // Aspect ratio options\n        float aspectRatioX = intent.getFloatExtra(UCrop.EXTRA_ASPECT_RATIO_X, 0);\n        float aspectRatioY = intent.getFloatExtra(UCrop.EXTRA_ASPECT_RATIO_Y, 0);\n\n        int aspectRationSelectedByDefault = intent.getIntExtra(UCrop.Options.EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, 0);\n        ArrayList<AspectRatio> aspectRatioList = intent.getParcelableArrayListExtra(UCrop.Options.EXTRA_ASPECT_RATIO_OPTIONS);\n\n        if (aspectRatioX > 0 && aspectRatioY > 0) {\n            mGestureCropImageView.setTargetAspectRatio(aspectRatioX / aspectRatioY);\n        } else if (aspectRatioList != null && aspectRationSelectedByDefault < aspectRatioList.size()) {\n            mGestureCropImageView.setTargetAspectRatio(aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioX() /\n                    aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioY());\n        } else {\n            mGestureCropImageView.setTargetAspectRatio(CropImageView.SOURCE_IMAGE_ASPECT_RATIO);\n        }\n\n        // Result bitmap max size options\n        int maxSizeX = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_X, 0);\n        int maxSizeY = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_Y, 0);\n\n        if (maxSizeX > 0 && maxSizeY > 0) {\n            mGestureCropImageView.setMaxResultImageSizeX(maxSizeX);\n            mGestureCropImageView.setMaxResultImageSizeY(maxSizeY);\n        }\n    }\n\n    private void setupViews() {\n        initiateRootViews();\n    }\n\n    private void initiateRootViews() {\n        mUCropView = findViewById(R.id.ucrop);\n        mGestureCropImageView = mUCropView.getCropImageView();\n        mOverlayView = mUCropView.getOverlayView();\n\n        mGestureCropImageView.setTransformImageListener(mImageListener);\n    }\n\n    private TransformImageView.TransformImageListener mImageListener = new TransformImageView.TransformImageListener() {\n        @Override\n        public void onRotate(float currentAngle) {\n        }\n\n        @Override\n        public void onScale(float currentScale) {\n        }\n\n        @Override\n        public void onLoadComplete() {\n            mUCropView.animate().alpha(1).setDuration(300).setInterpolator(new AccelerateInterpolator());\n            mBlockingView.setClickable(false);\n            supportInvalidateOptionsMenu();\n        }\n\n        @Override\n        public void onLoadFailure(@NonNull Exception e) {\n            setResultError(e);\n            closeActivity();\n        }\n\n    };\n\n    /**\n     * Adds view that covers everything below the Toolbar.\n     * When it's clickable - user won't be able to click/touch anything below the Toolbar.\n     * Need to block user input while loading and cropping an image.\n     */\n    private void addBlockingView() {\n        if (mBlockingView == null) {\n            mBlockingView = new View(this);\n            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n            lp.addRule(RelativeLayout.BELOW, R.id.toolbar);\n            mBlockingView.setLayoutParams(lp);\n            mBlockingView.setClickable(true);\n        }\n\n        ((LinearLayout) findViewById(R.id.ucrop_photobox)).addView(mBlockingView);\n    }\n\n    protected void cropAndSaveImage() {\n        mBlockingView.setClickable(true);\n        supportInvalidateOptionsMenu();\n        mGestureCropImageView.cropAndSaveImage(mCompressFormat, mCompressQuality, mIsCircleCrop, new BitmapCropCallback() {\n\n            @Override\n            public void onBitmapCropped(@NonNull Uri resultUri, int offsetX, int offsetY, int imageWidth, int imageHeight) {\n                setResultUri(resultUri, mGestureCropImageView.getTargetAspectRatio(), offsetX, offsetY, imageWidth, imageHeight);\n            }\n\n            @Override\n            public void onCropFailure(@NonNull Throwable t) {\n                setResultError(t);\n                closeActivity();\n            }\n        });\n    }\n\n    protected void setResultUri(Uri uri, float resultAspectRatio, int offsetX, int offsetY, int imageWidth, int imageHeight) {\n        setResult(RESULT_OK, new Intent()\n                .putExtra(UCrop.EXTRA_OUTPUT_URI, uri)\n                .putExtra(UCrop.EXTRA_OUTPUT_CROP_ASPECT_RATIO, resultAspectRatio)\n                .putExtra(UCrop.EXTRA_OUTPUT_IMAGE_WIDTH, imageWidth)\n                .putExtra(UCrop.EXTRA_OUTPUT_IMAGE_HEIGHT, imageHeight)\n                .putExtra(UCrop.EXTRA_OUTPUT_OFFSET_X, offsetX)\n                .putExtra(UCrop.EXTRA_OUTPUT_OFFSET_Y, offsetY)\n        );\n        closeActivity();\n    }\n\n    protected void setResultError(Throwable throwable) {\n        setResult(UCrop.RESULT_ERROR, new Intent().putExtra(UCrop.EXTRA_ERROR, throwable));\n    }\n\n    /**\n     * exit activity\n     */\n    protected void closeActivity() {\n        finish();\n//        exitAnimation();\n    }\n\n    protected void exitAnimation() {\n        int exitAnimation = getIntent().getIntExtra(UCrop.EXTRA_WINDOW_EXIT_ANIMATION, 0);\n        overridePendingTransition(R.anim.ucrop_anim_fade_in, exitAnimation != 0 ? exitAnimation : R.anim.ucrop_close);\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/UCropMulti.java",
    "content": "package com.matisse.ucrop;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Parcelable;\n\nimport androidx.annotation.AnimRes;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.FloatRange;\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\n\nimport com.matisse.BuildConfig;\nimport com.matisse.R;\nimport com.matisse.ucrop.model.AspectRatio;\nimport com.matisse.ucrop.model.CutInfo;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Locale;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n * <p/>\n * Builder class to ease Intent setup.\n */\npublic class UCropMulti {\n\n    public static final int REQUEST_MULTI_CROP = 609;\n    public static final int RESULT_ERROR = 96;\n\n    private static final String EXTRA_PREFIX = BuildConfig.APPLICATION_ID;\n\n    public static final String EXTRA_INPUT_URI = EXTRA_PREFIX + \".InputUri\";\n    public static final String EXTRA_OUTPUT_URI = EXTRA_PREFIX + \".OutputUri\";\n    public static final String EXTRA_OUTPUT_URI_LIST = EXTRA_PREFIX + \".OutputUriList\";\n    public static final String EXTRA_OUTPUT_CROP_ASPECT_RATIO = EXTRA_PREFIX + \".CropAspectRatio\";\n    public static final String EXTRA_OUTPUT_IMAGE_WIDTH = EXTRA_PREFIX + \".ImageWidth\";\n    public static final String EXTRA_OUTPUT_IMAGE_HEIGHT = EXTRA_PREFIX + \".ImageHeight\";\n    public static final String EXTRA_OUTPUT_OFFSET_X = EXTRA_PREFIX + \".OffsetX\";\n    public static final String EXTRA_OUTPUT_OFFSET_Y = EXTRA_PREFIX + \".OffsetY\";\n    public static final String EXTRA_ERROR = EXTRA_PREFIX + \".Error\";\n\n    public static final String EXTRA_WINDOW_EXIT_ANIMATION = EXTRA_PREFIX + \".WindowAnimation\";\n\n    public static final String EXTRA_NAV_BAR_COLOR = EXTRA_PREFIX + \".navBarColor\";\n\n    public static final String EXTRA_ASPECT_RATIO_X = EXTRA_PREFIX + \".AspectRatioX\";\n    public static final String EXTRA_ASPECT_RATIO_Y = EXTRA_PREFIX + \".AspectRatioY\";\n\n    public static final String EXTRA_MAX_SIZE_X = EXTRA_PREFIX + \".MaxSizeX\";\n    public static final String EXTRA_MAX_SIZE_Y = EXTRA_PREFIX + \".MaxSizeY\";\n\n    private Intent mCropIntent;\n    private Bundle mCropOptionsBundle;\n\n    /**\n     * This method creates new Intent builder and sets both source and destination image URIs.\n     *\n     * @param source      Uri for image to crop\n     * @param destination Uri for saving the cropped image\n     */\n    public static UCropMulti of(@NonNull Uri source, @NonNull Uri destination) {\n        return new UCropMulti(source, destination);\n    }\n\n    private UCropMulti(@NonNull Uri source, @NonNull Uri destination) {\n        mCropIntent = new Intent();\n        mCropOptionsBundle = new Bundle();\n        mCropOptionsBundle.putParcelable(EXTRA_INPUT_URI, source);\n        mCropOptionsBundle.putParcelable(EXTRA_OUTPUT_URI, destination);\n    }\n\n    /**\n     * Set an aspect ratio for crop bounds.\n     * User won't see the menu with other ratios options.\n     *\n     * @param x aspect ratio X\n     * @param y aspect ratio Y\n     */\n    public UCropMulti withAspectRatio(float x, float y) {\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);\n        return this;\n    }\n\n    /**\n     * Set an aspect ratio for crop bounds that is evaluated from source image width and height.\n     * User won't see the menu with other ratios options.\n     */\n    public UCropMulti useSourceImageAspectRatio() {\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);\n        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);\n        return this;\n    }\n\n    /**\n     * Set maximum size for result cropped image.\n     *\n     * @param width  max cropped image width\n     * @param height max cropped image height\n     */\n    public UCropMulti withMaxResultSize(@IntRange(from = 100) int width, @IntRange(from = 100) int height) {\n        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_X, width);\n        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_Y, height);\n        return this;\n    }\n\n    public UCropMulti withOptions(@NonNull Options options) {\n        mCropOptionsBundle.putAll(options.getOptionBundle());\n        return this;\n    }\n\n    /**\n     * Send the crop Intent from animation an Activity\n     *\n     * @param activity Activity to receive result\n     */\n    public void startAnimation(@NonNull Activity activity, @AnimRes int activityCropEnterAnimation) {\n        if (activityCropEnterAnimation != 0) {\n            start(activity, REQUEST_MULTI_CROP, activityCropEnterAnimation);\n        } else {\n            start(activity, REQUEST_MULTI_CROP);\n        }\n    }\n\n    /**\n     * Send the crop Intent from an Activity with a custom request code or animation\n     *\n     * @param activity    Activity to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Activity activity, int requestCode, @AnimRes int activityCropEnterAnimation) {\n        activity.startActivityForResult(getIntent(activity), requestCode);\n        activity.overridePendingTransition(activityCropEnterAnimation, R.anim.ucrop_anim_fade_in);\n    }\n\n    /**\n     * Send the crop Intent from an Activity\n     *\n     * @param activity Activity to receive result\n     */\n    public void start(@NonNull Activity activity) {\n        start(activity, REQUEST_MULTI_CROP);\n    }\n\n    /**\n     * Send the crop Intent from an Activity with a custom request code\n     *\n     * @param activity    Activity to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Activity activity, int requestCode) {\n        activity.startActivityForResult(getIntent(activity), requestCode);\n    }\n\n    /**\n     * Send the crop Intent from a Fragment\n     *\n     * @param fragment Fragment to receive result\n     */\n    public void start(@NonNull Context context, @NonNull Fragment fragment) {\n        start(context, fragment, REQUEST_MULTI_CROP);\n    }\n\n    /**\n     * Send the crop Intent with a custom request code\n     *\n     * @param fragment    Fragment to receive result\n     * @param requestCode requestCode for result\n     */\n    public void start(@NonNull Context context, @NonNull Fragment fragment, int requestCode) {\n        fragment.startActivityForResult(getIntent(context), requestCode);\n    }\n\n    /**\n     * Get Intent to start {@link PictureMultiCuttingActivity}\n     *\n     * @return Intent for {@link PictureMultiCuttingActivity}\n     */\n    public Intent getIntent(@NonNull Context context) {\n        mCropIntent.setClass(context, PictureMultiCuttingActivity.class);\n        mCropIntent.putExtras(mCropOptionsBundle);\n        return mCropIntent;\n    }\n\n\n    /**\n     * Retrieve cropped image Cuts from the result Intent\n     *\n     * @param intent crop result intent\n     */\n    @Nullable\n    public static List<CutInfo> getOutput(@NonNull Intent intent) {\n        return (List<CutInfo>) intent.getSerializableExtra(EXTRA_OUTPUT_URI_LIST);\n    }\n\n    /**\n     * Retrieve the width of the cropped image\n     *\n     * @param intent crop result intent\n     */\n    public static int getOutputImageWidth(@NonNull Intent intent) {\n        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_WIDTH, -1);\n    }\n\n    /**\n     * Retrieve the height of the cropped image\n     *\n     * @param intent crop result intent\n     */\n    public static int getOutputImageHeight(@NonNull Intent intent) {\n        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_HEIGHT, -1);\n    }\n\n    /**\n     * Retrieve cropped image aspect ratio from the result Intent\n     *\n     * @param intent crop result intent\n     * @return aspect ratio as a floating point value (x:y) - so it will be 1 for 1:1 or 4/3 for 4:3\n     */\n    public static float getOutputCropAspectRatio(@NonNull Intent intent) {\n        return intent.getFloatExtra(EXTRA_OUTPUT_CROP_ASPECT_RATIO, 1);\n    }\n\n    /**\n     * Method retrieves error from the result intent.\n     *\n     * @param result crop result Intent\n     * @return Throwable that could happen while image processing\n     */\n    @Nullable\n    public static Throwable getError(@NonNull Intent result) {\n        return (Throwable) result.getSerializableExtra(EXTRA_ERROR);\n    }\n\n\n    /**\n     * Class that helps to setup advanced configs that are not commonly used.\n     * Use it with method {@link #withOptions(Options)}\n     */\n    public static class Options {\n\n        public static final String EXTRA_COMPRESSION_FORMAT_NAME = EXTRA_PREFIX + \".CompressionFormatName\";\n        public static final String EXTRA_COMPRESSION_QUALITY = EXTRA_PREFIX + \".CompressionQuality\";\n\n        public static final String EXTRA_ALLOWED_GESTURES = EXTRA_PREFIX + \".AllowedGestures\";\n\n        public static final String EXTRA_MAX_BITMAP_SIZE = EXTRA_PREFIX + \".MaxBitmapSize\";\n        public static final String EXTRA_MAX_SCALE_MULTIPLIER = EXTRA_PREFIX + \".MaxScaleMultiplier\";\n        public static final String EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = EXTRA_PREFIX + \".ImageToCropBoundsAnimDuration\";\n\n        public static final String EXTRA_DIMMED_LAYER_COLOR = EXTRA_PREFIX + \".DimmedLayerColor\";\n        public static final String EXTRA_CIRCLE_DIMMED_LAYER = EXTRA_PREFIX + \".CircleDimmedLayer\";\n\n        public static final String EXTRA_SHOW_CROP_FRAME = EXTRA_PREFIX + \".ShowCropFrame\";\n        public static final String EXTRA_CROP_FRAME_COLOR = EXTRA_PREFIX + \".CropFrameColor\";\n        public static final String EXTRA_CROP_FRAME_STROKE_WIDTH = EXTRA_PREFIX + \".CropFrameStrokeWidth\";\n\n        public static final String EXTRA_SHOW_CROP_GRID = EXTRA_PREFIX + \".ShowCropGrid\";\n        public static final String EXTRA_CROP_GRID_ROW_COUNT = EXTRA_PREFIX + \".CropGridRowCount\";\n        public static final String EXTRA_CROP_GRID_COLUMN_COUNT = EXTRA_PREFIX + \".CropGridColumnCount\";\n        public static final String EXTRA_CROP_GRID_COLOR = EXTRA_PREFIX + \".CropGridColor\";\n        public static final String EXTRA_CROP_GRID_STROKE_WIDTH = EXTRA_PREFIX + \".CropGridStrokeWidth\";\n\n        public static final String EXTRA_TOOL_BAR_COLOR = EXTRA_PREFIX + \".ToolbarColor\";\n        public static final String EXTRA_STATUS_BAR_COLOR = EXTRA_PREFIX + \".StatusBarColor\";\n        public static final String EXTRA_UCROP_COLOR_WIDGET_ACTIVE = EXTRA_PREFIX + \".UcropColorWidgetActive\";\n\n        public static final String EXTRA_UCROP_WIDGET_CROP_OPEN_WHITE_STATUSBAR = EXTRA_PREFIX + \".openWhiteStatusBar\";\n\n        public static final String EXTRA_UCROP_WIDGET_COLOR_TOOLBAR = EXTRA_PREFIX + \".UcropToolbarWidgetColor\";\n        public static final String EXTRA_UCROP_TITLE_TEXT_TOOLBAR = EXTRA_PREFIX + \".UcropToolbarTitleText\";\n        public static final String EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE = EXTRA_PREFIX + \".UcropToolbarCancelDrawable\";\n        public static final String EXTRA_UCROP_WIDGET_CROP_DRAWABLE = EXTRA_PREFIX + \".UcropToolbarCropDrawable\";\n\n        public static final String EXTRA_UCROP_LOGO_COLOR = EXTRA_PREFIX + \".UcropLogoColor\";\n\n        public static final String EXTRA_FREE_STYLE_CROP = EXTRA_PREFIX + \".FreeStyleCrop\";\n\n        public static final String EXTRA_CUT_CROP = EXTRA_PREFIX + \".cuts\";\n\n        public static final String EXTRA_FREE_STATUS_FONT = EXTRA_PREFIX + \".StatusFont\";\n\n        public static final String EXTRA_DRAG_CROP_FRAME = EXTRA_PREFIX + \".DragCropFrame\";\n\n        public static final String EXTRA_ROTATE = EXTRA_PREFIX + \".rotate\";\n        public static final String EXTRA_SCALE = EXTRA_PREFIX + \".scale\";\n\n        public static final String EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT = EXTRA_PREFIX + \".AspectRatioSelectedByDefault\";\n        public static final String EXTRA_ASPECT_RATIO_OPTIONS = EXTRA_PREFIX + \".AspectRatioOptions\";\n\n        public static final String EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR = EXTRA_PREFIX + \".UcropRootViewBackgroundColor\";\n\n\n        private final Bundle mOptionBundle;\n\n        public Options() {\n            mOptionBundle = new Bundle();\n        }\n\n        @NonNull\n        public Bundle getOptionBundle() {\n            return mOptionBundle;\n        }\n\n        /**\n         * Set one of {@link Bitmap.CompressFormat} that will be used to save resulting Bitmap.\n         */\n        public Options setCompressionFormat(@NonNull Bitmap.CompressFormat format) {\n            mOptionBundle.putString(EXTRA_COMPRESSION_FORMAT_NAME, format.name());\n            return this;\n        }\n\n        /**\n         * Set compression quality [0-100] that will be used to save resulting Bitmap.\n         */\n        public Options setCompressionQuality(@IntRange(from = 0) int compressQuality) {\n            mOptionBundle.putInt(EXTRA_COMPRESSION_QUALITY, compressQuality);\n            return this;\n        }\n\n        /**\n         * Choose what set of gestures will be enabled on each tab - if any.\n         */\n        public Options setAllowedGestures(@PictureMultiCuttingActivity.GestureTypes int tabScale,\n                                       @PictureMultiCuttingActivity.GestureTypes int tabRotate,\n                                       @PictureMultiCuttingActivity.GestureTypes int tabAspectRatio) {\n            mOptionBundle.putIntArray(EXTRA_ALLOWED_GESTURES, new int[]{tabScale, tabRotate, tabAspectRatio});\n            return this;\n        }\n\n        /**\n         * This method sets multiplier that is used to calculate max image scale from min image scale.\n         *\n         * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale\n         */\n        public Options setMaxScaleMultiplier(@FloatRange(from = 1.0, fromInclusive = false) float maxScaleMultiplier) {\n            mOptionBundle.putFloat(EXTRA_MAX_SCALE_MULTIPLIER, maxScaleMultiplier);\n            return this;\n        }\n\n        /**\n         * This method sets animation duration for image to wrap the crop bounds\n         *\n         * @param durationMillis - duration in milliseconds\n         */\n        public Options setImageToCropBoundsAnimDuration(@IntRange(from = 100) int durationMillis) {\n            mOptionBundle.putInt(EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, durationMillis);\n            return this;\n        }\n\n        /**\n         * Setter for max size for both width and height of bitmap that will be decoded from an input Uri and used in the view.\n         *\n         * @param maxBitmapSize - size in pixels\n         */\n        public Options setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize) {\n            mOptionBundle.putInt(EXTRA_MAX_BITMAP_SIZE, maxBitmapSize);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of dimmed area around the crop bounds\n         */\n        public Options setDimmedLayerColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_DIMMED_LAYER_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param isCircle - set it to true if you want dimmed layer to have an circle inside\n         */\n        public Options setCircleDimmedLayer(boolean isCircle) {\n            mOptionBundle.putBoolean(EXTRA_CIRCLE_DIMMED_LAYER, isCircle);\n            return this;\n        }\n\n        /**\n         * @param show - set to true if you want to see a crop frame rectangle on top of an image\n         */\n        public Options setShowCropFrame(boolean show) {\n            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_FRAME, show);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of crop frame\n         */\n        public Options setCropFrameColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_CROP_FRAME_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param width - desired width of crop frame line in pixels\n         */\n        public Options setCropFrameStrokeWidth(@IntRange(from = 0) int width) {\n            mOptionBundle.putInt(EXTRA_CROP_FRAME_STROKE_WIDTH, width);\n            return this;\n        }\n\n        /**\n         * @param show - set to true if you want to see a crop grid/guidelines on top of an image\n         */\n        public Options setShowCropGrid(boolean show) {\n            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_GRID, show);\n            return this;\n        }\n\n        public Options setScaleEnabled(boolean scaleEnabled) {\n            mOptionBundle.putBoolean(EXTRA_SCALE, scaleEnabled);\n            return this;\n        }\n\n        public Options setRotateEnabled(boolean rotateEnabled) {\n            mOptionBundle.putBoolean(EXTRA_ROTATE, rotateEnabled);\n            return this;\n        }\n\n        /**\n         * @param isDragFrame - 是否可拖动裁剪框\n         */\n        public Options setDragFrameEnabled(boolean isDragFrame) {\n            mOptionBundle.putBoolean(EXTRA_DRAG_CROP_FRAME, isDragFrame);\n            return this;\n        }\n\n        /**\n         * @param count - crop grid rows count.\n         */\n        public Options setCropGridRowCount(@IntRange(from = 0) int count) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_ROW_COUNT, count);\n            return this;\n        }\n\n        /**\n         * @param count - crop grid columns count.\n         */\n        public Options setCropGridColumnCount(@IntRange(from = 0) int count) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_COLUMN_COUNT, count);\n            return this;\n        }\n\n        /**\n         * @param color - desired color of crop grid/guidelines\n         */\n        public Options setCropGridColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param width - desired width of crop grid lines in pixels\n         */\n        public Options setCropGridStrokeWidth(@IntRange(from = 0) int width) {\n            mOptionBundle.putInt(EXTRA_CROP_GRID_STROKE_WIDTH, width);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the toolbar\n         */\n        public Options setToolbarColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_TOOL_BAR_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the statusbar\n         */\n        public Options setStatusBarColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_STATUS_BAR_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of the active and selected widget (default is orange) and progress wheel middle line\n         */\n        public Options setActiveWidgetColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_COLOR_WIDGET_ACTIVE, color);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of Toolbar text and buttons (default is darker orange)\n         */\n        public Options setToolbarWidgetColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, color);\n            return this;\n        }\n\n        /**\n         * @param openWhiteStatusBar - Change the status bar font color\n         */\n        public Options isOpenWhiteStatusBar(boolean openWhiteStatusBar) {\n            mOptionBundle.putBoolean(EXTRA_UCROP_WIDGET_CROP_OPEN_WHITE_STATUSBAR, openWhiteStatusBar);\n            return this;\n        }\n\n        /**\n         * @param text - desired text for Toolbar title\n         */\n        public Options setToolbarTitle(@Nullable String text) {\n            mOptionBundle.putString(EXTRA_UCROP_TITLE_TEXT_TOOLBAR, text);\n            return this;\n        }\n\n        /**\n         * @param drawable - desired drawable for the Toolbar left cancel icon\n         */\n        public Options setToolbarCancelDrawable(@DrawableRes int drawable) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, drawable);\n            return this;\n        }\n\n        /**\n         * @param drawable - desired drawable for the Toolbar right crop icon\n         */\n        public Options setToolbarCropDrawable(@DrawableRes int drawable) {\n            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CROP_DRAWABLE, drawable);\n            return this;\n        }\n\n        /**\n         * @param color - desired resolved color of logo fill (default is darker grey)\n         */\n        public Options setLogoColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_LOGO_COLOR, color);\n            return this;\n        }\n\n        /**\n         * @param -set cuts path\n         */\n        public Options setCutListData(ArrayList<CutInfo> list) {\n            mOptionBundle.putSerializable(EXTRA_CUT_CROP, list);\n            return this;\n        }\n\n        /**\n         * @param enabled - set to true to let user resize crop bounds (disabled by default)\n         */\n        public Options setFreeStyleCropEnabled(boolean enabled) {\n            mOptionBundle.putBoolean(EXTRA_FREE_STYLE_CROP, enabled);\n            return this;\n        }\n\n        /**\n         * @param statusFont - Set status bar black\n         */\n        @Deprecated\n        public Options setStatusFont(boolean statusFont) {\n            mOptionBundle.putBoolean(EXTRA_FREE_STATUS_FONT, statusFont);\n            return this;\n        }\n\n        /**\n         * @param activityCropExitAnimation activity exit animation\n         */\n        public Options setCropExitAnimation(@AnimRes int activityCropExitAnimation) {\n            mOptionBundle.putInt(EXTRA_WINDOW_EXIT_ANIMATION, activityCropExitAnimation);\n            return this;\n        }\n\n        /**\n         * @param navBarColor set NavBar Color\n         */\n        public Options setNavBarColor(@ColorInt int navBarColor) {\n            mOptionBundle.putInt(EXTRA_NAV_BAR_COLOR, navBarColor);\n            return this;\n        }\n\n        /**\n         * Pass an ordered list of desired aspect ratios that should be available for a user.\n         *\n         * @param selectedByDefault - index of aspect ratio option that is selected by default (starts with 0).\n         * @param aspectRatio       - list of aspect ratio options that are available to user\n         */\n        public Options setAspectRatioOptions(int selectedByDefault, AspectRatio... aspectRatio) {\n            if (selectedByDefault > aspectRatio.length) {\n                throw new IllegalArgumentException(String.format(Locale.US,\n                        \"Index [selectedByDefault = %d] cannot be higher than aspect ratio options count [count = %d].\",\n                        selectedByDefault, aspectRatio.length));\n            }\n            mOptionBundle.putInt(EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, selectedByDefault);\n            mOptionBundle.putParcelableArrayList(EXTRA_ASPECT_RATIO_OPTIONS, new ArrayList<Parcelable>(Arrays.asList(aspectRatio)));\n            return this;\n        }\n\n        /**\n         * @param color - desired background color that should be applied to the root view\n         */\n        public Options setRootViewBackgroundColor(@ColorInt int color) {\n            mOptionBundle.putInt(EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, color);\n            return this;\n        }\n\n        /**\n         * Set an aspect ratio for crop bounds.\n         * User won't see the menu with other ratios options.\n         *\n         * @param x aspect ratio X\n         * @param y aspect ratio Y\n         */\n        public Options withAspectRatio(float x, float y) {\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);\n            return this;\n        }\n\n        /**\n         * Set an aspect ratio for crop bounds that is evaluated from source image width and height.\n         * User won't see the menu with other ratios options.\n         */\n        public Options useSourceImageAspectRatio() {\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);\n            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);\n            return this;\n        }\n\n        /**\n         * Set maximum size for result cropped image.\n         *\n         * @param width  max cropped image width\n         * @param height max cropped image height\n         */\n        public Options withMaxResultSize(int width, int height) {\n            mOptionBundle.putInt(EXTRA_MAX_SIZE_X, width);\n            mOptionBundle.putInt(EXTRA_MAX_SIZE_Y, height);\n            return this;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/callback/Callback.kt",
    "content": "@file:JvmName(\"Callback\")\npackage com.matisse.ucrop.callback\n\nimport android.graphics.Bitmap\nimport android.graphics.RectF\nimport android.net.Uri\nimport com.matisse.ucrop.model.ExifInfo\n\ninterface BitmapCropCallback {\n\n    fun onBitmapCropped(\n        resultUri: Uri,\n        offsetX: Int,\n        offsetY: Int,\n        imageWidth: Int,\n        imageHeight: Int\n    )\n\n    fun onCropFailure(t: Throwable)\n\n}\n\ninterface BitmapLoadCallback {\n\n    fun onBitmapLoaded(bitmap: Bitmap, exifInfo: ExifInfo, imageInputUri: Uri, imageOutputUri: Uri?)\n\n    fun onFailure(bitmapWorkerException: Exception)\n\n}\n\n\ninterface BitmapLoadShowCallback {\n\n    fun onBitmapLoaded(bitmap: Bitmap)\n\n    fun onFailure(bitmapWorkerException: Exception)\n\n}\n\ninterface CropBoundsChangeListener {\n\n    fun onCropAspectRatioChanged(cropRatio: Float)\n\n}\n\ninterface OverlayViewChangeListener {\n\n    fun onCropRectUpdated(cropRect: RectF)\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/immersion/CropImmersiveManage.java",
    "content": "package com.matisse.ucrop.immersion;\n\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\n/**\n * @author：luck\n * @data：2018/3/28 下午1:00\n * @描述: 沉浸式相关\n */\n\npublic class CropImmersiveManage {\n    /**\n     * 判定是否使用沉浸式\n     */\n    public static boolean immersiveUseful() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            return true;\n        }\n\n        return false;\n    }\n\n\n    /**\n     * 注意：使用最好将布局xml 跟布局加入    android:fitsSystemWindows=\"true\" ，这样可以避免有些手机上布局顶边的问题\n     *\n     * @param baseActivity        这个会留出来状态栏和底栏的空白\n     * @param statusBarColor      状态栏的颜色\n     * @param navigationBarColor  导航栏的颜色\n     * @param isDarkStatusBarIcon 状态栏图标颜色是否是深（黑）色  false状态栏图标颜色为白色\n     */\n    public static void immersiveAboveAPI23(AppCompatActivity baseActivity, int statusBarColor, int navigationBarColor, boolean isDarkStatusBarIcon) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            immersiveAboveAPI23(baseActivity, false, false, statusBarColor, navigationBarColor, isDarkStatusBarIcon);\n        }\n    }\n\n\n    /**\n     * @param baseActivity\n     * @param statusBarColor     状态栏的颜色\n     * @param navigationBarColor 导航栏的颜色\n     */\n    public static void immersiveAboveAPI23(AppCompatActivity baseActivity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, int statusBarColor, int navigationBarColor, boolean isDarkStatusBarIcon) {\n        try {\n            Window window = baseActivity.getWindow();\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n                //4.4版本及以上 5.0版本及以下\n                window.setFlags(\n                        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,\n                        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                if (isMarginStatusBar && isMarginNavigationBar) {\n                    //5.0版本及以上\n                    window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS\n                            | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);\n                    CropLightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar\n                            , isMarginNavigationBar\n                            , statusBarColor == Color.TRANSPARENT\n                            , isDarkStatusBarIcon);\n\n                    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n                } else if (!isMarginStatusBar && !isMarginNavigationBar) {\n                    window.requestFeature(Window.FEATURE_NO_TITLE);\n                    window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS\n                            | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);\n\n                    CropLightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar\n                            , isMarginNavigationBar\n                            , statusBarColor == Color.TRANSPARENT\n                            , isDarkStatusBarIcon);\n\n                    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n\n\n                } else if (!isMarginStatusBar && isMarginNavigationBar) {\n                    window.requestFeature(Window.FEATURE_NO_TITLE);\n                    window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS\n                            | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);\n                    CropLightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar\n                            , isMarginNavigationBar\n                            , statusBarColor == Color.TRANSPARENT\n                            , isDarkStatusBarIcon);\n\n                    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n\n\n                } else {\n                    //留出来状态栏 不留出来导航栏 没找到办法。。\n                    return;\n                }\n\n                window.setStatusBarColor(statusBarColor);\n                window.setNavigationBarColor(navigationBarColor);\n\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/immersion/CropLightStatusBarUtils.java",
    "content": "package com.matisse.ucrop.immersion;\n\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.os.Build;\nimport android.view.View;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\n\n/**\n * @author：luck\n * @data：2018/3/28 下午1:01\n * @描述: 沉浸式\n */\n\npublic class CropLightStatusBarUtils {\n    public static void setLightStatusBarAboveAPI23(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            setLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n        }\n    }\n\n    public static void setLightStatusBar(Activity activity, boolean dark) {\n        setLightStatusBar(activity, false, false, false, dark);\n    }\n\n    public static void setLightStatusBar(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) {\n        switch (CropRomUtils.getLightStatausBarAvailableRomType()) {\n            case CropRomUtils.AvailableRomType.MIUI:\n                if (CropRomUtils.getMIUIVersionCode() >= 7) {\n                    setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n                } else {\n                    setMIUILightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n                }\n                break;\n\n            case CropRomUtils.AvailableRomType.FLYME:\n                setFlymeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n                break;\n\n            case CropRomUtils.AvailableRomType.ANDROID_NATIVE:\n                setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n                break;\n\n            case CropRomUtils.AvailableRomType.NA:\n                // N/A do nothing\n                break;\n        }\n    }\n\n\n    private static boolean setMIUILightStatusBar(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean darkmode) {\n        initStatusBarStyle(activity, isMarginStatusBar, isMarginNavigationBar);\n\n        Class<? extends Window> clazz = activity.getWindow().getClass();\n        try {\n            int darkModeFlag = 0;\n            Class<?> layoutParams = Class.forName(\"android.view.MiuiWindowManager$LayoutParams\");\n            Field field = layoutParams.getField(\"EXTRA_FLAG_STATUS_BAR_DARK_MODE\");\n            darkModeFlag = field.getInt(layoutParams);\n            Method extraFlagField = clazz.getMethod(\"setExtraFlags\", int.class, int.class);\n            extraFlagField.invoke(activity.getWindow(), darkmode ? darkModeFlag : 0, darkModeFlag);\n            return true;\n        } catch (Exception e) {\n            setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, darkmode);\n        }\n        return false;\n    }\n\n    private static boolean setFlymeLightStatusBar(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) {\n        boolean result = false;\n        if (activity != null) {\n            initStatusBarStyle(activity, isMarginStatusBar, isMarginNavigationBar);\n            try {\n                WindowManager.LayoutParams lp = activity.getWindow().getAttributes();\n                Field darkFlag = WindowManager.LayoutParams.class\n                        .getDeclaredField(\"MEIZU_FLAG_DARK_STATUS_BAR_ICON\");\n                Field meizuFlags = WindowManager.LayoutParams.class\n                        .getDeclaredField(\"meizuFlags\");\n                darkFlag.setAccessible(true);\n                meizuFlags.setAccessible(true);\n                int bit = darkFlag.getInt(null);\n                int value = meizuFlags.getInt(lp);\n                if (dark) {\n                    value |= bit;\n                } else {\n                    value &= ~bit;\n                }\n                meizuFlags.setInt(lp, value);\n                activity.getWindow().setAttributes(lp);\n                result = true;\n\n                if (CropRomUtils.getFlymeVersion() >= 7) {\n                    setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n                }\n            } catch (Exception e) {\n                setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark);\n            }\n        }\n        return result;\n    }\n\n    @TargetApi(11)\n    private static void setAndroidNativeLightStatusBar(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean isDarkStatusBarIcon) {\n\n        try {\n            if (isTransStatusBar) {\n                Window window = activity.getWindow();\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                    if (isMarginStatusBar && isMarginNavigationBar) {\n                        //5.0版本及以上\n                        if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                                    | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                        } else {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n                        }\n                    } else if (!isMarginStatusBar && !isMarginNavigationBar) {\n\n                        if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n//                                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n                                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                                    | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                        } else {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n//                                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n                                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n                        }\n\n\n                    } else if (!isMarginStatusBar && isMarginNavigationBar) {\n                        if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                                    | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                        } else {\n                            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n                        }\n\n\n                    } else {\n                        //留出来状态栏 不留出来导航栏 没找到办法。。\n                        return;\n                    }\n                }\n            } else {\n                View decor = activity.getWindow().getDecorView();\n                if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                    decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                } else {\n                    // We want to change tint color to white again.\n                    // You can also record the flags in advance so that you can turn UI back completely if\n                    // you have set other flags before, such as translucent or full screen.\n                    decor.setSystemUiVisibility(0);\n                }\n            }\n        } catch (Exception e) {\n        }\n    }\n\n    private static void initStatusBarStyle(Activity activity, boolean isMarginStatusBar\n            , boolean isMarginNavigationBar) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {\n            if (isMarginStatusBar && isMarginNavigationBar) {\n                activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n            } else if (!isMarginStatusBar && !isMarginNavigationBar) {\n                activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n            } else if (!isMarginStatusBar && isMarginNavigationBar) {\n\n                activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n            } else {\n                //留出来状态栏 不留出来导航栏 没找到办法。。\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/immersion/CropRomUtils.java",
    "content": "package com.matisse.ucrop.immersion;\n\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.regex.Pattern;\n\n/**\n * @author：luck\n * @data：2018/3/28 下午1:02\n * @描述: Rom版本管理\n */\n\npublic class CropRomUtils {\n    public class AvailableRomType {\n        public static final int MIUI = 1;\n        public static final int FLYME = 2;\n        public static final int ANDROID_NATIVE = 3;\n        public static final int NA = 4;\n    }\n\n\n    private static Integer romType;\n\n    public static int getLightStatausBarAvailableRomType() {\n        if (romType != null) {\n            return romType;\n        }\n\n        if (isMIUIV6OrAbove()) {\n            romType = AvailableRomType.MIUI;\n            return romType;\n        }\n\n        if (isFlymeV4OrAbove()) {\n            romType = AvailableRomType.FLYME;\n            return romType;\n        }\n\n        if (isAndroid5OrAbove()) {\n            romType = AvailableRomType.ANDROID_NATIVE;\n            return romType;\n        }\n\n        romType = AvailableRomType.NA;\n        return romType;\n    }\n\n    //Flyme V4的displayId格式为 [Flyme OS 4.x.x.xA]\n    //Flyme V5的displayId格式为 [Flyme 5.x.x.x beta]\n    private static boolean isFlymeV4OrAbove() {\n        return (getFlymeVersion() >= 4);\n    }\n\n\n    //Flyme V4的displayId格式为 [Flyme OS 4.x.x.xA]\n    //Flyme V5的displayId格式为 [Flyme 5.x.x.x beta]\n    public static int getFlymeVersion() {\n        String displayId = Build.DISPLAY;\n        if (!TextUtils.isEmpty(displayId) && displayId.contains(\"Flyme\")) {\n            displayId = displayId.replaceAll(\"Flyme\", \"\");\n            displayId = displayId.replaceAll(\"OS\", \"\");\n            displayId = displayId.replaceAll(\" \", \"\");\n\n\n            String version = displayId.substring(0, 1);\n\n            if (version != null) {\n                return stringToInt(version);\n            }\n        }\n        return 0;\n    }\n\n    //MIUI V6对应的versionCode是4\n    //MIUI V7对应的versionCode是5\n    private static boolean isMIUIV6OrAbove() {\n        String miuiVersionCodeStr = getSystemProperty(\"ro.miui.ui.version.code\");\n        if (!TextUtils.isEmpty(miuiVersionCodeStr)) {\n            try {\n                int miuiVersionCode = Integer.parseInt(miuiVersionCodeStr);\n                if (miuiVersionCode >= 4) {\n                    return true;\n                }\n            } catch (Exception e) {\n            }\n        }\n        return false;\n    }\n\n\n    public static int getMIUIVersionCode() {\n        String miuiVersionCodeStr = getSystemProperty(\"ro.miui.ui.version.code\");\n        int miuiVersionCode = 0;\n        if (!TextUtils.isEmpty(miuiVersionCodeStr)) {\n            try {\n                miuiVersionCode = Integer.parseInt(miuiVersionCodeStr);\n                return miuiVersionCode;\n            } catch (Exception e) {\n            }\n        }\n        return miuiVersionCode;\n    }\n\n\n    //Android Api 23以上\n    private static boolean isAndroid5OrAbove() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            return true;\n        }\n        return false;\n    }\n\n\n    public static String getSystemProperty(String propName) {\n        String line;\n        BufferedReader input = null;\n        try {\n            Process p = Runtime.getRuntime().exec(\"getprop \" + propName);\n            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);\n            line = input.readLine();\n            input.close();\n        } catch (IOException ex) {\n            return null;\n        } finally {\n            if (input != null) {\n                try {\n                    input.close();\n                } catch (IOException e) {\n                }\n            }\n        }\n        return line;\n    }\n\n    public static int stringToInt(String str) {\n        Pattern pattern = Pattern.compile(\"^[-\\\\+]?[\\\\d]+$\");\n        if (pattern.matcher(str).matches()) {\n            return Integer.valueOf(str);\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/model/AspectRatio.java",
    "content": "package com.matisse.ucrop.model;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 6/24/16.\n */\npublic class AspectRatio implements Parcelable {\n\n    @Nullable\n    private final String mAspectRatioTitle;\n    private final float mAspectRatioX;\n    private final float mAspectRatioY;\n\n    protected AspectRatio(Parcel in) {\n        mAspectRatioTitle = in.readString();\n        mAspectRatioX = in.readFloat();\n        mAspectRatioY = in.readFloat();\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeString(mAspectRatioTitle);\n        dest.writeFloat(mAspectRatioX);\n        dest.writeFloat(mAspectRatioY);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public static final Creator<AspectRatio> CREATOR = new Creator<AspectRatio>() {\n        @Override\n        public AspectRatio createFromParcel(Parcel in) {\n            return new AspectRatio(in);\n        }\n\n        @Override\n        public AspectRatio[] newArray(int size) {\n            return new AspectRatio[size];\n        }\n    };\n\n    @Nullable\n    public String getAspectRatioTitle() {\n        return mAspectRatioTitle;\n    }\n\n    public float getAspectRatioX() {\n        return mAspectRatioX;\n    }\n\n    public float getAspectRatioY() {\n        return mAspectRatioY;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/model/CropParameters.java",
    "content": "package com.matisse.ucrop.model;\n\nimport android.graphics.Bitmap;\nimport android.net.Uri;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.\n */\npublic class CropParameters {\n\n    private int mMaxResultImageSizeX, mMaxResultImageSizeY;\n\n    private Bitmap.CompressFormat mCompressFormat;\n    private int mCompressQuality;\n    private Uri mImageInputUri;\n    private String mImageOutputPath;\n    private ExifInfo mExifInfo;\n\n\n    public CropParameters(int maxResultImageSizeX, int maxResultImageSizeY,\n                          Bitmap.CompressFormat compressFormat, int compressQuality,\n                          Uri imageInputUri, String imageOutputPath, ExifInfo exifInfo) {\n        mMaxResultImageSizeX = maxResultImageSizeX;\n        mMaxResultImageSizeY = maxResultImageSizeY;\n        mCompressFormat = compressFormat;\n        mCompressQuality = compressQuality;\n        mImageInputUri = imageInputUri;\n        mImageOutputPath = imageOutputPath;\n        mExifInfo = exifInfo;\n    }\n\n    public int getMaxResultImageSizeX() {\n        return mMaxResultImageSizeX;\n    }\n\n    public int getMaxResultImageSizeY() {\n        return mMaxResultImageSizeY;\n    }\n\n    public Bitmap.CompressFormat getCompressFormat() {\n        return mCompressFormat;\n    }\n\n    public int getCompressQuality() {\n        return mCompressQuality;\n    }\n\n    public Uri getImageInputUri() {\n        return mImageInputUri;\n    }\n\n    public String getImageOutputPath() {\n        return mImageOutputPath;\n    }\n\n    public ExifInfo getExifInfo() {\n        return mExifInfo;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/model/CutInfo.java",
    "content": "package com.matisse.ucrop.model;\n\nimport java.io.Serializable;\n\n/**\n * @author：luck\n * @data：2017/05/30 晚上23:00\n * @描述: CutInfo\n */\npublic class CutInfo implements Serializable {\n    /**\n     * File ID\n     */\n    private long id;\n    /**\n     * 原图\n     */\n    private String path;\n    /**\n     * 裁剪路径\n     */\n    private String cutPath;\n    /**\n     * Android Q特有地址\n     */\n    private String androidQToPath;\n    /**\n     * 裁剪比例\n     */\n    private int offsetX;\n    /**\n     * 裁剪比例\n     */\n    private int offsetY;\n    /**\n     * 图片宽\n     */\n    private int imageWidth;\n    /**\n     * 图片高\n     */\n    private int imageHeight;\n    /**\n     * 是否裁剪\n     */\n    private boolean isCut;\n\n    /**\n     * 资源类型\n     */\n    private String mimeType;\n\n    private float resultAspectRatio;\n\n    public CutInfo() {\n    }\n\n    public CutInfo(String path, boolean isCut) {\n        this.path = path;\n        this.isCut = isCut;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public void setPath(String path) {\n        this.path = path;\n    }\n\n    public String getCutPath() {\n        return cutPath;\n    }\n\n    public void setCutPath(String cutPath) {\n        this.cutPath = cutPath;\n    }\n\n    public int getOffsetX() {\n        return offsetX;\n    }\n\n    public void setOffsetX(int offsetX) {\n        this.offsetX = offsetX;\n    }\n\n    public int getOffsetY() {\n        return offsetY;\n    }\n\n    public void setOffsetY(int offsetY) {\n        this.offsetY = offsetY;\n    }\n\n    public int getImageWidth() {\n        return imageWidth;\n    }\n\n    public void setImageWidth(int imageWidth) {\n        this.imageWidth = imageWidth;\n    }\n\n    public int getImageHeight() {\n        return imageHeight;\n    }\n\n    public void setImageHeight(int imageHeight) {\n        this.imageHeight = imageHeight;\n    }\n\n    public float getResultAspectRatio() {\n        return resultAspectRatio;\n    }\n\n    public void setResultAspectRatio(float resultAspectRatio) {\n        this.resultAspectRatio = resultAspectRatio;\n    }\n\n    public String getMimeType() {\n        return mimeType;\n    }\n\n    public void setMimeType(String mimeType) {\n        this.mimeType = mimeType;\n    }\n\n    public boolean isCut() {\n        return isCut;\n    }\n\n    public void setCut(boolean cut) {\n        isCut = cut;\n    }\n\n    public String getAndroidQToPath() {\n        return androidQToPath;\n    }\n\n    public void setAndroidQToPath(String androidQToPath) {\n        this.androidQToPath = androidQToPath;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public void setId(long id) {\n        this.id = id;\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/model/ExifInfo.java",
    "content": "package com.matisse.ucrop.model;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.\n */\npublic class ExifInfo {\n\n    private int mExifOrientation;\n    private int mExifDegrees;\n    private int mExifTranslation;\n\n    public ExifInfo(int exifOrientation, int exifDegrees, int exifTranslation) {\n        mExifOrientation = exifOrientation;\n        mExifDegrees = exifDegrees;\n        mExifTranslation = exifTranslation;\n    }\n\n    public int getExifOrientation() {\n        return mExifOrientation;\n    }\n\n    public int getExifDegrees() {\n        return mExifDegrees;\n    }\n\n    public int getExifTranslation() {\n        return mExifTranslation;\n    }\n\n    public void setExifOrientation(int exifOrientation) {\n        mExifOrientation = exifOrientation;\n    }\n\n    public void setExifDegrees(int exifDegrees) {\n        mExifDegrees = exifDegrees;\n    }\n\n    public void setExifTranslation(int exifTranslation) {\n        mExifTranslation = exifTranslation;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        ExifInfo exifInfo = (ExifInfo) o;\n\n        if (mExifOrientation != exifInfo.mExifOrientation) return false;\n        if (mExifDegrees != exifInfo.mExifDegrees) return false;\n        return mExifTranslation == exifInfo.mExifTranslation;\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = mExifOrientation;\n        result = 31 * result + mExifDegrees;\n        result = 31 * result + mExifTranslation;\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/model/ImageState.java",
    "content": "package com.matisse.ucrop.model;\n\nimport android.graphics.RectF;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.\n */\npublic class ImageState {\n\n    private RectF mCropRect;\n    private RectF mCurrentImageRect;\n\n    private float mCurrentScale, mCurrentAngle;\n\n    public ImageState(RectF cropRect, RectF currentImageRect, float currentScale, float currentAngle) {\n        mCropRect = cropRect;\n        mCurrentImageRect = currentImageRect;\n        mCurrentScale = currentScale;\n        mCurrentAngle = currentAngle;\n    }\n\n    public RectF getCropRect() {\n        return mCropRect;\n    }\n\n    public RectF getCurrentImageRect() {\n        return mCurrentImageRect;\n    }\n\n    public float getCurrentScale() {\n        return mCurrentScale;\n    }\n\n    public float getCurrentAngle() {\n        return mCurrentAngle;\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/task/BitmapCropTask.java",
    "content": "package com.matisse.ucrop.task;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.media.ExifInterface;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.ParcelFileDescriptor;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.matisse.ucrop.callback.BitmapCropCallback;\nimport com.matisse.ucrop.model.CropParameters;\nimport com.matisse.ucrop.model.ExifInfo;\nimport com.matisse.ucrop.model.ImageState;\nimport com.matisse.ucrop.util.BitmapLoadUtils;\nimport com.matisse.ucrop.util.FileUtils;\nimport com.matisse.ucrop.util.ImageHeaderParser;\nimport com.matisse.ucrop.util.VersionUtils;\n\nimport java.io.File;\nimport java.io.FileDescriptor;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.lang.ref.WeakReference;\n\n/**\n * Crops part of image that fills the crop bounds.\n * <p/>\n * First image is downscaled if max size was set and if resulting image is larger that max size.\n * Then image is rotated accordingly.\n * Finally new Bitmap object is created and saved to file.\n */\npublic class BitmapCropTask extends AsyncTask<Void, Void, Throwable> {\n\n    private static final String TAG = \"BitmapCropTask\";\n\n    private final WeakReference<Context> mContext;\n\n    private Bitmap mViewBitmap;\n\n    private final RectF mCropRect;\n    private final RectF mCurrentImageRect;\n\n    private float mCurrentScale, mCurrentAngle;\n    private final int mMaxResultImageSizeX, mMaxResultImageSizeY;\n\n    private final Bitmap.CompressFormat mCompressFormat;\n    private final int mCompressQuality;\n    private final Uri mImageInputUri;\n    private final String mImageOutputPath;\n    private final ExifInfo mExifInfo;\n    private final BitmapCropCallback mCropCallback;\n\n    private int mCroppedImageWidth, mCroppedImageHeight;\n    private int cropOffsetX, cropOffsetY;\n    private boolean mIsCircleCrop;\n\n    public BitmapCropTask(@NonNull Context context, @Nullable Bitmap viewBitmap\n            , @NonNull ImageState imageState, @NonNull CropParameters cropParameters\n            , boolean isCircleCrop, @Nullable BitmapCropCallback cropCallback) {\n\n        mContext = new WeakReference<>(context);\n\n        mViewBitmap = viewBitmap;\n        mCropRect = imageState.getCropRect();\n        mCurrentImageRect = imageState.getCurrentImageRect();\n\n        mCurrentScale = imageState.getCurrentScale();\n        mCurrentAngle = imageState.getCurrentAngle();\n        mMaxResultImageSizeX = cropParameters.getMaxResultImageSizeX();\n        mMaxResultImageSizeY = cropParameters.getMaxResultImageSizeY();\n\n        mCompressFormat = cropParameters.getCompressFormat();\n        mCompressQuality = cropParameters.getCompressQuality();\n\n        mIsCircleCrop = isCircleCrop;\n        mImageInputUri = cropParameters.getImageInputUri();\n        mImageOutputPath = cropParameters.getImageOutputPath();\n        mExifInfo = cropParameters.getExifInfo();\n\n        mCropCallback = cropCallback;\n    }\n\n    @Override\n    @Nullable\n    protected Throwable doInBackground(Void... params) {\n        if (mViewBitmap == null) {\n            return new NullPointerException(\"ViewBitmap is null\");\n        } else if (mViewBitmap.isRecycled()) {\n            return new NullPointerException(\"ViewBitmap is recycled\");\n        } else if (mCurrentImageRect.isEmpty()) {\n            return new NullPointerException(\"CurrentImageRect is empty\");\n        }\n\n\n        try {\n            crop();\n            mViewBitmap = null;\n        } catch (Throwable throwable) {\n            return throwable;\n        }\n\n        return null;\n    }\n\n\n    private boolean crop() throws IOException {\n        // Downsize if needed\n        if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {\n            float cropWidth = mCropRect.width() / mCurrentScale;\n            float cropHeight = mCropRect.height() / mCurrentScale;\n\n            if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {\n\n                float scaleX = mMaxResultImageSizeX / cropWidth;\n                float scaleY = mMaxResultImageSizeY / cropHeight;\n                float resizeScale = Math.min(scaleX, scaleY);\n\n                Bitmap resizedBitmap = Bitmap.createScaledBitmap(mViewBitmap,\n                        Math.round(mViewBitmap.getWidth() * resizeScale),\n                        Math.round(mViewBitmap.getHeight() * resizeScale), false);\n                if (mViewBitmap != resizedBitmap) {\n                    mViewBitmap.recycle();\n                }\n                mViewBitmap = resizedBitmap;\n\n                mCurrentScale /= resizeScale;\n            }\n        }\n\n        // Rotate if needed\n        if (mCurrentAngle != 0) {\n            Matrix tempMatrix = new Matrix();\n            tempMatrix.setRotate(mCurrentAngle, mViewBitmap.getWidth() / 2, mViewBitmap.getHeight() / 2);\n\n            Bitmap rotatedBitmap = Bitmap.createBitmap(mViewBitmap, 0, 0, mViewBitmap.getWidth(), mViewBitmap.getHeight(),\n                    tempMatrix, true);\n            if (mViewBitmap != rotatedBitmap) {\n                mViewBitmap.recycle();\n            }\n            mViewBitmap = rotatedBitmap;\n        }\n\n        cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);\n        cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);\n        mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);\n        mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);\n\n        boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);\n        boolean isAndroidQ = VersionUtils.isAndroidQ();\n        if (shouldCrop) {\n            ExifInterface originalExif;\n            if (isAndroidQ && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {\n                ParcelFileDescriptor parcelFileDescriptor =\n                        mContext.get().getContentResolver().openFileDescriptor(mImageInputUri, \"r\");\n                FileInputStream inputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());\n                originalExif = new ExifInterface(inputStream);\n            } else {\n                originalExif = new ExifInterface(mImageInputUri.getPath());\n            }\n\n            if (mIsCircleCrop) {\n                int cropRadio = Math.min(mCroppedImageWidth, mCroppedImageHeight);\n                saveImage(Bitmap.createBitmap(mViewBitmap, cropOffsetX, cropOffsetY, cropRadio, cropRadio));\n            } else {\n                saveImage(Bitmap.createBitmap(mViewBitmap, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight));\n            }\n            if (mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {\n                if (mIsCircleCrop) {\n                    int cropRadio = Math.min(mCroppedImageWidth, mCroppedImageHeight);\n                    ImageHeaderParser.copyExif(originalExif, cropRadio, cropRadio, mImageOutputPath);\n                } else {\n                    ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);\n                }\n            }\n            return true;\n        } else {\n            if (isAndroidQ) {\n                ParcelFileDescriptor parcelFileDescriptor =\n                        mContext.get().getContentResolver().openFileDescriptor(mImageInputUri, \"r\");\n                FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();\n                FileInputStream inputStream = new FileInputStream(fileDescriptor);\n                FileUtils.copyFile(inputStream, mImageOutputPath);\n            } else {\n                FileUtils.copyFile(mImageInputUri.getPath(), mImageOutputPath);\n            }\n            return true;\n        }\n    }\n\n    private void saveImage(@NonNull Bitmap croppedBitmap) throws FileNotFoundException {\n        Context context = mContext.get();\n        if (context == null) {\n            return;\n        }\n\n        OutputStream outputStream = null;\n        try {\n            outputStream = context.getContentResolver().openOutputStream(Uri.fromFile(new File(mImageOutputPath)));\n            croppedBitmap.compress(mCompressFormat, mCompressQuality, outputStream);\n            croppedBitmap.recycle();\n        } finally {\n            BitmapLoadUtils.close(outputStream);\n        }\n    }\n\n    /**\n     * Check whether an image should be cropped at all or just file can be copied to the destination path.\n     * For each 1000 pixels there is one pixel of error due to matrix calculations etc.\n     *\n     * @param width  - crop area width\n     * @param height - crop area height\n     * @return - true if image must be cropped, false - if original image fits requirements\n     */\n    private boolean shouldCrop(int width, int height) {\n        int pixelError = 1;\n        pixelError += Math.round(Math.max(width, height) / 1000f);\n        return (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0)\n                || Math.abs(mCropRect.left - mCurrentImageRect.left) > pixelError\n                || Math.abs(mCropRect.top - mCurrentImageRect.top) > pixelError\n                || Math.abs(mCropRect.bottom - mCurrentImageRect.bottom) > pixelError\n                || Math.abs(mCropRect.right - mCurrentImageRect.right) > pixelError;\n    }\n\n    @Override\n    protected void onPostExecute(@Nullable Throwable t) {\n        if (mCropCallback != null) {\n            if (t == null) {\n                Uri uri = Uri.fromFile(new File(mImageOutputPath));\n                mCropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight);\n            } else {\n                mCropCallback.onCropFailure(t);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/task/BitmapLoadShowTask.java",
    "content": "package com.matisse.ucrop.task;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.ParcelFileDescriptor;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.matisse.ucrop.callback.BitmapLoadShowCallback;\nimport com.matisse.ucrop.model.ExifInfo;\nimport com.matisse.ucrop.util.BitmapLoadUtils;\n\nimport java.io.FileDescriptor;\nimport java.io.FileNotFoundException;\n\n/**\n * Creates and returns a Bitmap for a given Uri(String url).\n * inSampleSize is calculated based on requiredWidth property. However can be adjusted if OOM occurs.\n * If any EXIF config is found - bitmap is transformed properly.\n */\npublic class BitmapLoadShowTask extends AsyncTask<Void, Void, BitmapLoadShowTask.BitmapWorkerResult> {\n\n    private static final String TAG = \"BitmapWorkerTask\";\n\n    private final Context mContext;\n    private Uri mInputUri;\n    private final int mRequiredWidth;\n    private final int mRequiredHeight;\n\n    private final BitmapLoadShowCallback mBitmapLoadShowCallback;\n\n    public static class BitmapWorkerResult {\n\n        Bitmap mBitmapResult;\n        ExifInfo mExifInfo;\n        Exception mBitmapWorkerException;\n\n        public BitmapWorkerResult(@NonNull Bitmap bitmapResult, @NonNull ExifInfo exifInfo) {\n            mBitmapResult = bitmapResult;\n            mExifInfo = exifInfo;\n        }\n\n        public BitmapWorkerResult(@NonNull Exception bitmapWorkerException) {\n            mBitmapWorkerException = bitmapWorkerException;\n        }\n\n    }\n\n    public BitmapLoadShowTask(@NonNull Context context,\n                              @NonNull Uri inputUri,\n                              int requiredWidth, int requiredHeight,\n                              BitmapLoadShowCallback loadCallback) {\n        mContext = context;\n        mInputUri = inputUri;\n        mRequiredWidth = requiredWidth;\n        mRequiredHeight = requiredHeight;\n        mBitmapLoadShowCallback = loadCallback;\n    }\n\n    @Override\n    @NonNull\n    protected BitmapWorkerResult doInBackground(Void... params) {\n        if (mInputUri == null) {\n            return new BitmapWorkerResult(new NullPointerException(\"Input Uri cannot be null\"));\n        }\n\n        final ParcelFileDescriptor parcelFileDescriptor;\n        try {\n            parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(mInputUri, \"r\");\n        } catch (FileNotFoundException e) {\n            return new BitmapWorkerResult(e);\n        }\n\n        final FileDescriptor fileDescriptor;\n        if (parcelFileDescriptor != null) {\n            fileDescriptor = parcelFileDescriptor.getFileDescriptor();\n        } else {\n            return new BitmapWorkerResult(new NullPointerException(\"ParcelFileDescriptor was null for given Uri: [\" + mInputUri + \"]\"));\n        }\n\n        final BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);\n        if (options.outWidth == -1 || options.outHeight == -1) {\n            return new BitmapWorkerResult(new IllegalArgumentException(\"Bounds for bitmap could not be retrieved from the Uri: [\" + mInputUri + \"]\"));\n        }\n\n        options.inSampleSize = BitmapLoadUtils.calculateInSampleSize(options, mRequiredWidth, mRequiredHeight);\n        options.inJustDecodeBounds = false;\n\n        Bitmap decodeSampledBitmap = null;\n\n        boolean decodeAttemptSuccess = false;\n        while (!decodeAttemptSuccess) {\n            try {\n                decodeSampledBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);\n                decodeAttemptSuccess = true;\n            } catch (OutOfMemoryError error) {\n                Log.e(TAG, \"doInBackground: BitmapFactory.decodeFileDescriptor: \", error);\n                options.inSampleSize *= 2;\n            }\n        }\n\n        if (decodeSampledBitmap == null) {\n            return new BitmapWorkerResult(new IllegalArgumentException(\"Bitmap could not be decoded from the Uri: [\" + mInputUri + \"]\"));\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {\n            BitmapLoadUtils.close(parcelFileDescriptor);\n        }\n\n        int exifOrientation = BitmapLoadUtils.getExifOrientation(mContext, mInputUri);\n        int exifDegrees = BitmapLoadUtils.exifToDegrees(exifOrientation);\n        int exifTranslation = BitmapLoadUtils.exifToTranslation(exifOrientation);\n\n        ExifInfo exifInfo = new ExifInfo(exifOrientation, exifDegrees, exifTranslation);\n\n        Matrix matrix = new Matrix();\n        if (exifDegrees != 0) {\n            matrix.preRotate(exifDegrees);\n        }\n        if (exifTranslation != 1) {\n            matrix.postScale(exifTranslation, 1);\n        }\n        if (!matrix.isIdentity()) {\n            return new BitmapWorkerResult(BitmapLoadUtils.transformBitmap(decodeSampledBitmap, matrix), exifInfo);\n        }\n\n        return new BitmapWorkerResult(decodeSampledBitmap, exifInfo);\n    }\n\n\n    @Override\n    protected void onPostExecute(@NonNull BitmapWorkerResult result) {\n        if (result.mBitmapWorkerException == null) {\n            mBitmapLoadShowCallback.onBitmapLoaded(result.mBitmapResult);\n        } else {\n            mBitmapLoadShowCallback.onFailure(result.mBitmapWorkerException);\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/task/BitmapLoadTask.java",
    "content": "package com.matisse.ucrop.task;\n\nimport android.Manifest.permission;\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.ParcelFileDescriptor;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport com.matisse.ucrop.callback.BitmapLoadCallback;\nimport com.matisse.ucrop.model.ExifInfo;\nimport com.matisse.ucrop.util.BitmapLoadUtils;\nimport com.matisse.ucrop.util.VersionUtils;\nimport com.matisse.ucrop.util.FileUtils;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileDescriptor;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.URL;\n\n/**\n * Creates and returns a Bitmap for a given Uri(String url).\n * inSampleSize is calculated based on requiredWidth property. However can be adjusted if OOM occurs.\n * If any EXIF config is found - bitmap is transformed properly.\n */\npublic class BitmapLoadTask extends AsyncTask<Void, Void, BitmapLoadTask.BitmapWorkerResult> {\n\n    private static final String TAG = \"BitmapWorkerTask\";\n\n    private final Context mContext;\n    private Uri mInputUri;\n    private Uri mOutputUri;\n    private final int mRequiredWidth;\n    private final int mRequiredHeight;\n\n    private final BitmapLoadCallback mBitmapLoadCallback;\n\n    public static class BitmapWorkerResult {\n\n        Bitmap mBitmapResult;\n        ExifInfo mExifInfo;\n        Exception mBitmapWorkerException;\n\n        public BitmapWorkerResult(@NonNull Bitmap bitmapResult, @NonNull ExifInfo exifInfo) {\n            mBitmapResult = bitmapResult;\n            mExifInfo = exifInfo;\n        }\n\n        public BitmapWorkerResult(@NonNull Exception bitmapWorkerException) {\n            mBitmapWorkerException = bitmapWorkerException;\n        }\n\n    }\n\n    public BitmapLoadTask(@NonNull Context context,\n                          @NonNull Uri inputUri, @Nullable Uri outputUri,\n                          int requiredWidth, int requiredHeight,\n                          BitmapLoadCallback loadCallback) {\n        mContext = context;\n        mInputUri = inputUri;\n        mOutputUri = outputUri;\n        mRequiredWidth = requiredWidth;\n        mRequiredHeight = requiredHeight;\n        mBitmapLoadCallback = loadCallback;\n    }\n\n    @Override\n    @NonNull\n    protected BitmapWorkerResult doInBackground(Void... params) {\n        if (mInputUri == null) {\n            return new BitmapWorkerResult(new NullPointerException(\"Input Uri cannot be null\"));\n        }\n\n        try {\n            processInputUri();\n        } catch (NullPointerException | IOException e) {\n            return new BitmapWorkerResult(e);\n        }\n\n        final ParcelFileDescriptor parcelFileDescriptor;\n        try {\n            parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(mInputUri, \"r\");\n        } catch (FileNotFoundException e) {\n            return new BitmapWorkerResult(e);\n        }\n\n        final FileDescriptor fileDescriptor;\n        if (parcelFileDescriptor != null) {\n            fileDescriptor = parcelFileDescriptor.getFileDescriptor();\n        } else {\n            return new BitmapWorkerResult(new NullPointerException(\"ParcelFileDescriptor was null for given Uri: [\" + mInputUri + \"]\"));\n        }\n\n        final BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);\n        if (options.outWidth == -1 || options.outHeight == -1) {\n            return new BitmapWorkerResult(new IllegalArgumentException(\"Bounds for bitmap could not be retrieved from the Uri: [\" + mInputUri + \"]\"));\n        }\n\n        options.inSampleSize = BitmapLoadUtils.calculateInSampleSize(options, mRequiredWidth, mRequiredHeight);\n        options.inJustDecodeBounds = false;\n\n        Bitmap decodeSampledBitmap = null;\n\n        boolean decodeAttemptSuccess = false;\n        while (!decodeAttemptSuccess) {\n            try {\n                decodeSampledBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);\n                decodeAttemptSuccess = true;\n            } catch (OutOfMemoryError error) {\n                Log.e(TAG, \"doInBackground: BitmapFactory.decodeFileDescriptor: \", error);\n                options.inSampleSize *= 2;\n            }\n        }\n\n        if (decodeSampledBitmap == null) {\n            return new BitmapWorkerResult(new IllegalArgumentException(\"Bitmap could not be decoded from the Uri: [\" + mInputUri + \"]\"));\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {\n            BitmapLoadUtils.close(parcelFileDescriptor);\n        }\n\n        int exifOrientation = BitmapLoadUtils.getExifOrientation(mContext, mInputUri);\n        int exifDegrees = BitmapLoadUtils.exifToDegrees(exifOrientation);\n        int exifTranslation = BitmapLoadUtils.exifToTranslation(exifOrientation);\n\n        ExifInfo exifInfo = new ExifInfo(exifOrientation, exifDegrees, exifTranslation);\n\n        Matrix matrix = new Matrix();\n        if (exifDegrees != 0) {\n            matrix.preRotate(exifDegrees);\n        }\n        if (exifTranslation != 1) {\n            matrix.postScale(exifTranslation, 1);\n        }\n        if (!matrix.isIdentity()) {\n            return new BitmapWorkerResult(BitmapLoadUtils.transformBitmap(decodeSampledBitmap, matrix), exifInfo);\n        }\n\n        return new BitmapWorkerResult(decodeSampledBitmap, exifInfo);\n    }\n\n    private void processInputUri() throws NullPointerException, IOException {\n        String inputUriScheme = mInputUri.toString();\n        Log.d(TAG, \"Uri scheme: \" + inputUriScheme);\n        if (inputUriScheme.startsWith(\"http\") || inputUriScheme.startsWith(\"https\")) {\n            try {\n                downloadFile(mInputUri, mOutputUri);\n            } catch (NullPointerException | IOException e) {\n                Log.e(TAG, \"Downloading failed\", e);\n                throw e;\n            }\n        } else {\n            String path = getFilePath();\n            if (!TextUtils.isEmpty(path) && new File(path).exists()) {\n                mInputUri = VersionUtils.isAndroidQ() ?\n                        mInputUri : Uri.fromFile(new File(path));\n            } else {\n                try {\n                    copyFile(mInputUri, mOutputUri);\n                } catch (NullPointerException | IOException e) {\n                    Log.e(TAG, \"Copying failed\", e);\n                    throw e;\n                }\n            }\n        }\n    }\n\n    private String getFilePath() {\n        if (ContextCompat.checkSelfPermission(mContext, permission.READ_EXTERNAL_STORAGE)\n                == PackageManager.PERMISSION_GRANTED) {\n            return FileUtils.getPath(mContext, mInputUri);\n        } else {\n            return null;\n        }\n    }\n\n    private void copyFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws NullPointerException, IOException {\n        Log.d(TAG, \"copyFile\");\n\n        if (outputUri == null) {\n            throw new NullPointerException(\"Output Uri is null - cannot copy image\");\n        }\n\n        InputStream inputStream = null;\n        OutputStream outputStream = null;\n        try {\n            inputStream = mContext.getContentResolver().openInputStream(inputUri);\n            outputStream = new FileOutputStream(new File(outputUri.getPath()));\n            if (inputStream == null) {\n                throw new NullPointerException(\"InputStream for given input Uri is null\");\n            }\n\n            byte buffer[] = new byte[1024];\n            int length;\n            while ((length = inputStream.read(buffer)) > 0) {\n                outputStream.write(buffer, 0, length);\n            }\n        } finally {\n            BitmapLoadUtils.close(outputStream);\n            BitmapLoadUtils.close(inputStream);\n\n            // swap uris, because input image was copied to the output destination\n            // (cropped image will override it later)\n            mInputUri = mOutputUri;\n        }\n    }\n\n    private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws NullPointerException, IOException {\n        Log.d(TAG, \"downloadFile\");\n\n        if (outputUri == null) {\n            throw new NullPointerException(\"Output Uri is null - cannot download image\");\n        }\n        try {\n            URL u = new URL(inputUri.toString());\n            byte[] buffer = new byte[1024];\n            int read;\n            BufferedInputStream bin;\n            bin = new BufferedInputStream(u.openStream());\n            OutputStream outputStream = mContext.getContentResolver().openOutputStream(outputUri);\n            BufferedOutputStream bout = new BufferedOutputStream(\n                    outputStream);\n            while ((read = bin.read(buffer)) > -1) {\n                bout.write(buffer, 0, read);\n            }\n            bout.flush();\n            bout.close();\n            bin.close();\n            outputStream.close();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n\n        }\n        mInputUri = mOutputUri;\n    }\n\n//    private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws NullPointerException, IOException {\n//        Log.d(TAG, \"downloadFile\");\n//\n//        if (outputUri == null) {\n//            throw new NullPointerException(\"Output Uri is null - cannot download image\");\n//        }\n//\n//        OkHttpClient client = new OkHttpClient();\n//\n//        BufferedSource source = null;\n//        Sink sink = null;\n//        Response response = null;\n//        try {\n//            Request request = new Request.Builder()\n//                    .url(inputUri.toString())\n//                    .build();\n//            response = client.newCall(request).execute();\n//            source = response.body().source();\n//\n//            OutputStream outputStream = mContext.getContentResolver().openOutputStream(outputUri);\n//            if (outputStream != null) {\n//                sink = Okio.sink(outputStream);\n//                source.readAll(sink);\n//            } else {\n//                throw new NullPointerException(\"OutputStream for given output Uri is null\");\n//            }\n//        } finally {\n//            BitmapLoadUtils.close(source);\n//            BitmapLoadUtils.close(sink);\n//            if (response != null) {\n//                BitmapLoadUtils.close(response.body());\n//            }\n//            client.dispatcher().cancelAll();\n//\n//            // swap uris, because input image was downloaded to the output destination\n//            // (cropped image will override it later)\n//            mInputUri = mOutputUri;\n//        }\n//    }\n\n    @Override\n    protected void onPostExecute(@NonNull BitmapWorkerResult result) {\n        if (result.mBitmapWorkerException == null) {\n            mBitmapLoadCallback.onBitmapLoaded(result.mBitmapResult, result.mExifInfo, mInputUri, (mOutputUri == null) ? null : mOutputUri);\n        } else {\n            mBitmapLoadCallback.onFailure(result.mBitmapWorkerException);\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/BitmapLoadUtils.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.Matrix;\nimport android.graphics.Point;\nimport android.media.ExifInterface;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.util.Log;\nimport android.view.Display;\nimport android.view.WindowManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.matisse.ucrop.callback.BitmapLoadCallback;\nimport com.matisse.ucrop.callback.BitmapLoadShowCallback;\nimport com.matisse.ucrop.task.BitmapLoadShowTask;\nimport com.matisse.ucrop.task.BitmapLoadTask;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\npublic class BitmapLoadUtils {\n\n    private static final String TAG = \"BitmapLoadUtils\";\n\n    public static void decodeBitmapInBackground(@NonNull Context context,\n                                                @NonNull Uri uri, @Nullable Uri outputUri,\n                                                int requiredWidth, int requiredHeight,\n                                                BitmapLoadCallback loadCallback) {\n\n        new BitmapLoadTask(context, uri, outputUri, requiredWidth, requiredHeight, loadCallback).execute();\n    }\n\n    public static void decodeBitmapInBackground(@NonNull Context context,\n                                                @NonNull Uri uri,\n                                                int requiredWidth,\n                                                int requiredHeight,\n                                                BitmapLoadShowCallback loadCallback) {\n\n        new BitmapLoadShowTask(context, uri, requiredWidth, requiredHeight, loadCallback).execute();\n    }\n\n    public static Bitmap transformBitmap(@NonNull Bitmap bitmap, @NonNull Matrix transformMatrix) {\n        try {\n            Bitmap converted = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transformMatrix, true);\n            if (!bitmap.sameAs(converted)) {\n                bitmap = converted;\n            }\n        } catch (OutOfMemoryError error) {\n            Log.e(TAG, \"transformBitmap: \", error);\n        }\n        return bitmap;\n    }\n\n    public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {\n        // Raw height and width of image\n        final int height = options.outHeight;\n        final int width = options.outWidth;\n        int inSampleSize = 1;\n\n        if (height > reqHeight || width > reqWidth) {\n            // Calculate the largest inSampleSize value that is a power of 2 and keeps both\n            // height and width lower or equal to the requested height and width.\n            while ((height / inSampleSize) > reqHeight || (width / inSampleSize) > reqWidth) {\n                inSampleSize *= 2;\n            }\n        }\n        return inSampleSize;\n    }\n\n    public static int getExifOrientation(@NonNull Context context, @NonNull Uri imageUri) {\n        int orientation = ExifInterface.ORIENTATION_UNDEFINED;\n        try {\n            InputStream stream = context.getContentResolver().openInputStream(imageUri);\n            if (stream == null) {\n                return orientation;\n            }\n            orientation = new ImageHeaderParser(stream).getOrientation();\n            close(stream);\n        } catch (IOException e) {\n            Log.e(TAG, \"getExifOrientation: \" + imageUri.toString(), e);\n        }\n        return orientation;\n    }\n\n    public static int exifToDegrees(int exifOrientation) {\n        int rotation;\n        switch (exifOrientation) {\n            case ExifInterface.ORIENTATION_ROTATE_90:\n            case ExifInterface.ORIENTATION_TRANSPOSE:\n                rotation = 90;\n                break;\n            case ExifInterface.ORIENTATION_ROTATE_180:\n            case ExifInterface.ORIENTATION_FLIP_VERTICAL:\n                rotation = 180;\n                break;\n            case ExifInterface.ORIENTATION_ROTATE_270:\n            case ExifInterface.ORIENTATION_TRANSVERSE:\n                rotation = 270;\n                break;\n            default:\n                rotation = 0;\n        }\n        return rotation;\n    }\n\n    public static int exifToTranslation(int exifOrientation) {\n        int translation;\n        switch (exifOrientation) {\n            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:\n            case ExifInterface.ORIENTATION_FLIP_VERTICAL:\n            case ExifInterface.ORIENTATION_TRANSPOSE:\n            case ExifInterface.ORIENTATION_TRANSVERSE:\n                translation = -1;\n                break;\n            default:\n                translation = 1;\n        }\n        return translation;\n    }\n\n    /**\n     * This method calculates maximum size of both width and height of bitmap.\n     * It is twice the device screen diagonal for default implementation (extra quality to zoom image).\n     * Size cannot exceed max texture size.\n     *\n     * @return - max bitmap size in pixels.\n     */\n    @SuppressWarnings({\"SuspiciousNameCombination\", \"deprecation\"})\n    public static int calculateMaxBitmapSize(@NonNull Context context) {\n        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n        Display display = wm.getDefaultDisplay();\n\n        Point size = new Point();\n        int width, height;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {\n            display.getSize(size);\n            width = size.x;\n            height = size.y;\n        } else {\n            width = display.getWidth();\n            height = display.getHeight();\n        }\n\n        // Twice the device screen diagonal as default\n        int maxBitmapSize = (int) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));\n\n        // Check for max texture size via Canvas\n        Canvas canvas = new Canvas();\n        final int maxCanvasSize = Math.min(canvas.getMaximumBitmapWidth(), canvas.getMaximumBitmapHeight());\n        if (maxCanvasSize > 0) {\n            maxBitmapSize = Math.min(maxBitmapSize, maxCanvasSize);\n        }\n\n        // Check for max texture size via GL\n        final int maxTextureSize = EglUtils.getMaxTextureSize();\n        if (maxTextureSize > 0) {\n            maxBitmapSize = Math.min(maxBitmapSize, maxTextureSize);\n        }\n\n        Log.d(TAG, \"maxBitmapSize: \" + maxBitmapSize);\n        return maxBitmapSize;\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public static void close(@Nullable Closeable c) {\n        if (c != null && c instanceof Closeable) { // java.lang.IncompatibleClassChangeError: interface not implemented\n            try {\n                c.close();\n            } catch (IOException e) {\n                // silence\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/CubicEasing.java",
    "content": "package com.matisse.ucrop.util;\n\npublic final class CubicEasing {\n\n    public static float easeOut(float time, float start, float end, float duration) {\n        return end * ((time = time / duration - 1.0f) * time * time + 1.0f) + start;\n    }\n\n    public static float easeIn(float time, float start, float end, float duration) {\n        return end * (time /= duration) * time * time + start;\n    }\n\n    public static float easeInOut(float time, float start, float end, float duration) {\n        return (time /= duration / 2.0f) < 1.0f ? end / 2.0f * time * time * time + start : end / 2.0f * ((time -= 2.0f) * time * time + 2.0f) + start;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/EglUtils.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.annotation.TargetApi;\nimport android.opengl.EGL14;\nimport android.opengl.EGLConfig;\nimport android.opengl.EGLContext;\nimport android.opengl.EGLDisplay;\nimport android.opengl.EGLSurface;\nimport android.opengl.GLES10;\nimport android.opengl.GLES20;\nimport android.os.Build;\nimport android.util.Log;\n\nimport javax.microedition.khronos.egl.EGL10;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 9/8/16.\n */\npublic class EglUtils {\n\n    private static final String TAG = \"EglUtils\";\n\n    private EglUtils() {\n\n    }\n\n    public static int getMaxTextureSize() {\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n                return getMaxTextureEgl14();\n            } else {\n                return getMaxTextureEgl10();\n            }\n        } catch (Exception e) {\n            Log.d(TAG, \"getMaxTextureSize: \", e);\n            return 0;\n        }\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)\n    private static int getMaxTextureEgl14() {\n        EGLDisplay dpy = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);\n        int[] vers = new int[2];\n        EGL14.eglInitialize(dpy, vers, 0, vers, 1);\n\n        int[] configAttr = {\n                EGL14.EGL_COLOR_BUFFER_TYPE, EGL14.EGL_RGB_BUFFER,\n                EGL14.EGL_LEVEL, 0,\n                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,\n                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT,\n                EGL14.EGL_NONE\n        };\n        EGLConfig[] configs = new EGLConfig[1];\n        int[] numConfig = new int[1];\n        EGL14.eglChooseConfig(dpy, configAttr, 0,\n                configs, 0, 1, numConfig, 0);\n        if (numConfig[0] == 0) {\n            return 0;\n        }\n        EGLConfig config = configs[0];\n\n        int[] surfAttr = {\n                EGL14.EGL_WIDTH, 64,\n                EGL14.EGL_HEIGHT, 64,\n                EGL14.EGL_NONE\n        };\n        EGLSurface surf = EGL14.eglCreatePbufferSurface(dpy, config, surfAttr, 0);\n\n        int[] ctxAttrib = {\n                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,\n                EGL14.EGL_NONE\n        };\n        EGLContext ctx = EGL14.eglCreateContext(dpy, config, EGL14.EGL_NO_CONTEXT, ctxAttrib, 0);\n\n        EGL14.eglMakeCurrent(dpy, surf, surf, ctx);\n\n        int[] maxSize = new int[1];\n        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxSize, 0);\n\n        EGL14.eglMakeCurrent(dpy, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,\n                EGL14.EGL_NO_CONTEXT);\n        EGL14.eglDestroySurface(dpy, surf);\n        EGL14.eglDestroyContext(dpy, ctx);\n        EGL14.eglTerminate(dpy);\n\n        return maxSize[0];\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    private static int getMaxTextureEgl10() {\n        EGL10 egl = (EGL10) javax.microedition.khronos.egl.EGLContext.getEGL();\n\n        javax.microedition.khronos.egl.EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);\n        int[] vers = new int[2];\n        egl.eglInitialize(dpy, vers);\n\n        int[] configAttr = {\n                EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RGB_BUFFER,\n                EGL10.EGL_LEVEL, 0,\n                EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,\n                EGL10.EGL_NONE\n        };\n        javax.microedition.khronos.egl.EGLConfig[] configs = new javax.microedition.khronos.egl.EGLConfig[1];\n        int[] numConfig = new int[1];\n        egl.eglChooseConfig(dpy, configAttr, configs, 1, numConfig);\n        if (numConfig[0] == 0) {\n            return 0;\n        }\n        javax.microedition.khronos.egl.EGLConfig config = configs[0];\n\n        int[] surfAttr = {\n                EGL10.EGL_WIDTH, 64,\n                EGL10.EGL_HEIGHT, 64,\n                EGL10.EGL_NONE\n        };\n        javax.microedition.khronos.egl.EGLSurface surf = egl.eglCreatePbufferSurface(dpy, config, surfAttr);\n        final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;  // missing in EGL10\n        int[] ctxAttrib = {\n                EGL_CONTEXT_CLIENT_VERSION, 1,\n                EGL10.EGL_NONE\n        };\n        javax.microedition.khronos.egl.EGLContext ctx = egl.eglCreateContext(dpy, config, EGL10.EGL_NO_CONTEXT, ctxAttrib);\n        egl.eglMakeCurrent(dpy, surf, surf, ctx);\n        int[] maxSize = new int[1];\n        GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0);\n        egl.eglMakeCurrent(dpy, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,\n                EGL10.EGL_NO_CONTEXT);\n        egl.eglDestroySurface(dpy, surf);\n        egl.eglDestroyContext(dpy, ctx);\n        egl.eglTerminate(dpy);\n\n        return maxSize[0];\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/FastBitmapDrawable.java",
    "content": "/*\n * Copyright (C) 2008 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 */\npackage com.matisse.ucrop.util;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.ColorFilter;\nimport android.graphics.Paint;\nimport android.graphics.PixelFormat;\nimport android.graphics.drawable.Drawable;\n\npublic class FastBitmapDrawable extends Drawable {\n\n    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);\n\n    private Bitmap mBitmap;\n    private int mAlpha;\n    private int mWidth, mHeight;\n\n    public FastBitmapDrawable(Bitmap b) {\n        mAlpha = 255;\n        setBitmap(b);\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        if (mBitmap != null && !mBitmap.isRecycled()) {\n            canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);\n        }\n    }\n\n    @Override\n    public void setColorFilter(ColorFilter cf) {\n        mPaint.setColorFilter(cf);\n    }\n\n    @Override\n    public int getOpacity() {\n        return PixelFormat.TRANSLUCENT;\n    }\n\n    public void setFilterBitmap(boolean filterBitmap) {\n        mPaint.setFilterBitmap(filterBitmap);\n    }\n\n    public int getAlpha() {\n        return mAlpha;\n    }\n\n    @Override\n    public void setAlpha(int alpha) {\n        mAlpha = alpha;\n        mPaint.setAlpha(alpha);\n    }\n\n    @Override\n    public int getIntrinsicWidth() {\n        return mWidth;\n    }\n\n    @Override\n    public int getIntrinsicHeight() {\n        return mHeight;\n    }\n\n    @Override\n    public int getMinimumWidth() {\n        return mWidth;\n    }\n\n    @Override\n    public int getMinimumHeight() {\n        return mHeight;\n    }\n\n    public Bitmap getBitmap() {\n        return mBitmap;\n    }\n\n    public void setBitmap(Bitmap b) {\n        mBitmap = b;\n        if (b != null) {\n            mWidth = mBitmap.getWidth();\n            mHeight = mBitmap.getHeight();\n        } else {\n            mWidth = mHeight = 0;\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/FileUtils.java",
    "content": "/*\n * Copyright (C) 2007-2008 OpenIntents.org\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.matisse.ucrop.util;\n\nimport android.annotation.SuppressLint;\nimport android.content.ContentUris;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.graphics.BitmapFactory;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.DocumentsContract;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.channels.FileChannel;\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\n\n/**\n * @author Peli\n * @author paulburke (ipaulpro)\n * @version 2013-12-11\n */\npublic class FileUtils {\n    private static SimpleDateFormat sf = new SimpleDateFormat(\"yyyyMMdd_HHmmssSS\");\n    /**\n     * TAG for log messages.\n     */\n    static final String TAG = \"FileUtils\";\n\n    private FileUtils() {\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is ExternalStorageProvider.\n     * @author paulburke\n     */\n    public static boolean isExternalStorageDocument(Uri uri) {\n        return \"com.android.externalstorage.documents\".equals(uri.getAuthority());\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is DownloadsProvider.\n     * @author paulburke\n     */\n    public static boolean isDownloadsDocument(Uri uri) {\n        return \"com.android.providers.downloads.documents\".equals(uri.getAuthority());\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is MediaProvider.\n     * @author paulburke\n     */\n    public static boolean isMediaDocument(Uri uri) {\n        return \"com.android.providers.media.documents\".equals(uri.getAuthority());\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is Google Photos.\n     */\n    public static boolean isGooglePhotosUri(Uri uri) {\n        return \"com.google.android.apps.photos.content\".equals(uri.getAuthority());\n    }\n\n    /**\n     * Get the value of the data column for this Uri. This is useful for\n     * MediaStore Uris, and other file-based ContentProviders.\n     *\n     * @param context       The context.\n     * @param uri           The Uri to query.\n     * @param selection     (Optional) Filter used in the query.\n     * @param selectionArgs (Optional) Selection arguments used in the query.\n     * @return The value of the _data column, which is typically a file path.\n     * @author paulburke\n     */\n    public static String getDataColumn(Context context, Uri uri, String selection,\n                                       String[] selectionArgs) {\n\n        Cursor cursor = null;\n        final String column = \"_data\";\n        final String[] projection = {\n                column\n        };\n\n        try {\n            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,\n                    null);\n            if (cursor != null && cursor.moveToFirst()) {\n                final int column_index = cursor.getColumnIndexOrThrow(column);\n                return cursor.getString(column_index);\n            }\n        } catch (IllegalArgumentException ex) {\n            Log.i(TAG, String.format(Locale.getDefault(), \"getDataColumn: _data - [%s]\", ex.getMessage()));\n        } finally {\n            if (cursor != null) {\n                cursor.close();\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Get a file path from a Uri. This will get the the path for Storage Access\n     * Framework Documents, as well as the _data field for the MediaStore and\n     * other file-based ContentProviders.<br>\n     * <br>\n     * Callers should check whether the path is local before assuming it\n     * represents a local file.\n     *\n     * @param context The context.\n     * @param uri     The Uri to query.\n     * @author paulburke\n     */\n    @SuppressLint(\"NewApi\")\n    public static String getPath(final Context context, final Uri uri) {\n        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;\n\n        // DocumentProvider\n        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {\n            if (isExternalStorageDocument(uri)) {\n                final String docId = DocumentsContract.getDocumentId(uri);\n                final String[] split = docId.split(\":\");\n                final String type = split[0];\n\n                if (\"primary\".equalsIgnoreCase(type)) {\n                    return context\n                            .getExternalFilesDir(Environment.DIRECTORY_PICTURES) + \"/\" + split[1];\n                }\n\n                // TODO handle non-primary volumes\n            }\n            // DownloadsProvider\n            else if (isDownloadsDocument(uri)) {\n\n                final String id = DocumentsContract.getDocumentId(uri);\n                final Uri contentUri = ContentUris.withAppendedId(\n                        Uri.parse(\"content://downloads/public_downloads\"), Long.valueOf(id));\n\n                return getDataColumn(context, contentUri, null, null);\n            }\n            // MediaProvider\n            else if (isMediaDocument(uri)) {\n                final String docId = DocumentsContract.getDocumentId(uri);\n                final String[] split = docId.split(\":\");\n                final String type = split[0];\n\n                Uri contentUri = null;\n                if (\"image\".equals(type)) {\n                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n                } else if (\"video\".equals(type)) {\n                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;\n                } else if (\"audio\".equals(type)) {\n                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;\n                }\n\n                final String selection = \"_id=?\";\n                final String[] selectionArgs = new String[]{\n                        split[1]\n                };\n\n                return getDataColumn(context, contentUri, selection, selectionArgs);\n            }\n        }\n        // MediaStore (and general)\n        else if (\"content\".equalsIgnoreCase(uri.getScheme())) {\n\n            // Return the remote address\n            if (isGooglePhotosUri(uri)) {\n                return uri.getLastPathSegment();\n            }\n\n            return getDataColumn(context, uri, null, null);\n        }\n        // File\n        else if (\"file\".equalsIgnoreCase(uri.getScheme())) {\n            return uri.getPath();\n        }\n\n        return null;\n    }\n\n    /**\n     * Copies one file into the other with the given paths.\n     * In the event that the paths are the same, trying to copy one file to the other\n     * will cause both files to become null.\n     * Simply skipping this step if the paths are identical.\n     */\n    public static void copyFile(@NonNull String pathFrom, @NonNull String pathTo) throws IOException {\n        if (pathFrom.equalsIgnoreCase(pathTo)) {\n            return;\n        }\n\n        FileChannel outputChannel = null;\n        FileChannel inputChannel = null;\n        try {\n            inputChannel = new FileInputStream(new File(pathFrom)).getChannel();\n            outputChannel = new FileOutputStream(new File(pathTo)).getChannel();\n            inputChannel.transferTo(0, inputChannel.size(), outputChannel);\n            inputChannel.close();\n        } finally {\n            if (inputChannel != null) inputChannel.close();\n            if (outputChannel != null) outputChannel.close();\n        }\n    }\n\n\n    public static boolean isGifForSuffix(String suffix) {\n        return suffix != null && suffix.startsWith(\".gif\") || suffix.startsWith(\".GIF\");\n    }\n\n    /**\n     * 是否是gif\n     *\n     * @param mimeType\n     * @return\n     */\n    public static boolean isGif(String mimeType) {\n        return mimeType != null && (mimeType.equals(\"image/gif\") || mimeType.equals(\"image/GIF\"));\n    }\n\n    /**\n     * 是否是网络图片\n     *\n     * @param path\n     * @return\n     */\n    public static boolean isHttp(String path) {\n        if (!TextUtils.isEmpty(path)) {\n            if (path.startsWith(\"http\")\n                    || path.startsWith(\"https\")) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n\n    /**\n     * Copies one file into the other with the given paths.\n     * In the event that the paths are the same, trying to copy one file to the other\n     * will cause both files to become null.\n     * Simply skipping this step if the paths are identical.\n     */\n    public static boolean copyFile(FileInputStream fileInputStream, String outFilePath) throws IOException {\n        if (fileInputStream == null) {\n            return false;\n        }\n        FileChannel inputChannel = null;\n        FileChannel outputChannel = null;\n        try {\n            inputChannel = fileInputStream.getChannel();\n            outputChannel = new FileOutputStream(new File(outFilePath)).getChannel();\n            inputChannel.transferTo(0, inputChannel.size(), outputChannel);\n            inputChannel.close();\n            return true;\n        } catch (Exception e) {\n            return false;\n        } finally {\n            if (inputChannel != null) inputChannel.close();\n            if (outputChannel != null) outputChannel.close();\n        }\n    }\n\n\n    public static String extSuffix(InputStream input) {\n        try {\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inJustDecodeBounds = true;\n            BitmapFactory.decodeStream(input, null, options);\n            return options.outMimeType.replace(\"image/\", \".\");\n        } catch (Exception e) {\n            return \".jpg\";\n        }\n    }\n\n    /**\n     * 根据时间戳创建文件名\n     *\n     * @param prefix 前缀名\n     * @return\n     */\n    public static String getCreateFileName(String prefix) {\n        long millis = System.currentTimeMillis();\n        return prefix + sf.format(millis);\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/ImageHeaderParser.java",
    "content": "/*\n * Copyright 2015 Google, Inc. All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without modification, are\n * permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this list of\n * conditions and the following disclaimer.\n *\n * 2. Redistributions in binary form must reproduce the above copyright notice, this list\n * of conditions and the following disclaimer in the documentation and/or other materials\n * provided with the distribution.\n *\n * THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED\n * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\n * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR\n * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n * The views and conclusions contained in the software and documentation are those of the\n * authors and should not be interpreted as representing official policies, either expressed\n * or implied, of Google, Inc.\n *\n * Adapted for the uCrop library.\n */\n\npackage com.matisse.ucrop.util;\n\nimport android.media.ExifInterface;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.charset.Charset;\n\n/**\n * A class for parsing the exif orientation from an image header.\n */\npublic class ImageHeaderParser {\n    private static final String TAG = \"ImageHeaderParser\";\n    /**\n     * A constant indicating we were unable to parse the orientation from the image either because\n     * no exif segment containing orientation data existed, or because of an I/O error attempting to\n     * read the exif segment.\n     */\n    public static final int UNKNOWN_ORIENTATION = -1;\n\n    private static final int EXIF_MAGIC_NUMBER = 0xFFD8;\n    // \"MM\".\n    private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D;\n    // \"II\".\n    private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949;\n    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = \"Exif\\0\\0\";\n    private static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES =\n            JPEG_EXIF_SEGMENT_PREAMBLE.getBytes(Charset.forName(\"UTF-8\"));\n    private static final int SEGMENT_SOS = 0xDA;\n    private static final int MARKER_EOI = 0xD9;\n    private static final int SEGMENT_START_ID = 0xFF;\n    private static final int EXIF_SEGMENT_TYPE = 0xE1;\n    private static final int ORIENTATION_TAG_TYPE = 0x0112;\n    private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};\n\n    private final Reader reader;\n\n    public ImageHeaderParser(InputStream is) {\n        reader = new StreamReader(is);\n    }\n\n    /**\n     * Parse the orientation from the image header. If it doesn't handle this image type (or this is\n     * not an image) it will return a default value rather than throwing an exception.\n     *\n     * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't\n     * contain an orientation\n     * @throws IOException\n     */\n    public int getOrientation() throws IOException {\n        final int magicNumber = reader.getUInt16();\n\n        if (!handles(magicNumber)) {\n            if (Log.isLoggable(TAG, Log.DEBUG)) {\n                Log.d(TAG, \"Parser doesn't handle magic number: \" + magicNumber);\n            }\n            return UNKNOWN_ORIENTATION;\n        } else {\n            int exifSegmentLength = moveToExifSegmentAndGetLength();\n            if (exifSegmentLength == -1) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Failed to parse exif segment length, or exif segment not found\");\n                }\n                return UNKNOWN_ORIENTATION;\n            }\n\n            byte[] exifData = new byte[exifSegmentLength];\n            return parseExifSegment(exifData, exifSegmentLength);\n        }\n    }\n\n    private int parseExifSegment(byte[] tempArray, int exifSegmentLength) throws IOException {\n        int read = reader.read(tempArray, exifSegmentLength);\n        if (read != exifSegmentLength) {\n            if (Log.isLoggable(TAG, Log.DEBUG)) {\n                Log.d(TAG, \"Unable to read exif segment data\"\n                        + \", length: \" + exifSegmentLength\n                        + \", actually read: \" + read);\n            }\n            return UNKNOWN_ORIENTATION;\n        }\n\n        boolean hasJpegExifPreamble = hasJpegExifPreamble(tempArray, exifSegmentLength);\n        if (hasJpegExifPreamble) {\n            return parseExifSegment(new RandomAccessReader(tempArray, exifSegmentLength));\n        } else {\n            if (Log.isLoggable(TAG, Log.DEBUG)) {\n                Log.d(TAG, \"Missing jpeg exif preamble\");\n            }\n            return UNKNOWN_ORIENTATION;\n        }\n    }\n\n    private boolean hasJpegExifPreamble(byte[] exifData, int exifSegmentLength) {\n        boolean result =\n                exifData != null && exifSegmentLength > JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length;\n        if (result) {\n            for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) {\n                if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) {\n                    result = false;\n                    break;\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Moves reader to the start of the exif segment and returns the length of the exif segment or\n     * {@code -1} if no exif segment is found.\n     */\n    private int moveToExifSegmentAndGetLength() throws IOException {\n        short segmentId, segmentType;\n        int segmentLength;\n        while (true) {\n            segmentId = reader.getUInt8();\n            if (segmentId != SEGMENT_START_ID) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Unknown segmentId=\" + segmentId);\n                }\n                return -1;\n            }\n\n            segmentType = reader.getUInt8();\n\n            if (segmentType == SEGMENT_SOS) {\n                return -1;\n            } else if (segmentType == MARKER_EOI) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Found MARKER_EOI in exif segment\");\n                }\n                return -1;\n            }\n\n            // Segment length includes bytes for segment length.\n            segmentLength = reader.getUInt16() - 2;\n\n            if (segmentType != EXIF_SEGMENT_TYPE) {\n                long skipped = reader.skip(segmentLength);\n                if (skipped != segmentLength) {\n                    if (Log.isLoggable(TAG, Log.DEBUG)) {\n                        Log.d(TAG, \"Unable to skip enough data\"\n                                + \", type: \" + segmentType\n                                + \", wanted to skip: \" + segmentLength\n                                + \", but actually skipped: \" + skipped);\n                    }\n                    return -1;\n                }\n            } else {\n                return segmentLength;\n            }\n        }\n    }\n\n    private static int parseExifSegment(RandomAccessReader segmentData) {\n        final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length();\n\n        short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize);\n        final ByteOrder byteOrder;\n        if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) {\n            byteOrder = ByteOrder.BIG_ENDIAN;\n        } else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) {\n            byteOrder = ByteOrder.LITTLE_ENDIAN;\n        } else {\n            if (Log.isLoggable(TAG, Log.DEBUG)) {\n                Log.d(TAG, \"Unknown endianness = \" + byteOrderIdentifier);\n            }\n            byteOrder = ByteOrder.BIG_ENDIAN;\n        }\n\n        segmentData.order(byteOrder);\n\n        int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize;\n        int tagCount = segmentData.getInt16(firstIfdOffset);\n\n        int tagOffset, tagType, formatCode, componentCount;\n        for (int i = 0; i < tagCount; i++) {\n            tagOffset = calcTagOffset(firstIfdOffset, i);\n            tagType = segmentData.getInt16(tagOffset);\n\n            // We only want orientation.\n            if (tagType != ORIENTATION_TAG_TYPE) {\n                continue;\n            }\n\n            formatCode = segmentData.getInt16(tagOffset + 2);\n\n            // 12 is max format code.\n            if (formatCode < 1 || formatCode > 12) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Got invalid format code = \" + formatCode);\n                }\n                continue;\n            }\n\n            componentCount = segmentData.getInt32(tagOffset + 4);\n\n            if (componentCount < 0) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Negative tiff component count\");\n                }\n                continue;\n            }\n\n            if (Log.isLoggable(TAG, Log.DEBUG)) {\n                Log.d(TAG, \"Got tagIndex=\" + i + \" tagType=\" + tagType + \" formatCode=\" + formatCode\n                        + \" componentCount=\" + componentCount);\n            }\n\n            final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode];\n\n            if (byteCount > 4) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Got byte count > 4, not orientation, continuing, formatCode=\" + formatCode);\n                }\n                continue;\n            }\n\n            final int tagValueOffset = tagOffset + 8;\n\n            if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Illegal tagValueOffset=\" + tagValueOffset + \" tagType=\" + tagType);\n                }\n                continue;\n            }\n\n            if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) {\n                if (Log.isLoggable(TAG, Log.DEBUG)) {\n                    Log.d(TAG, \"Illegal number of bytes for TI tag data tagType=\" + tagType);\n                }\n                continue;\n            }\n\n            //assume componentCount == 1 && fmtCode == 3\n            return segmentData.getInt16(tagValueOffset);\n        }\n\n        return -1;\n    }\n\n    private static int calcTagOffset(int ifdOffset, int tagIndex) {\n        return ifdOffset + 2 + 12 * tagIndex;\n    }\n\n    private static boolean handles(int imageMagicNumber) {\n        return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER\n                || imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER\n                || imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER;\n    }\n\n    private static class RandomAccessReader {\n        private final ByteBuffer data;\n\n        public RandomAccessReader(byte[] data, int length) {\n            this.data = (ByteBuffer) ByteBuffer.wrap(data)\n                    .order(ByteOrder.BIG_ENDIAN)\n                    .limit(length);\n        }\n\n        public void order(ByteOrder byteOrder) {\n            this.data.order(byteOrder);\n        }\n\n        public int length() {\n            return data.remaining();\n        }\n\n        public int getInt32(int offset) {\n            return data.getInt(offset);\n        }\n\n        public short getInt16(int offset) {\n            return data.getShort(offset);\n        }\n    }\n\n    private interface Reader {\n        int getUInt16() throws IOException;\n\n        short getUInt8() throws IOException;\n\n        long skip(long total) throws IOException;\n\n        int read(byte[] buffer, int byteCount) throws IOException;\n    }\n\n    private static class StreamReader implements Reader {\n        private final InputStream is;\n\n        // Motorola / big endian byte order.\n        public StreamReader(InputStream is) {\n            this.is = is;\n        }\n\n        @Override\n        public int getUInt16() throws IOException {\n            return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);\n        }\n\n        @Override\n        public short getUInt8() throws IOException {\n            return (short) (is.read() & 0xFF);\n        }\n\n        @Override\n        public long skip(long total) throws IOException {\n            if (total < 0) {\n                return 0;\n            }\n\n            long toSkip = total;\n            while (toSkip > 0) {\n                long skipped = is.skip(toSkip);\n                if (skipped > 0) {\n                    toSkip -= skipped;\n                } else {\n                    // Skip has no specific contract as to what happens when you reach the end of\n                    // the stream. To differentiate between temporarily not having more data and\n                    // having finished the stream, we read a single byte when we fail to skip any\n                    // amount of data.\n                    int testEofByte = is.read();\n                    if (testEofByte == -1) {\n                        break;\n                    } else {\n                        toSkip--;\n                    }\n                }\n            }\n            return total - toSkip;\n        }\n\n        @Override\n        public int read(byte[] buffer, int byteCount) throws IOException {\n            int toRead = byteCount;\n            int read;\n            while (toRead > 0 && ((read = is.read(buffer, byteCount - toRead, toRead)) != -1)) {\n                toRead -= read;\n            }\n            return byteCount - toRead;\n        }\n    }\n\n    public static void copyExif(ExifInterface originalExif, int width, int height, String imageOutputPath) {\n        String[] attributes = new String[]{\n                ExifInterface.TAG_APERTURE,\n                ExifInterface.TAG_DATETIME,\n                ExifInterface.TAG_DATETIME_DIGITIZED,\n                ExifInterface.TAG_EXPOSURE_TIME,\n                ExifInterface.TAG_FLASH,\n                ExifInterface.TAG_FOCAL_LENGTH,\n                ExifInterface.TAG_GPS_ALTITUDE,\n                ExifInterface.TAG_GPS_ALTITUDE_REF,\n                ExifInterface.TAG_GPS_DATESTAMP,\n                ExifInterface.TAG_GPS_LATITUDE,\n                ExifInterface.TAG_GPS_LATITUDE_REF,\n                ExifInterface.TAG_GPS_LONGITUDE,\n                ExifInterface.TAG_GPS_LONGITUDE_REF,\n                ExifInterface.TAG_GPS_PROCESSING_METHOD,\n                ExifInterface.TAG_GPS_TIMESTAMP,\n                ExifInterface.TAG_ISO,\n                ExifInterface.TAG_MAKE,\n                ExifInterface.TAG_MODEL,\n                ExifInterface.TAG_SUBSEC_TIME,\n                ExifInterface.TAG_SUBSEC_TIME_DIG,\n                ExifInterface.TAG_SUBSEC_TIME_ORIG,\n                ExifInterface.TAG_WHITE_BALANCE\n        };\n\n        try {\n            ExifInterface newExif = new ExifInterface(imageOutputPath);\n            String value;\n            if (originalExif != null) {\n                for (String attribute : attributes) {\n                    value = originalExif.getAttribute(attribute);\n                    if (!TextUtils.isEmpty(value)) {\n                        newExif.setAttribute(attribute, value);\n                    }\n                }\n            }\n            newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width));\n            newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height));\n            newExif.setAttribute(ExifInterface.TAG_ORIENTATION, \"0\");\n\n            newExif.saveAttributes();\n\n        } catch (IOException e) {\n            Log.d(TAG, e.getMessage());\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/RectUtils.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.graphics.RectF;\n\npublic class RectUtils {\n\n    /**\n     * Gets a float array of the 2D coordinates representing a rectangles\n     * corners.\n     * The order of the corners in the float array is:\n     * 0------->1\n     * ^        |\n     * |        |\n     * |        v\n     * 3<-------2\n     *\n     * @param r the rectangle to get the corners of\n     * @return the float array of corners (8 floats)\n     */\n    public static float[] getCornersFromRect(RectF r) {\n        return new float[]{\n                r.left, r.top,\n                r.right, r.top,\n                r.right, r.bottom,\n                r.left, r.bottom\n        };\n    }\n\n    /**\n     * Gets a float array of two lengths representing a rectangles width and height\n     * The order of the corners in the input float array is:\n     * 0------->1\n     * ^        |\n     * |        |\n     * |        v\n     * 3<-------2\n     *\n     * @param corners the float array of corners (8 floats)\n     * @return the float array of width and height (2 floats)\n     */\n    public static float[] getRectSidesFromCorners(float[] corners) {\n        return new float[]{(float) Math.sqrt(Math.pow(corners[0] - corners[2], 2) + Math.pow(corners[1] - corners[3], 2)),\n                (float) Math.sqrt(Math.pow(corners[2] - corners[4], 2) + Math.pow(corners[3] - corners[5], 2))};\n    }\n\n    public static float[] getCenterFromRect(RectF r) {\n        return new float[]{r.centerX(), r.centerY()};\n    }\n\n    /**\n     * Takes an array of 2D coordinates representing corners and returns the\n     * smallest rectangle containing those coordinates.\n     *\n     * @param array array of 2D coordinates\n     * @return smallest rectangle containing coordinates\n     */\n    public static RectF trapToRect(float[] array) {\n        RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,\n                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);\n        for (int i = 1; i < array.length; i += 2) {\n            float x = Math.round(array[i - 1] * 10) / 10.f;\n            float y = Math.round(array[i] * 10) / 10.f;\n            r.left = (x < r.left) ? x : r.left;\n            r.top = (y < r.top) ? y : r.top;\n            r.right = (x > r.right) ? x : r.right;\n            r.bottom = (y > r.bottom) ? y : r.bottom;\n        }\n        r.sort();\n        return r;\n    }\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/RotationGestureDetector.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.view.MotionEvent;\n\nimport androidx.annotation.NonNull;\n\npublic class RotationGestureDetector {\n\n    private static final int INVALID_POINTER_INDEX = -1;\n\n    private float fX, fY, sX, sY;\n\n    private int mPointerIndex1, mPointerIndex2;\n    private float mAngle;\n    private boolean mIsFirstTouch;\n\n    private OnRotationGestureListener mListener;\n\n    public RotationGestureDetector(OnRotationGestureListener listener) {\n        mListener = listener;\n        mPointerIndex1 = INVALID_POINTER_INDEX;\n        mPointerIndex2 = INVALID_POINTER_INDEX;\n    }\n\n    public float getAngle() {\n        return mAngle;\n    }\n\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        switch (event.getActionMasked()) {\n            case MotionEvent.ACTION_DOWN:\n                sX = event.getX();\n                sY = event.getY();\n                mPointerIndex1 = event.findPointerIndex(event.getPointerId(0));\n                mAngle = 0;\n                mIsFirstTouch = true;\n                break;\n            case MotionEvent.ACTION_POINTER_DOWN:\n                fX = event.getX();\n                fY = event.getY();\n                mPointerIndex2 = event.findPointerIndex(event.getPointerId(event.getActionIndex()));\n                mAngle = 0;\n                mIsFirstTouch = true;\n                break;\n            case MotionEvent.ACTION_MOVE:\n                if (mPointerIndex1 != INVALID_POINTER_INDEX && mPointerIndex2 != INVALID_POINTER_INDEX && event.getPointerCount() > mPointerIndex2) {\n                    float nfX, nfY, nsX, nsY;\n\n                    nsX = event.getX(mPointerIndex1);\n                    nsY = event.getY(mPointerIndex1);\n                    nfX = event.getX(mPointerIndex2);\n                    nfY = event.getY(mPointerIndex2);\n\n                    if (mIsFirstTouch) {\n                        mAngle = 0;\n                        mIsFirstTouch = false;\n                    } else {\n                        calculateAngleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);\n                    }\n\n                    if (mListener != null) {\n                        mListener.onRotation(this);\n                    }\n                    fX = nfX;\n                    fY = nfY;\n                    sX = nsX;\n                    sY = nsY;\n                }\n                break;\n            case MotionEvent.ACTION_UP:\n                mPointerIndex1 = INVALID_POINTER_INDEX;\n                break;\n            case MotionEvent.ACTION_POINTER_UP:\n                mPointerIndex2 = INVALID_POINTER_INDEX;\n                break;\n        }\n        return true;\n    }\n\n    private float calculateAngleBetweenLines(float fx1, float fy1, float fx2, float fy2,\n                                             float sx1, float sy1, float sx2, float sy2) {\n        return calculateAngleDelta(\n                (float) Math.toDegrees((float) Math.atan2((fy1 - fy2), (fx1 - fx2))),\n                (float) Math.toDegrees((float) Math.atan2((sy1 - sy2), (sx1 - sx2))));\n    }\n\n    private float calculateAngleDelta(float angleFrom, float angleTo) {\n        mAngle = angleTo % 360.0f - angleFrom % 360.0f;\n\n        if (mAngle < -180.0f) {\n            mAngle += 360.0f;\n        } else if (mAngle > 180.0f) {\n            mAngle -= 360.0f;\n        }\n\n        return mAngle;\n    }\n\n    public static class SimpleOnRotationGestureListener implements OnRotationGestureListener {\n\n        @Override\n        public boolean onRotation(RotationGestureDetector rotationDetector) {\n            return false;\n        }\n    }\n\n    public interface OnRotationGestureListener {\n\n        boolean onRotation(RotationGestureDetector rotationDetector);\n    }\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/SelectedStateListDrawable.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.graphics.PorterDuff;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.StateListDrawable;\n\n/**\n * Hack class to properly support state drawable back to Android 1.6\n */\npublic class SelectedStateListDrawable extends StateListDrawable {\n\n    private int mSelectionColor;\n\n    public SelectedStateListDrawable(Drawable drawable, int selectionColor) {\n        super();\n        this.mSelectionColor = selectionColor;\n        addState(new int[]{android.R.attr.state_selected}, drawable);\n        addState(new int[]{}, drawable);\n    }\n\n    @Override\n    protected boolean onStateChange(int[] states) {\n        boolean isStatePressedInArray = false;\n        for (int state : states) {\n            if (state == android.R.attr.state_selected) {\n                isStatePressedInArray = true;\n            }\n        }\n        if (isStatePressedInArray) {\n            super.setColorFilter(mSelectionColor, PorterDuff.Mode.SRC_ATOP);\n        } else {\n            super.clearColorFilter();\n        }\n        return super.onStateChange(states);\n    }\n\n    @Override\n    public boolean isStateful() {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/util/VersionUtils.java",
    "content": "package com.matisse.ucrop.util;\n\nimport android.os.Build;\n\n/**\n * Created by Oleksii Shliama [https://github.com/shliama] on 9/8/16.\n */\npublic class VersionUtils {\n\n    private VersionUtils() {\n\n    }\n\n    public static boolean isAndroidQ() {\n        return Build.VERSION.SDK_INT >= 29;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/CropImageView.java",
    "content": "package com.matisse.ucrop.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.matisse.R;\nimport com.matisse.ucrop.callback.BitmapCropCallback;\nimport com.matisse.ucrop.callback.CropBoundsChangeListener;\nimport com.matisse.ucrop.model.CropParameters;\nimport com.matisse.ucrop.model.ImageState;\nimport com.matisse.ucrop.task.BitmapCropTask;\nimport com.matisse.ucrop.util.CubicEasing;\nimport com.matisse.ucrop.util.RectUtils;\n\nimport java.lang.ref.WeakReference;\nimport java.util.Arrays;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n * <p/>\n * This class adds crop feature, methods to draw crop guidelines, and keep image in correct state.\n * Also it extends parent class methods to add checks for scale; animating zoom in/out.\n */\npublic class CropImageView extends TransformImageView {\n\n    public static final int DEFAULT_MAX_BITMAP_SIZE = 0;\n    public static final int DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = 500;\n    public static final float DEFAULT_MAX_SCALE_MULTIPLIER = 10.0f;\n    public static final float SOURCE_IMAGE_ASPECT_RATIO = 0f;\n    public static final float DEFAULT_ASPECT_RATIO = SOURCE_IMAGE_ASPECT_RATIO;\n\n    private final RectF mCropRect = new RectF();\n\n    private final Matrix mTempMatrix = new Matrix();\n\n    private float mTargetAspectRatio;\n    private float mMaxScaleMultiplier = DEFAULT_MAX_SCALE_MULTIPLIER;\n\n    private CropBoundsChangeListener mCropBoundsChangeListener;\n\n    private Runnable mWrapCropBoundsRunnable, mZoomImageToPositionRunnable = null;\n\n    private float mMaxScale, mMinScale;\n    private int mMaxResultImageSizeX = 0, mMaxResultImageSizeY = 0;\n    private long mImageToWrapCropBoundsAnimDuration = DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION;\n\n    public CropImageView(Context context) {\n        this(context, null);\n    }\n\n    public CropImageView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public CropImageView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    /**\n     * Cancels all current animations and sets image to fill crop area (without animation).\n     * Then creates and executes {@link BitmapCropTask} with proper parameters.\n     */\n    public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,\n                                 boolean mIsCircleCrop, @Nullable BitmapCropCallback cropCallback) {\n        cancelAllAnimations();\n        setImageToWrapCropBounds(false);\n\n        final ImageState imageState = new ImageState(\n                mCropRect, RectUtils.trapToRect(mCurrentImageCorners),\n                getCurrentScale(), getCurrentAngle());\n\n        final CropParameters cropParameters = new CropParameters(\n                mMaxResultImageSizeX, mMaxResultImageSizeY,\n                compressFormat, compressQuality,\n                getImageInputUri(), getImageOutputPath(), getExifInfo());\n\n        new BitmapCropTask(getContext(), getViewBitmap(), imageState, cropParameters\n                , mIsCircleCrop, cropCallback).execute();\n    }\n\n    /**\n     * @return - maximum scale value for current image and crop ratio\n     */\n    public float getMaxScale() {\n        return mMaxScale;\n    }\n\n    /**\n     * @return - minimum scale value for current image and crop ratio\n     */\n    public float getMinScale() {\n        return mMinScale;\n    }\n\n    /**\n     * @return - aspect ratio for crop bounds\n     */\n    public float getTargetAspectRatio() {\n        return mTargetAspectRatio;\n    }\n\n    /**\n     * Updates current crop rectangle with given. Also recalculates image properties and position\n     * to fit new crop rectangle.\n     *\n     * @param cropRect - new crop rectangle\n     */\n    public void setCropRect(RectF cropRect) {\n        mTargetAspectRatio = cropRect.width() / cropRect.height();\n        mCropRect.set(cropRect.left - getPaddingLeft(), cropRect.top - getPaddingTop(),\n                cropRect.right - getPaddingRight(), cropRect.bottom - getPaddingBottom());\n        calculateImageScaleBounds();\n        setImageToWrapCropBounds();\n    }\n\n    /**\n     * This method sets aspect ratio for crop bounds.\n     * If {@link #SOURCE_IMAGE_ASPECT_RATIO} value is passed - aspect ratio is calculated\n     * based on current image width and height.\n     *\n     * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)\n     */\n    public void setTargetAspectRatio(float targetAspectRatio) {\n        final Drawable drawable = getDrawable();\n        if (drawable == null) {\n            mTargetAspectRatio = targetAspectRatio;\n            return;\n        }\n\n        if (targetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {\n            mTargetAspectRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight();\n        } else {\n            mTargetAspectRatio = targetAspectRatio;\n        }\n\n        if (mCropBoundsChangeListener != null) {\n            mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);\n        }\n    }\n\n    @Nullable\n    public CropBoundsChangeListener getCropBoundsChangeListener() {\n        return mCropBoundsChangeListener;\n    }\n\n    public void setCropBoundsChangeListener(@Nullable CropBoundsChangeListener cropBoundsChangeListener) {\n        mCropBoundsChangeListener = cropBoundsChangeListener;\n    }\n\n    /**\n     * This method sets maximum width for resulting cropped image\n     *\n     * @param maxResultImageSizeX - size in pixels\n     */\n    public void setMaxResultImageSizeX(@IntRange(from = 10) int maxResultImageSizeX) {\n        mMaxResultImageSizeX = maxResultImageSizeX;\n    }\n\n    /**\n     * This method sets maximum width for resulting cropped image\n     *\n     * @param maxResultImageSizeY - size in pixels\n     */\n    public void setMaxResultImageSizeY(@IntRange(from = 10) int maxResultImageSizeY) {\n        mMaxResultImageSizeY = maxResultImageSizeY;\n    }\n\n    /**\n     * This method sets animation duration for image to wrap the crop bounds\n     *\n     * @param imageToWrapCropBoundsAnimDuration - duration in milliseconds\n     */\n    public void setImageToWrapCropBoundsAnimDuration(@IntRange(from = 100) long imageToWrapCropBoundsAnimDuration) {\n        if (imageToWrapCropBoundsAnimDuration > 0) {\n            mImageToWrapCropBoundsAnimDuration = imageToWrapCropBoundsAnimDuration;\n        } else {\n            throw new IllegalArgumentException(\"Animation duration cannot be negative value.\");\n        }\n    }\n\n    /**\n     * This method sets multiplier that is used to calculate max image scale from min image scale.\n     *\n     * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale\n     */\n    public void setMaxScaleMultiplier(float maxScaleMultiplier) {\n        mMaxScaleMultiplier = maxScaleMultiplier;\n    }\n\n    /**\n     * This method scales image down for given value related to image center.\n     */\n    public void zoomOutImage(float deltaScale) {\n        zoomOutImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());\n    }\n\n    /**\n     * This method scales image down for given value related given coords (x, y).\n     */\n    public void zoomOutImage(float scale, float centerX, float centerY) {\n        if (scale >= getMinScale()) {\n            postScale(scale / getCurrentScale(), centerX, centerY);\n        }\n    }\n\n    /**\n     * This method scales image up for given value related to image center.\n     */\n    public void zoomInImage(float deltaScale) {\n        zoomInImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());\n    }\n\n    /**\n     * This method scales image up for given value related to given coords (x, y).\n     */\n    public void zoomInImage(float scale, float centerX, float centerY) {\n        if (scale <= getMaxScale()) {\n            postScale(scale / getCurrentScale(), centerX, centerY);\n        }\n    }\n\n    /**\n     * This method changes image scale for given value related to point (px, py) but only if\n     * resulting scale is in min/max bounds.\n     *\n     * @param deltaScale - scale value\n     * @param px         - scale center X\n     * @param py         - scale center Y\n     */\n    public void postScale(float deltaScale, float px, float py) {\n        if (deltaScale > 1 && getCurrentScale() * deltaScale <= getMaxScale()) {\n            super.postScale(deltaScale, px, py);\n        } else if (deltaScale < 1 && getCurrentScale() * deltaScale >= getMinScale()) {\n            super.postScale(deltaScale, px, py);\n        }\n    }\n\n    /**\n     * This method rotates image for given angle related to the image center.\n     *\n     * @param deltaAngle - angle to rotate\n     */\n    public void postRotate(float deltaAngle) {\n        postRotate(deltaAngle, mCropRect.centerX(), mCropRect.centerY());\n    }\n\n    /**\n     * This method cancels all current Runnable objects that represent animations.\n     */\n    public void cancelAllAnimations() {\n        removeCallbacks(mWrapCropBoundsRunnable);\n        removeCallbacks(mZoomImageToPositionRunnable);\n    }\n\n    public void setImageToWrapCropBounds() {\n        setImageToWrapCropBounds(true);\n    }\n\n    /**\n     * If image doesn't fill the crop bounds it must be translated and scaled properly to fill those.\n     * <p/>\n     * Therefore this method calculates delta X, Y and scale values and passes them to the\n     * {@link WrapCropBoundsRunnable} which animates image.\n     * Scale value must be calculated only if image won't fill the crop bounds after it's translated to the\n     * crop bounds rectangle center. Using temporary variables this method checks this case.\n     */\n    public void setImageToWrapCropBounds(boolean animate) {\n        if (mBitmapLaidOut && !isImageWrapCropBounds()) {\n\n            float currentX = mCurrentImageCenter[0];\n            float currentY = mCurrentImageCenter[1];\n            float currentScale = getCurrentScale();\n\n            float deltaX = mCropRect.centerX() - currentX;\n            float deltaY = mCropRect.centerY() - currentY;\n            float deltaScale = 0;\n\n            mTempMatrix.reset();\n            mTempMatrix.setTranslate(deltaX, deltaY);\n\n            final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);\n            mTempMatrix.mapPoints(tempCurrentImageCorners);\n\n            boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);\n\n            if (willImageWrapCropBoundsAfterTranslate) {\n                final float[] imageIndents = calculateImageIndents();\n                deltaX = -(imageIndents[0] + imageIndents[2]);\n                deltaY = -(imageIndents[1] + imageIndents[3]);\n            } else {\n                RectF tempCropRect = new RectF(mCropRect);\n                mTempMatrix.reset();\n                mTempMatrix.setRotate(getCurrentAngle());\n                mTempMatrix.mapRect(tempCropRect);\n\n                final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);\n\n                deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],\n                        tempCropRect.height() / currentImageSides[1]);\n                deltaScale = deltaScale * currentScale - currentScale;\n            }\n\n            if (animate) {\n                post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(\n                        CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,\n                        currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));\n            } else {\n                postTranslate(deltaX, deltaY);\n                if (!willImageWrapCropBoundsAfterTranslate) {\n                    zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());\n                }\n            }\n        }\n    }\n\n    /**\n     * First, un-rotate image and crop rectangles (make image rectangle axis-aligned).\n     * Second, calculate deltas between those rectangles sides.\n     * Third, depending on delta (its sign) put them or zero inside an array.\n     * Fourth, using Matrix, rotate back those points (indents).\n     *\n     * @return - the float array of image indents (4 floats) - in this order [left, top, right, bottom]\n     */\n    private float[] calculateImageIndents() {\n        mTempMatrix.reset();\n        mTempMatrix.setRotate(-getCurrentAngle());\n\n        float[] unrotatedImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);\n        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);\n\n        mTempMatrix.mapPoints(unrotatedImageCorners);\n        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);\n\n        RectF unrotatedImageRect = RectUtils.trapToRect(unrotatedImageCorners);\n        RectF unrotatedCropRect = RectUtils.trapToRect(unrotatedCropBoundsCorners);\n\n        float deltaLeft = unrotatedImageRect.left - unrotatedCropRect.left;\n        float deltaTop = unrotatedImageRect.top - unrotatedCropRect.top;\n        float deltaRight = unrotatedImageRect.right - unrotatedCropRect.right;\n        float deltaBottom = unrotatedImageRect.bottom - unrotatedCropRect.bottom;\n\n        float indents[] = new float[4];\n        indents[0] = (deltaLeft > 0) ? deltaLeft : 0;\n        indents[1] = (deltaTop > 0) ? deltaTop : 0;\n        indents[2] = (deltaRight < 0) ? deltaRight : 0;\n        indents[3] = (deltaBottom < 0) ? deltaBottom : 0;\n\n        mTempMatrix.reset();\n        mTempMatrix.setRotate(getCurrentAngle());\n        mTempMatrix.mapPoints(indents);\n\n        return indents;\n    }\n\n    /**\n     * When image is laid out it must be centered properly to fit current crop bounds.\n     */\n    @Override\n    protected void onImageLaidOut() {\n        super.onImageLaidOut();\n        final Drawable drawable = getDrawable();\n        if (drawable == null) {\n            return;\n        }\n\n        float drawableWidth = drawable.getIntrinsicWidth();\n        float drawableHeight = drawable.getIntrinsicHeight();\n\n        if (mTargetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {\n            mTargetAspectRatio = drawableWidth / drawableHeight;\n        }\n\n        int height = (int) (mThisWidth / mTargetAspectRatio);\n        if (height > mThisHeight) {\n            int width = (int) (mThisHeight * mTargetAspectRatio);\n            int halfDiff = (mThisWidth - width) / 2;\n            mCropRect.set(halfDiff, 0, width + halfDiff, mThisHeight);\n        } else {\n            int halfDiff = (mThisHeight - height) / 2;\n            mCropRect.set(0, halfDiff, mThisWidth, height + halfDiff);\n        }\n\n        calculateImageScaleBounds(drawableWidth, drawableHeight);\n        setupInitialImagePosition(drawableWidth, drawableHeight);\n\n        if (mCropBoundsChangeListener != null) {\n            mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);\n        }\n        if (mTransformImageListener != null) {\n            mTransformImageListener.onScale(getCurrentScale());\n            mTransformImageListener.onRotate(getCurrentAngle());\n        }\n    }\n\n    /**\n     * This method checks whether current image fills the crop bounds.\n     */\n    protected boolean isImageWrapCropBounds() {\n        return isImageWrapCropBounds(mCurrentImageCorners);\n    }\n\n    /**\n     * This methods checks whether a rectangle that is represented as 4 corner points (8 floats)\n     * fills the crop bounds rectangle.\n     *\n     * @param imageCorners - corners of a rectangle\n     * @return - true if it wraps crop bounds, false - otherwise\n     */\n    protected boolean isImageWrapCropBounds(float[] imageCorners) {\n        mTempMatrix.reset();\n        mTempMatrix.setRotate(-getCurrentAngle());\n\n        float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);\n        mTempMatrix.mapPoints(unrotatedImageCorners);\n\n        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);\n        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);\n\n        return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));\n    }\n\n    /**\n     * This method changes image scale (animating zoom for given duration), related to given center (x,y).\n     *\n     * @param scale      - target scale\n     * @param centerX    - scale center X\n     * @param centerY    - scale center Y\n     * @param durationMs - zoom animation duration\n     */\n    protected void zoomImageToPosition(float scale, float centerX, float centerY, long durationMs) {\n        if (scale > getMaxScale()) {\n            scale = getMaxScale();\n        }\n\n        final float oldScale = getCurrentScale();\n        final float deltaScale = scale - oldScale;\n\n        post(mZoomImageToPositionRunnable = new ZoomImageToPosition(CropImageView.this,\n                durationMs, oldScale, deltaScale, centerX, centerY));\n    }\n\n    private void calculateImageScaleBounds() {\n        final Drawable drawable = getDrawable();\n        if (drawable == null) {\n            return;\n        }\n        calculateImageScaleBounds(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());\n    }\n\n    /**\n     * This method calculates image minimum and maximum scale values for current {@link #mCropRect}.\n     *\n     * @param drawableWidth  - image width\n     * @param drawableHeight - image height\n     */\n    private void calculateImageScaleBounds(float drawableWidth, float drawableHeight) {\n        float widthScale = Math.min(mCropRect.width() / drawableWidth, mCropRect.width() / drawableHeight);\n        float heightScale = Math.min(mCropRect.height() / drawableHeight, mCropRect.height() / drawableWidth);\n\n        mMinScale = Math.min(widthScale, heightScale);\n        mMaxScale = mMinScale * mMaxScaleMultiplier;\n    }\n\n    /**\n     * This method calculates initial image position so it is positioned properly.\n     * Then it sets those values to the current image matrix.\n     *\n     * @param drawableWidth  - image width\n     * @param drawableHeight - image height\n     */\n    private void setupInitialImagePosition(float drawableWidth, float drawableHeight) {\n        float cropRectWidth = mCropRect.width();\n        float cropRectHeight = mCropRect.height();\n\n        float widthScale = mCropRect.width() / drawableWidth;\n        float heightScale = mCropRect.height() / drawableHeight;\n\n        float initialMinScale = Math.max(widthScale, heightScale);\n\n        float tw = (cropRectWidth - drawableWidth * initialMinScale) / 2.0f + mCropRect.left;\n        float th = (cropRectHeight - drawableHeight * initialMinScale) / 2.0f + mCropRect.top;\n\n        mCurrentImageMatrix.reset();\n        mCurrentImageMatrix.postScale(initialMinScale, initialMinScale);\n        mCurrentImageMatrix.postTranslate(tw, th);\n        setImageMatrix(mCurrentImageMatrix);\n    }\n\n    /**\n     * This method extracts all needed values from the styled attributes.\n     * Those are used to configure the view.\n     */\n    @SuppressWarnings(\"deprecation\")\n    protected void processStyledAttributes(@NonNull TypedArray a) {\n        float targetAspectRatioX = Math.abs(a.getFloat(R.styleable.ucrop_UCropView_ucrop_aspect_ratio_x, DEFAULT_ASPECT_RATIO));\n        float targetAspectRatioY = Math.abs(a.getFloat(R.styleable.ucrop_UCropView_ucrop_aspect_ratio_y, DEFAULT_ASPECT_RATIO));\n\n        if (targetAspectRatioX == SOURCE_IMAGE_ASPECT_RATIO || targetAspectRatioY == SOURCE_IMAGE_ASPECT_RATIO) {\n            mTargetAspectRatio = SOURCE_IMAGE_ASPECT_RATIO;\n        } else {\n            mTargetAspectRatio = targetAspectRatioX / targetAspectRatioY;\n        }\n    }\n\n    /**\n     * This Runnable is used to animate an image so it fills the crop bounds entirely.\n     * Given values are interpolated during the animation time.\n     * Runnable can be terminated either vie {@link #cancelAllAnimations()} method\n     * or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.\n     */\n    private static class WrapCropBoundsRunnable implements Runnable {\n\n        private final WeakReference<CropImageView> mCropImageView;\n\n        private final long mDurationMs, mStartTime;\n        private final float mOldX, mOldY;\n        private final float mCenterDiffX, mCenterDiffY;\n        private final float mOldScale;\n        private final float mDeltaScale;\n        private final boolean mWillBeImageInBoundsAfterTranslate;\n\n        public WrapCropBoundsRunnable(CropImageView cropImageView,\n                                      long durationMs,\n                                      float oldX, float oldY,\n                                      float centerDiffX, float centerDiffY,\n                                      float oldScale, float deltaScale,\n                                      boolean willBeImageInBoundsAfterTranslate) {\n\n            mCropImageView = new WeakReference<>(cropImageView);\n\n            mDurationMs = durationMs;\n            mStartTime = System.currentTimeMillis();\n            mOldX = oldX;\n            mOldY = oldY;\n            mCenterDiffX = centerDiffX;\n            mCenterDiffY = centerDiffY;\n            mOldScale = oldScale;\n            mDeltaScale = deltaScale;\n            mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;\n        }\n\n        @Override\n        public void run() {\n            CropImageView cropImageView = mCropImageView.get();\n            if (cropImageView == null) {\n                return;\n            }\n\n            long now = System.currentTimeMillis();\n            float currentMs = Math.min(mDurationMs, now - mStartTime);\n\n            float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);\n            float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);\n            float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);\n\n            if (currentMs < mDurationMs) {\n                cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX), newY - (cropImageView.mCurrentImageCenter[1] - mOldY));\n                if (!mWillBeImageInBoundsAfterTranslate) {\n                    cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(), cropImageView.mCropRect.centerY());\n                }\n                if (!cropImageView.isImageWrapCropBounds()) {\n                    cropImageView.post(this);\n                }\n            }\n        }\n    }\n\n    /**\n     * This Runnable is used to animate an image zoom.\n     * Given values are interpolated during the animation time.\n     * Runnable can be terminated either vie {@link #cancelAllAnimations()} method\n     * or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.\n     */\n    private static class ZoomImageToPosition implements Runnable {\n\n        private final WeakReference<CropImageView> mCropImageView;\n\n        private final long mDurationMs, mStartTime;\n        private final float mOldScale;\n        private final float mDeltaScale;\n        private final float mDestX;\n        private final float mDestY;\n\n        public ZoomImageToPosition(CropImageView cropImageView,\n                                   long durationMs,\n                                   float oldScale, float deltaScale,\n                                   float destX, float destY) {\n\n            mCropImageView = new WeakReference<>(cropImageView);\n\n            mStartTime = System.currentTimeMillis();\n            mDurationMs = durationMs;\n            mOldScale = oldScale;\n            mDeltaScale = deltaScale;\n            mDestX = destX;\n            mDestY = destY;\n        }\n\n        @Override\n        public void run() {\n            CropImageView cropImageView = mCropImageView.get();\n            if (cropImageView == null) {\n                return;\n            }\n\n            long now = System.currentTimeMillis();\n            float currentMs = Math.min(mDurationMs, now - mStartTime);\n            float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);\n\n            if (currentMs < mDurationMs) {\n                cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);\n                cropImageView.post(this);\n            } else {\n                cropImageView.setImageToWrapCropBounds();\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/GestureCropImageView.java",
    "content": "package com.matisse.ucrop.view;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.ScaleGestureDetector;\n\nimport com.matisse.ucrop.util.RotationGestureDetector;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\npublic class GestureCropImageView extends CropImageView {\n\n    private static final int DOUBLE_TAP_ZOOM_DURATION = 200;\n\n    private ScaleGestureDetector mScaleDetector;\n    private RotationGestureDetector mRotateDetector;\n    private GestureDetector mGestureDetector;\n\n    private float mMidPntX, mMidPntY;\n\n    private boolean mIsRotateEnabled = true, mIsScaleEnabled = true;\n    private int mDoubleTapScaleSteps = 5;\n\n    public GestureCropImageView(Context context) {\n        super(context);\n    }\n\n    public GestureCropImageView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public GestureCropImageView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    public void setScaleEnabled(boolean scaleEnabled) {\n        mIsScaleEnabled = scaleEnabled;\n    }\n\n    public boolean isScaleEnabled() {\n        return mIsScaleEnabled;\n    }\n\n    public void setRotateEnabled(boolean rotateEnabled) {\n        mIsRotateEnabled = rotateEnabled;\n    }\n\n    public boolean isRotateEnabled() {\n        return mIsRotateEnabled;\n    }\n\n    public void setDoubleTapScaleSteps(int doubleTapScaleSteps) {\n        mDoubleTapScaleSteps = doubleTapScaleSteps;\n    }\n\n    public int getDoubleTapScaleSteps() {\n        return mDoubleTapScaleSteps;\n    }\n\n    /**\n     * If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled.\n     * If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected.\n     * If there are more than 2 fingers - update focal point coordinates.\n     * Pass the event to the gesture detectors if those are enabled.\n     */\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {\n            cancelAllAnimations();\n        }\n\n        if (event.getPointerCount() > 1) {\n            mMidPntX = (event.getX(0) + event.getX(1)) / 2;\n            mMidPntY = (event.getY(0) + event.getY(1)) / 2;\n        }\n\n        mGestureDetector.onTouchEvent(event);\n\n        if (mIsScaleEnabled) {\n            mScaleDetector.onTouchEvent(event);\n        }\n\n        if (mIsRotateEnabled) {\n            mRotateDetector.onTouchEvent(event);\n        }\n\n        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {\n            setImageToWrapCropBounds();\n        }\n        return true;\n    }\n\n    @Override\n    protected void init() {\n        super.init();\n        setupGestureListeners();\n    }\n\n    /**\n     * This method calculates target scale value for double tap gesture.\n     * User is able to zoom the image from min scale value\n     * to the max scale value with {@link #mDoubleTapScaleSteps} double taps.\n     */\n    protected float getDoubleTapTargetScale() {\n        return getCurrentScale() * (float) Math.pow(getMaxScale() / getMinScale(), 1.0f / mDoubleTapScaleSteps);\n    }\n\n    private void setupGestureListeners() {\n        mGestureDetector = new GestureDetector(getContext(), new GestureListener(), null, true);\n        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());\n        mRotateDetector = new RotationGestureDetector(new RotateListener());\n    }\n\n    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {\n\n        @Override\n        public boolean onScale(ScaleGestureDetector detector) {\n            postScale(detector.getScaleFactor(), mMidPntX, mMidPntY);\n            return true;\n        }\n    }\n\n    private class GestureListener extends GestureDetector.SimpleOnGestureListener {\n\n        @Override\n        public boolean onDoubleTap(MotionEvent e) {\n            zoomImageToPosition(getDoubleTapTargetScale(), e.getX(), e.getY(), DOUBLE_TAP_ZOOM_DURATION);\n            return super.onDoubleTap(e);\n        }\n\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n            postTranslate(-distanceX, -distanceY);\n            return true;\n        }\n\n    }\n\n    private class RotateListener extends RotationGestureDetector.SimpleOnRotationGestureListener {\n\n        @Override\n        public boolean onRotation(RotationGestureDetector rotationDetector) {\n            postRotate(rotationDetector.getAngle(), mMidPntX, mMidPntY);\n            return true;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/OverlayView.java",
    "content": "package com.matisse.ucrop.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.RectF;\nimport android.graphics.Region;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\n\nimport com.matisse.R;\n\nimport com.matisse.ucrop.callback.OverlayViewChangeListener;\nimport com.matisse.ucrop.util.RectUtils;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n * <p/>\n * This view is used for drawing the overlay on top of the image. It may have frame, crop guidelines and dimmed area.\n * This must have LAYER_TYPE_SOFTWARE to draw itself properly.\n */\npublic class OverlayView extends View {\n    public static final boolean DEFAULT_DRAG_FRAME = true;\n    public static final boolean DEFAULT_SHOW_CROP_FRAME = true;\n    public static final boolean DEFAULT_SHOW_CROP_GRID = true;\n    public static final boolean DEFAULT_CIRCLE_DIMMED_LAYER = false;\n    public static final boolean DEFAULT_FREESTYLE_CROP_ENABLED = false;\n    public static final int DEFAULT_CROP_GRID_ROW_COUNT = 2;\n    public static final int DEFAULT_CROP_GRID_COLUMN_COUNT = 2;\n    private boolean mIsDragFrame = DEFAULT_DRAG_FRAME;\n    private final RectF mCropViewRect = new RectF();\n    private final RectF mTempRect = new RectF();\n\n    private int mCropGridRowCount, mCropGridColumnCount;\n    private float mTargetAspectRatio;\n    private float[] mGridPoints = null;\n    private boolean mShowCropFrame, mShowCropGrid;\n    private boolean mCircleDimmedLayer;\n    private int mDimmedColor;\n    private Path mCircularPath = new Path();\n    private Paint mDimmedStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n    private Paint mCropGridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n    private Paint mCropFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n    private Paint mCropFrameCornersPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n\n    protected int mThisWidth, mThisHeight;\n\n    private boolean mIsFreestyleCropEnabled = DEFAULT_FREESTYLE_CROP_ENABLED;\n    protected float[] mCropGridCorners;\n    protected float[] mCropGridCenter;\n    private float mPreviousTouchX = -1, mPreviousTouchY = -1;\n    private int mCurrentTouchCornerIndex = -1;\n    private int mTouchPointThreshold;\n    private int mCropRectMinSize;\n    private int mCropRectCornerTouchAreaLineLength;\n\n    private OverlayViewChangeListener mCallback;\n\n    private boolean mShouldSetupCropBounds;\n\n    {\n        mTouchPointThreshold = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_threshold);\n        mCropRectMinSize = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_min_size);\n        mCropRectCornerTouchAreaLineLength = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_area_line_length);\n    }\n\n    public OverlayView(Context context) {\n        this(context, null);\n    }\n\n    public OverlayView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public OverlayView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init();\n    }\n\n    public OverlayViewChangeListener getOverlayViewChangeListener() {\n        return mCallback;\n    }\n\n    public void setOverlayViewChangeListener(OverlayViewChangeListener callback) {\n        mCallback = callback;\n    }\n\n    @NonNull\n    public RectF getCropViewRect() {\n        return mCropViewRect;\n    }\n\n    public boolean isFreestyleCropEnabled() {\n        return mIsFreestyleCropEnabled;\n    }\n\n    public void setFreestyleCropEnabled(boolean freestyleCropEnabled) {\n        mIsFreestyleCropEnabled = freestyleCropEnabled;\n    }\n\n    public boolean ismIsDragFrame() {\n        return mIsDragFrame;\n    }\n\n    public void setDragFrame(boolean mIsDragFrame) {\n        this.mIsDragFrame = mIsDragFrame;\n    }\n\n    /**\n     * Setter for {@link #mCircleDimmedLayer} variable.\n     *\n     * @param circleDimmedLayer - set it to true if you want dimmed layer to be an circle\n     */\n    public void setCircleDimmedLayer(boolean circleDimmedLayer) {\n        mCircleDimmedLayer = circleDimmedLayer;\n    }\n\n    /**\n     * Setter for crop grid rows count.\n     * Resets {@link #mGridPoints} variable because it is not valid anymore.\n     */\n    public void setCropGridRowCount(@IntRange(from = 0) int cropGridRowCount) {\n        mCropGridRowCount = cropGridRowCount;\n        mGridPoints = null;\n    }\n\n    /**\n     * Setter for crop grid columns count.\n     * Resets {@link #mGridPoints} variable because it is not valid anymore.\n     */\n    public void setCropGridColumnCount(@IntRange(from = 0) int cropGridColumnCount) {\n        mCropGridColumnCount = cropGridColumnCount;\n        mGridPoints = null;\n    }\n\n    /**\n     * Setter for {@link #mShowCropFrame} variable.\n     *\n     * @param showCropFrame - set to true if you want to see a crop frame rectangle on top of an image\n     */\n    public void setShowCropFrame(boolean showCropFrame) {\n        mShowCropFrame = showCropFrame;\n    }\n\n    /**\n     * Setter for {@link #mShowCropGrid} variable.\n     *\n     * @param showCropGrid - set to true if you want to see a crop grid on top of an image\n     */\n    public void setShowCropGrid(boolean showCropGrid) {\n        mShowCropGrid = showCropGrid;\n    }\n\n    /**\n     * Setter for {@link #mDimmedColor} variable.\n     *\n     * @param dimmedColor - desired color of dimmed area around the crop bounds\n     */\n    public void setDimmedColor(@ColorInt int dimmedColor) {\n        mDimmedColor = dimmedColor;\n    }\n\n    /**\n     * Setter for crop frame stroke width\n     */\n    public void setCropFrameStrokeWidth(@IntRange(from = 0) int width) {\n        mCropFramePaint.setStrokeWidth(width);\n    }\n\n    /**\n     * Setter for crop grid stroke width\n     */\n    public void setCropGridStrokeWidth(@IntRange(from = 0) int width) {\n        mCropGridPaint.setStrokeWidth(width);\n    }\n\n    /**\n     * Setter for crop frame color\n     */\n    public void setCropFrameColor(@ColorInt int color) {\n        mCropFramePaint.setColor(color);\n    }\n\n    /**\n     * Setter for crop grid color\n     */\n    public void setCropGridColor(@ColorInt int color) {\n        mCropGridPaint.setColor(color);\n    }\n\n    /**\n     * This method sets aspect ratio for crop bounds.\n     *\n     * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)\n     */\n    public void setTargetAspectRatio(final float targetAspectRatio) {\n        mTargetAspectRatio = targetAspectRatio;\n        if (mThisWidth > 0) {\n            setupCropBounds();\n            postInvalidate();\n        } else {\n            mShouldSetupCropBounds = true;\n        }\n    }\n\n    /**\n     * This method setups crop bounds rectangles for given aspect ratio and view size.\n     * {@link #mCropViewRect} is used to draw crop bounds - uses padding.\n     */\n    public void setupCropBounds() {\n        int height = (int) (mThisWidth / mTargetAspectRatio);\n        if (height > mThisHeight) {\n            int width = (int) (mThisHeight * mTargetAspectRatio);\n            int halfDiff = (mThisWidth - width) / 2;\n            mCropViewRect.set(getPaddingLeft() + halfDiff, getPaddingTop(),\n                    getPaddingLeft() + width + halfDiff, getPaddingTop() + mThisHeight);\n        } else {\n            int halfDiff = (mThisHeight - height) / 2;\n            mCropViewRect.set(getPaddingLeft(), getPaddingTop() + halfDiff,\n                    getPaddingLeft() + mThisWidth, getPaddingTop() + height + halfDiff);\n        }\n\n        if (mCallback != null) {\n            mCallback.onCropRectUpdated(mCropViewRect);\n        }\n\n        updateGridPoints();\n    }\n\n    private void updateGridPoints() {\n        mCropGridCorners = RectUtils.getCornersFromRect(mCropViewRect);\n        mCropGridCenter = RectUtils.getCenterFromRect(mCropViewRect);\n\n        mGridPoints = null;\n        mCircularPath.reset();\n        mCircularPath.addCircle(mCropViewRect.centerX(), mCropViewRect.centerY(),\n                Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, Path.Direction.CW);\n    }\n\n    protected void init() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 &&\n                Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {\n            setLayerType(LAYER_TYPE_SOFTWARE, null);\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        if (changed) {\n            left = getPaddingLeft();\n            top = getPaddingTop();\n            right = getWidth() - getPaddingRight();\n            bottom = getHeight() - getPaddingBottom();\n            mThisWidth = right - left;\n            mThisHeight = bottom - top;\n\n            if (mShouldSetupCropBounds) {\n                mShouldSetupCropBounds = false;\n                setTargetAspectRatio(mTargetAspectRatio);\n            }\n        }\n    }\n\n    /**\n     * Along with image there are dimmed layer, crop bounds and crop guidelines that must be drawn.\n     */\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        drawDimmedLayer(canvas);\n        drawCropGrid(canvas);\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mCropViewRect.isEmpty() || !mIsFreestyleCropEnabled) return false;\n\n        float x = event.getX();\n        float y = event.getY();\n\n        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {\n            if (mPreviousTouchX < 0) {\n                mPreviousTouchX = x;\n                mPreviousTouchY = y;\n            }\n            mCurrentTouchCornerIndex = getCurrentTouchIndex(x, y);\n            return mCurrentTouchCornerIndex != -1;\n        }\n\n        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) {\n            if (event.getPointerCount() == 1 && mCurrentTouchCornerIndex != -1) {\n\n                x = Math.min(Math.max(x, getPaddingLeft()), getWidth() - getPaddingRight());\n                y = Math.min(Math.max(y, getPaddingTop()), getHeight() - getPaddingBottom());\n\n                updateCropViewRect(x, y);\n\n                mPreviousTouchX = x;\n                mPreviousTouchY = y;\n\n                return true;\n            }\n        }\n\n        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {\n            mPreviousTouchX = -1;\n            mPreviousTouchY = -1;\n            mCurrentTouchCornerIndex = -1;\n\n            if (mCallback != null) {\n                mCallback.onCropRectUpdated(mCropViewRect);\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * * The order of the corners is:\n     * 0------->1\n     * ^        |\n     * |   4    |\n     * |        v\n     * 3<-------2\n     */\n    private void updateCropViewRect(float touchX, float touchY) {\n        mTempRect.set(mCropViewRect);\n\n        switch (mCurrentTouchCornerIndex) {\n            // resize rectangle\n            case 0:\n                // 是否可拖动裁剪框\n                if (mIsDragFrame) {\n                    mTempRect.set(touchX, touchY, mCropViewRect.right, mCropViewRect.bottom);\n                }\n                break;\n            case 1:\n                // 是否可拖动裁剪框\n                if (mIsDragFrame) {\n                    mTempRect.set(mCropViewRect.left, touchY, touchX, mCropViewRect.bottom);\n                }\n                break;\n            case 2:\n                // 是否可拖动裁剪框\n                if (mIsDragFrame) {\n                    mTempRect.set(mCropViewRect.left, mCropViewRect.top, touchX, touchY);\n                }\n                break;\n            case 3:\n                // 是否可拖动裁剪框\n                if (mIsDragFrame) {\n                    mTempRect.set(touchX, mCropViewRect.top, mCropViewRect.right, touchY);\n                }\n                break;\n            // move rectangle\n            case 4:\n                mTempRect.offset(touchX - mPreviousTouchX, touchY - mPreviousTouchY);\n                if (mTempRect.left > getLeft() && mTempRect.top > getTop()\n                        && mTempRect.right < getRight() && mTempRect.bottom < getBottom()) {\n                    mCropViewRect.set(mTempRect);\n                    updateGridPoints();\n                    postInvalidate();\n                }\n                return;\n        }\n\n        boolean changeHeight = mTempRect.height() >= mCropRectMinSize;\n        boolean changeWidth = mTempRect.width() >= mCropRectMinSize;\n        mCropViewRect.set(\n                changeWidth ? mTempRect.left : mCropViewRect.left,\n                changeHeight ? mTempRect.top : mCropViewRect.top,\n                changeWidth ? mTempRect.right : mCropViewRect.right,\n                changeHeight ? mTempRect.bottom : mCropViewRect.bottom);\n\n        if (changeHeight || changeWidth) {\n            updateGridPoints();\n            postInvalidate();\n        }\n    }\n\n    /**\n     * * The order of the corners in the float array is:\n     * 0------->1\n     * ^        |\n     * |   4    |\n     * |        v\n     * 3<-------2\n     *\n     * @return - index of corner that is being dragged\n     */\n    private int getCurrentTouchIndex(float touchX, float touchY) {\n        int closestPointIndex = -1;\n        double closestPointDistance = mTouchPointThreshold;\n        for (int i = 0; i < 8; i += 2) {\n            double distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2)\n                    + Math.pow(touchY - mCropGridCorners[i + 1], 2));\n            if (distanceToCorner < closestPointDistance) {\n                closestPointDistance = distanceToCorner;\n                closestPointIndex = i / 2;\n            }\n        }\n        if (closestPointIndex < 0 && mCropViewRect.contains(touchX, touchY)) {\n            return 4;\n        }\n\n//        for (int i = 0; i <= 8; i += 2) {\n//\n//            double distanceToCorner;\n//            if (i < 8) { // corners\n//                distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2)\n//                        + Math.pow(touchY - mCropGridCorners[i + 1], 2));\n//            } else { // center\n//                distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCenter[0], 2)\n//                        + Math.pow(touchY - mCropGridCenter[1], 2));\n//            }\n//            if (distanceToCorner < closestPointDistance) {\n//                closestPointDistance = distanceToCorner;\n//                closestPointIndex = i / 2;\n//            }\n//        }\n        return closestPointIndex;\n    }\n\n    /**\n     * This method draws dimmed area around the crop bounds.\n     *\n     * @param canvas - valid canvas object\n     */\n    protected void drawDimmedLayer(@NonNull Canvas canvas) {\n        canvas.save();\n        if (mCircleDimmedLayer) {\n            canvas.clipPath(mCircularPath, Region.Op.DIFFERENCE);\n        } else {\n            canvas.clipRect(mCropViewRect, Region.Op.DIFFERENCE);\n        }\n        canvas.drawColor(mDimmedColor);\n        canvas.restore();\n\n        if (mCircleDimmedLayer) { // Draw 1px stroke to fix antialias\n            canvas.drawCircle(mCropViewRect.centerX(), mCropViewRect.centerY(),\n                    Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, mDimmedStrokePaint);\n        }\n    }\n\n    /**\n     * This method draws crop bounds (empty rectangle)\n     * and crop guidelines (vertical and horizontal lines inside the crop bounds) if needed.\n     *\n     * @param canvas - valid canvas object\n     */\n    protected void drawCropGrid(@NonNull Canvas canvas) {\n        if (mShowCropGrid) {\n            if (mGridPoints == null && !mCropViewRect.isEmpty()) {\n\n                mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4];\n\n                int index = 0;\n                for (int i = 0; i < mCropGridRowCount; i++) {\n                    mGridPoints[index++] = mCropViewRect.left;\n                    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;\n                    mGridPoints[index++] = mCropViewRect.right;\n                    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;\n                }\n\n                for (int i = 0; i < mCropGridColumnCount; i++) {\n                    mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;\n                    mGridPoints[index++] = mCropViewRect.top;\n                    mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;\n                    mGridPoints[index++] = mCropViewRect.bottom;\n                }\n            }\n\n            if (mGridPoints != null) {\n                canvas.drawLines(mGridPoints, mCropGridPaint);\n            }\n        }\n\n        if (mShowCropFrame) {\n            canvas.drawRect(mCropViewRect, mCropFramePaint);\n        }\n\n        if (mIsFreestyleCropEnabled) {\n            canvas.save();\n\n            mTempRect.set(mCropViewRect);\n            mTempRect.inset(mCropRectCornerTouchAreaLineLength, -mCropRectCornerTouchAreaLineLength);\n            canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);\n\n            mTempRect.set(mCropViewRect);\n            mTempRect.inset(-mCropRectCornerTouchAreaLineLength, mCropRectCornerTouchAreaLineLength);\n            canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);\n\n            canvas.drawRect(mCropViewRect, mCropFrameCornersPaint);\n\n            canvas.restore();\n        }\n    }\n\n    /**\n     * This method extracts all needed values from the styled attributes.\n     * Those are used to configure the view.\n     */\n    @SuppressWarnings(\"deprecation\")\n    protected void processStyledAttributes(@NonNull TypedArray a) {\n        mCircleDimmedLayer = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_circle_dimmed_layer, DEFAULT_CIRCLE_DIMMED_LAYER);\n        mDimmedColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_dimmed_color,\n                getResources().getColor(R.color.ucrop_color_default_dimmed));\n        mDimmedStrokePaint.setColor(mDimmedColor);\n        mDimmedStrokePaint.setStyle(Paint.Style.STROKE);\n        mDimmedStrokePaint.setStrokeWidth(1);\n\n        initCropFrameStyle(a);\n        mShowCropFrame = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_frame, DEFAULT_SHOW_CROP_FRAME);\n\n        initCropGridStyle(a);\n        mShowCropGrid = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_grid, DEFAULT_SHOW_CROP_GRID);\n    }\n\n    /**\n     * This method setups Paint object for the crop bounds.\n     */\n    @SuppressWarnings(\"deprecation\")\n    private void initCropFrameStyle(@NonNull TypedArray a) {\n        int cropFrameStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_frame_stroke_size,\n                getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width));\n        int cropFrameColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_frame_color,\n                getResources().getColor(R.color.ucrop_color_default_crop_frame));\n        mCropFramePaint.setStrokeWidth(cropFrameStrokeSize);\n        mCropFramePaint.setColor(cropFrameColor);\n        mCropFramePaint.setStyle(Paint.Style.STROKE);\n\n        mCropFrameCornersPaint.setStrokeWidth(cropFrameStrokeSize * 3);\n        mCropFrameCornersPaint.setColor(cropFrameColor);\n        mCropFrameCornersPaint.setStyle(Paint.Style.STROKE);\n    }\n\n    /**\n     * This method setups Paint object for the crop guidelines.\n     */\n    @SuppressWarnings(\"deprecation\")\n    private void initCropGridStyle(@NonNull TypedArray a) {\n        int cropGridStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_grid_stroke_size,\n                getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width));\n        int cropGridColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_grid_color,\n                getResources().getColor(R.color.ucrop_color_default_crop_grid));\n        mCropGridPaint.setStrokeWidth(cropGridStrokeSize);\n        mCropGridPaint.setColor(cropGridColor);\n\n        mCropGridRowCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_row_count, DEFAULT_CROP_GRID_ROW_COUNT);\n        mCropGridColumnCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_column_count, DEFAULT_CROP_GRID_COLUMN_COUNT);\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/TransformImageView.java",
    "content": "package com.matisse.ucrop.view;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.widget.ImageView;\n\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.matisse.ucrop.callback.BitmapLoadCallback;\nimport com.matisse.ucrop.model.ExifInfo;\nimport com.matisse.ucrop.util.BitmapLoadUtils;\nimport com.matisse.ucrop.util.FastBitmapDrawable;\nimport com.matisse.ucrop.util.RectUtils;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n * <p/>\n * This class provides base logic to setup the image, transform it with matrix (move, scale, rotate),\n * and methods to get current matrix state.\n */\npublic class TransformImageView extends ImageView {\n\n    private static final String TAG = \"TransformImageView\";\n\n    private static final int RECT_CORNER_POINTS_COORDS = 8;\n    private static final int RECT_CENTER_POINT_COORDS = 2;\n    private static final int MATRIX_VALUES_COUNT = 9;\n\n    protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS];\n    protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS];\n\n    private final float[] mMatrixValues = new float[MATRIX_VALUES_COUNT];\n\n    protected Matrix mCurrentImageMatrix = new Matrix();\n    protected int mThisWidth, mThisHeight;\n\n    protected TransformImageListener mTransformImageListener;\n\n    private float[] mInitialImageCorners;\n    private float[] mInitialImageCenter;\n\n    protected boolean mBitmapDecoded = false;\n    protected boolean mBitmapLaidOut = false;\n\n    private int mMaxBitmapSize = 0;\n\n    private Uri mImageInputUri;\n    private String mImageOutputPath;\n    private ExifInfo mExifInfo;\n\n    /**\n     * Interface for rotation and scale change notifying.\n     */\n    public interface TransformImageListener {\n\n        void onLoadComplete();\n\n        void onLoadFailure(@NonNull Exception e);\n\n        void onRotate(float currentAngle);\n\n        void onScale(float currentScale);\n\n    }\n\n    public TransformImageView(Context context) {\n        this(context, null);\n    }\n\n    public TransformImageView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public TransformImageView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init();\n    }\n\n    public void setTransformImageListener(TransformImageListener transformImageListener) {\n        mTransformImageListener = transformImageListener;\n    }\n\n    @Override\n    public void setScaleType(ScaleType scaleType) {\n        if (scaleType == ScaleType.MATRIX) {\n            super.setScaleType(scaleType);\n        } else {\n            Log.w(TAG, \"Invalid ScaleType. Only ScaleType.MATRIX can be used\");\n        }\n    }\n\n    /**\n     * Setter for {@link #mMaxBitmapSize} value.\n     * Be sure to call it before {@link #setImageURI(Uri)} or other image setters.\n     *\n     * @param maxBitmapSize - max size for both width and height of bitmap that will be used in the view.\n     */\n    public void setMaxBitmapSize(int maxBitmapSize) {\n        mMaxBitmapSize = maxBitmapSize;\n    }\n\n    public int getMaxBitmapSize() {\n        if (mMaxBitmapSize <= 0) {\n            mMaxBitmapSize = BitmapLoadUtils.calculateMaxBitmapSize(getContext());\n        }\n        return mMaxBitmapSize;\n    }\n\n    @Override\n    public void setImageBitmap(final Bitmap bitmap) {\n        setImageDrawable(new FastBitmapDrawable(bitmap));\n    }\n\n    public Uri getImageInputUri() {\n        return mImageInputUri;\n    }\n\n    public String getImageOutputPath() {\n        return mImageOutputPath;\n    }\n\n    public ExifInfo getExifInfo() {\n        return mExifInfo;\n    }\n\n    /**\n     * This method takes an Uri as a parameter, then calls method to decode it into Bitmap with specified size.\n     *\n     * @param imageUri - image Uri\n     * @throws Exception - can throw exception if having problems with decoding Uri or OOM.\n     */\n    public void setImageUri(@NonNull Uri imageUri, @Nullable Uri outputUri) throws Exception {\n        int maxBitmapSize = getMaxBitmapSize();\n\n        BitmapLoadUtils.decodeBitmapInBackground(getContext(), imageUri, outputUri, maxBitmapSize, maxBitmapSize,\n                new BitmapLoadCallback() {\n\n                    @Override\n                    public void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull Uri imageInputUri, @Nullable Uri imageOutputUri) {\n                        mImageInputUri = imageInputUri;\n                        mImageOutputPath = imageOutputUri.getPath();\n                        mExifInfo = exifInfo;\n\n                        mBitmapDecoded = true;\n                        setImageBitmap(bitmap);\n                    }\n\n                    @Override\n                    public void onFailure(@NonNull Exception bitmapWorkerException) {\n                        Log.e(TAG, \"onFailure: setImageUri\", bitmapWorkerException);\n                        if (mTransformImageListener != null) {\n                            mTransformImageListener.onLoadFailure(bitmapWorkerException);\n                        }\n                    }\n                });\n    }\n\n    /**\n     * @return - current image scale value.\n     * [1.0f - for original image, 2.0f - for 200% scaled image, etc.]\n     */\n    public float getCurrentScale() {\n        return getMatrixScale(mCurrentImageMatrix);\n    }\n\n    /**\n     * This method calculates scale value for given Matrix object.\n     */\n    public float getMatrixScale(@NonNull Matrix matrix) {\n        return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2)\n                + Math.pow(getMatrixValue(matrix, Matrix.MSKEW_Y), 2));\n    }\n\n    /**\n     * @return - current image rotation angle.\n     */\n    public float getCurrentAngle() {\n        return getMatrixAngle(mCurrentImageMatrix);\n    }\n\n    /**\n     * This method calculates rotation angle for given Matrix object.\n     */\n    public float getMatrixAngle(@NonNull Matrix matrix) {\n        return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X),\n                getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI));\n    }\n\n    @Override\n    public void setImageMatrix(Matrix matrix) {\n        super.setImageMatrix(matrix);\n        mCurrentImageMatrix.set(matrix);\n        updateCurrentImagePoints();\n    }\n\n    @Nullable\n    public Bitmap getViewBitmap() {\n        if (getDrawable() == null || !(getDrawable() instanceof FastBitmapDrawable)) {\n            return null;\n        } else {\n            return ((FastBitmapDrawable) getDrawable()).getBitmap();\n        }\n    }\n\n    /**\n     * This method translates current image.\n     *\n     * @param deltaX - horizontal shift\n     * @param deltaY - vertical shift\n     */\n    public void postTranslate(float deltaX, float deltaY) {\n        if (deltaX != 0 || deltaY != 0) {\n            mCurrentImageMatrix.postTranslate(deltaX, deltaY);\n            setImageMatrix(mCurrentImageMatrix);\n        }\n    }\n\n    /**\n     * This method scales current image.\n     *\n     * @param deltaScale - scale value\n     * @param px         - scale center X\n     * @param py         - scale center Y\n     */\n    public void postScale(float deltaScale, float px, float py) {\n        if (deltaScale != 0) {\n            mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);\n            setImageMatrix(mCurrentImageMatrix);\n            if (mTransformImageListener != null) {\n                mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));\n            }\n        }\n    }\n\n    /**\n     * This method rotates current image.\n     *\n     * @param deltaAngle - rotation angle\n     * @param px         - rotation center X\n     * @param py         - rotation center Y\n     */\n    public void postRotate(float deltaAngle, float px, float py) {\n        if (deltaAngle != 0) {\n            mCurrentImageMatrix.postRotate(deltaAngle, px, py);\n            setImageMatrix(mCurrentImageMatrix);\n            if (mTransformImageListener != null) {\n                mTransformImageListener.onRotate(getMatrixAngle(mCurrentImageMatrix));\n            }\n        }\n    }\n\n    protected void init() {\n        setScaleType(ScaleType.MATRIX);\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        if (changed || (mBitmapDecoded && !mBitmapLaidOut)) {\n\n            left = getPaddingLeft();\n            top = getPaddingTop();\n            right = getWidth() - getPaddingRight();\n            bottom = getHeight() - getPaddingBottom();\n            mThisWidth = right - left;\n            mThisHeight = bottom - top;\n\n            onImageLaidOut();\n        }\n    }\n\n    /**\n     * When image is laid out {@link #mInitialImageCenter} and {@link #mInitialImageCenter}\n     * must be set.\n     */\n    protected void onImageLaidOut() {\n        final Drawable drawable = getDrawable();\n        if (drawable == null) {\n            return;\n        }\n\n        float w = drawable.getIntrinsicWidth();\n        float h = drawable.getIntrinsicHeight();\n\n        Log.d(TAG, String.format(\"Image size: [%d:%d]\", (int) w, (int) h));\n\n        RectF initialImageRect = new RectF(0, 0, w, h);\n        mInitialImageCorners = RectUtils.getCornersFromRect(initialImageRect);\n        mInitialImageCenter = RectUtils.getCenterFromRect(initialImageRect);\n\n        mBitmapLaidOut = true;\n\n        if (mTransformImageListener != null) {\n            mTransformImageListener.onLoadComplete();\n        }\n    }\n\n    /**\n     * This method returns Matrix value for given index.\n     *\n     * @param matrix     - valid Matrix object\n     * @param valueIndex - index of needed value. See {@link Matrix#MSCALE_X} and others.\n     * @return - matrix value for index\n     */\n    protected float getMatrixValue(@NonNull Matrix matrix, @IntRange(from = 0, to = MATRIX_VALUES_COUNT) int valueIndex) {\n        matrix.getValues(mMatrixValues);\n        return mMatrixValues[valueIndex];\n    }\n\n    /**\n     * This method logs given matrix X, Y, scale, and angle values.\n     * Can be used for debug.\n     */\n    @SuppressWarnings(\"unused\")\n    protected void printMatrix(@NonNull String logPrefix, @NonNull Matrix matrix) {\n        float x = getMatrixValue(matrix, Matrix.MTRANS_X);\n        float y = getMatrixValue(matrix, Matrix.MTRANS_Y);\n        float rScale = getMatrixScale(matrix);\n        float rAngle = getMatrixAngle(matrix);\n        Log.d(TAG, logPrefix + \": matrix: { x: \" + x + \", y: \" + y + \", scale: \" + rScale + \", angle: \" + rAngle + \" }\");\n    }\n\n    /**\n     * This method updates current image corners and center points that are stored in\n     * {@link #mCurrentImageCorners} and {@link #mCurrentImageCenter} arrays.\n     * Those are used for several calculations.\n     */\n    private void updateCurrentImagePoints() {\n        mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);\n        mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/UCropView.java",
    "content": "package com.matisse.ucrop.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\n\nimport com.matisse.R;\n\npublic class UCropView extends FrameLayout {\n\n    private final GestureCropImageView mGestureCropImageView;\n    private final OverlayView mViewOverlay;\n\n    public UCropView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public UCropView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        LayoutInflater.from(context).inflate(R.layout.ucrop_view, this, true);\n        mGestureCropImageView = findViewById(R.id.image_view_crop);\n        mViewOverlay = findViewById(R.id.view_overlay);\n\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_UCropView);\n        mViewOverlay.processStyledAttributes(a);\n        mGestureCropImageView.processStyledAttributes(a);\n        a.recycle();\n\n\n        mGestureCropImageView.setCropBoundsChangeListener(cropRatio -> mViewOverlay.setTargetAspectRatio(cropRatio));\n        mViewOverlay.setOverlayViewChangeListener(cropRect -> mGestureCropImageView.setCropRect(cropRect));\n    }\n\n    @Override\n    public boolean shouldDelayChildPressedState() {\n        return false;\n    }\n\n    @NonNull\n    public GestureCropImageView getCropImageView() {\n        return mGestureCropImageView;\n    }\n\n    @NonNull\n    public OverlayView getOverlayView() {\n        return mViewOverlay;\n    }\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/widget/AspectRatioTextView.java",
    "content": "package com.matisse.ucrop.view.widget;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.os.Build;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.view.Gravity;\nimport android.widget.TextView;\n\nimport com.matisse.ucrop.model.AspectRatio;\nimport com.matisse.R;\nimport com.matisse.ucrop.view.CropImageView;\n\nimport java.util.Locale;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\npublic class AspectRatioTextView extends TextView {\n\n    private final Rect mCanvasClipBounds = new Rect();\n    private Paint mDotPaint;\n    private int mDotSize;\n    private float mAspectRatio;\n\n    private String mAspectRatioTitle;\n    private float mAspectRatioX, mAspectRatioY;\n\n    public AspectRatioTextView(Context context) {\n        this(context, null);\n    }\n\n    public AspectRatioTextView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);\n        init(a);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);\n        init(a);\n    }\n\n    /**\n     * @param activeColor the resolved color for active elements\n     */\n\n    public void setActiveColor(@ColorInt int activeColor) {\n        applyActiveColor(activeColor);\n        invalidate();\n    }\n\n    public void setAspectRatio(@NonNull AspectRatio aspectRatio) {\n        mAspectRatioTitle = aspectRatio.getAspectRatioTitle();\n        mAspectRatioX = aspectRatio.getAspectRatioX();\n        mAspectRatioY = aspectRatio.getAspectRatioY();\n\n        if (mAspectRatioX == CropImageView.SOURCE_IMAGE_ASPECT_RATIO || mAspectRatioY == CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {\n            mAspectRatio = CropImageView.SOURCE_IMAGE_ASPECT_RATIO;\n        } else {\n            mAspectRatio = mAspectRatioX / mAspectRatioY;\n        }\n\n        setTitle();\n    }\n\n    public float getAspectRatio(boolean toggleRatio) {\n        if (toggleRatio) {\n            toggleAspectRatio();\n            setTitle();\n        }\n        return mAspectRatio;\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n\n        if (isSelected()) {\n            canvas.getClipBounds(mCanvasClipBounds);\n            canvas.drawCircle((mCanvasClipBounds.right - mCanvasClipBounds.left) / 2.0f, mCanvasClipBounds.bottom - mDotSize,\n                    mDotSize / 2, mDotPaint);\n        }\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    private void init(@NonNull TypedArray a) {\n        setGravity(Gravity.CENTER_HORIZONTAL);\n\n        mAspectRatioTitle = a.getString(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_title);\n        mAspectRatioX = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_x, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);\n        mAspectRatioY = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_y, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);\n\n        if (mAspectRatioX == CropImageView.SOURCE_IMAGE_ASPECT_RATIO || mAspectRatioY == CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {\n            mAspectRatio = CropImageView.SOURCE_IMAGE_ASPECT_RATIO;\n        } else {\n            mAspectRatio = mAspectRatioX / mAspectRatioY;\n        }\n\n        mDotSize = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_size_dot_scale_text_view);\n        mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mDotPaint.setStyle(Paint.Style.FILL);\n\n        setTitle();\n\n        int activeColor = getResources().getColor(R.color.ucrop_color_widget_active);\n        applyActiveColor(activeColor);\n\n        a.recycle();\n    }\n\n    private void applyActiveColor(@ColorInt int activeColor) {\n        if (mDotPaint != null) {\n            mDotPaint.setColor(activeColor);\n        }\n        ColorStateList textViewColorStateList = new ColorStateList(\n                new int[][]{\n                        new int[]{android.R.attr.state_selected},\n                        new int[]{0}\n                },\n                new int[]{\n                        activeColor,\n                        ContextCompat.getColor(getContext(), R.color.ucrop_color_widget)\n                }\n        );\n\n        setTextColor(textViewColorStateList);\n    }\n\n    private void toggleAspectRatio() {\n        if (mAspectRatio != CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {\n            float tempRatioW = mAspectRatioX;\n            mAspectRatioX = mAspectRatioY;\n            mAspectRatioY = tempRatioW;\n\n            mAspectRatio = mAspectRatioX / mAspectRatioY;\n        }\n    }\n\n    private void setTitle() {\n        if (!TextUtils.isEmpty(mAspectRatioTitle)) {\n            setText(mAspectRatioTitle);\n        } else {\n            setText(String.format(Locale.US, \"%d:%d\", (int) mAspectRatioX, (int) mAspectRatioY));\n        }\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ucrop/view/widget/HorizontalProgressWheelView.java",
    "content": "package com.matisse.ucrop.view.widget;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.os.Build;\n\nimport androidx.annotation.ColorInt;\nimport androidx.core.content.ContextCompat;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport com.matisse.R;\n\n/**\n * Created by Oleksii Shliama (https://github.com/shliama).\n */\npublic class HorizontalProgressWheelView extends View {\n\n    private final Rect mCanvasClipBounds = new Rect();\n\n    private ScrollingListener mScrollingListener;\n    private float mLastTouchedPosition;\n\n    private Paint mProgressLinePaint;\n    private int mProgressLineWidth, mProgressLineHeight;\n    private int mProgressLineMargin;\n\n    private boolean mScrollStarted;\n    private float mTotalScrollDistance;\n\n    private int mMiddleLineColor;\n\n    public HorizontalProgressWheelView(Context context) {\n        this(context, null);\n    }\n\n    public HorizontalProgressWheelView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init();\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n    }\n\n    public void setScrollingListener(ScrollingListener scrollingListener) {\n        mScrollingListener = scrollingListener;\n    }\n\n    public void setMiddleLineColor(@ColorInt int middleLineColor) {\n        mMiddleLineColor = middleLineColor;\n        invalidate();\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mLastTouchedPosition = event.getX();\n                break;\n            case MotionEvent.ACTION_UP:\n                if (mScrollingListener != null) {\n                    mScrollStarted = false;\n                    mScrollingListener.onScrollEnd();\n                }\n                break;\n            case MotionEvent.ACTION_MOVE:\n                float distance = event.getX() - mLastTouchedPosition;\n                if (distance != 0) {\n                    if (!mScrollStarted) {\n                        mScrollStarted = true;\n                        if (mScrollingListener != null) {\n                            mScrollingListener.onScrollStart();\n                        }\n                    }\n                    onScrollEvent(event, distance);\n                }\n                break;\n        }\n        return true;\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        canvas.getClipBounds(mCanvasClipBounds);\n\n        int linesCount = mCanvasClipBounds.width() / (mProgressLineWidth + mProgressLineMargin);\n        float deltaX = (mTotalScrollDistance) % (float) (mProgressLineMargin + mProgressLineWidth);\n\n        mProgressLinePaint.setColor(getResources().getColor(R.color.ucrop_color_progress_wheel_line));\n        for (int i = 0; i < linesCount; i++) {\n            if (i < (linesCount / 4)) {\n                mProgressLinePaint.setAlpha((int) (255 * (i / (float) (linesCount / 4))));\n            } else if (i > (linesCount * 3 / 4)) {\n                mProgressLinePaint.setAlpha((int) (255 * ((linesCount - i) / (float) (linesCount / 4))));\n            } else {\n                mProgressLinePaint.setAlpha(255);\n            }\n            canvas.drawLine(\n                    -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),\n                    mCanvasClipBounds.centerY() - mProgressLineHeight / 4.0f,\n                    -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),\n                    mCanvasClipBounds.centerY() + mProgressLineHeight / 4.0f, mProgressLinePaint);\n        }\n\n        mProgressLinePaint.setColor(mMiddleLineColor);\n        canvas.drawLine(mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() - mProgressLineHeight / 2.0f, mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() + mProgressLineHeight / 2.0f, mProgressLinePaint);\n\n    }\n\n    private void onScrollEvent(MotionEvent event, float distance) {\n        mTotalScrollDistance -= distance;\n        postInvalidate();\n        mLastTouchedPosition = event.getX();\n        if (mScrollingListener != null) {\n            mScrollingListener.onScroll(-distance, mTotalScrollDistance);\n        }\n    }\n\n    private void init() {\n        mMiddleLineColor = ContextCompat.getColor(getContext(), R.color.ucrop_color_progress_wheel_line);\n\n        mProgressLineWidth = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_width_horizontal_wheel_progress_line);\n        mProgressLineHeight = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_height_horizontal_wheel_progress_line);\n        mProgressLineMargin = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_margin_horizontal_wheel_progress_line);\n\n        mProgressLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mProgressLinePaint.setStyle(Paint.Style.STROKE);\n        mProgressLinePaint.setStrokeWidth(mProgressLineWidth);\n\n    }\n\n    public interface ScrollingListener {\n\n        void onScrollStart();\n\n        void onScroll(float delta, float totalDistance);\n\n        void onScrollEnd();\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/AlbumPreviewActivity.kt",
    "content": "package com.matisse.ui.activity\n\nimport android.database.Cursor\nimport com.matisse.entity.Album\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.Item\nimport com.matisse.model.AlbumCallbacks\nimport com.matisse.model.AlbumMediaCollection\nimport com.matisse.ui.adapter.PreviewPagerAdapter\nimport kotlinx.android.synthetic.main.activity_media_preview.*\n\n/**\n * Created by liubo on 2018/9/11.\n */\nclass AlbumPreviewActivity : BasePreviewActivity(), AlbumCallbacks {\n\n    private var collection = AlbumMediaCollection()\n    private var isAlreadySetPosition = false\n\n    override fun setViewData() {\n        super.setViewData()\n        collection.onCreate(this, this)\n        val album = intent.getParcelableExtra<Album>(ConstValue.EXTRA_ALBUM) ?: return\n        collection.load(album)\n        val item = intent.getParcelableExtra<Item>(ConstValue.EXTRA_ITEM)\n        check_view?.apply {\n            if (spec?.isCountable() == true) {\n                setCheckedNum(selectedCollection.checkedNumOf(item))\n            } else {\n                setChecked(selectedCollection.isSelected(item))\n            }\n        }\n        updateSize(item)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        collection.onDestroy()\n    }\n\n    override fun onAlbumLoad(cursor: Cursor) {\n        val items = ArrayList<Item>()\n        while (cursor.moveToNext()) {\n            Item.valueOf(cursor)?.run { items.add(this) }\n        }\n\n        if (items.isEmpty()) return\n        val adapter = pager?.adapter as PreviewPagerAdapter\n        adapter.addAll(items)\n        adapter.notifyDataSetChanged()\n        if (!isAlreadySetPosition) {\n            isAlreadySetPosition = true\n            val selected = intent.getParcelableExtra<Item>(ConstValue.EXTRA_ITEM) ?: return\n            val selectedIndex = items.indexOf(selected)\n            pager?.setCurrentItem(selectedIndex, false)\n            previousPos = selectedIndex\n        }\n    }\n\n    override fun onAlbumReset() {\n    }\n\n    override fun onAlbumStart() {\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/BaseActivity.kt",
    "content": "package com.matisse.ui.activity\n\nimport android.app.Activity\nimport android.content.pm.ActivityInfo\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport com.matisse.R\nimport com.matisse.entity.IncapableCause\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.handleCause\nimport com.matisse.utils.obtainAttrString\n\nabstract class BaseActivity : AppCompatActivity() {\n\n    lateinit var activity: Activity\n    var spec: SelectionSpec? = null\n    var instanceState: Bundle? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        spec = SelectionSpec.getInstance()\n        setTheme(spec?.themeId ?: R.style.Matisse_Default)\n        super.onCreate(savedInstanceState)\n        if (safeCancelActivity()) return\n        activity = this\n        setContentView(getResourceLayoutId())\n        configActivity()\n        configSaveInstanceState(savedInstanceState)\n        setViewData()\n        initListener()\n    }\n\n    private fun safeCancelActivity(): Boolean {\n        if (spec?.hasInited == false) {\n            setResult(Activity.RESULT_CANCELED)\n            finish()\n            return true\n        }\n\n        return false\n    }\n\n    /**\n     * 处理状态栏(状态栏颜色、状态栏字体颜色、是否隐藏等操作)\n     *\n     * 空实现，供外部重写\n     */\n    open fun configActivity() {\n        if (spec?.needOrientationRestriction() == true) {\n            requestedOrientation = spec?.orientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n        }\n    }\n\n    abstract fun getResourceLayoutId(): Int\n\n    private fun configSaveInstanceState(savedInstanceState: Bundle?) {\n        instanceState = savedInstanceState\n    }\n\n    abstract fun setViewData()\n\n    abstract fun initListener()\n\n    /**\n     * 获取主题配置中的属性值\n     * @param attr 主题配置属性key\n     * @param defaultRes 默认值\n     */\n    fun getAttrString(attr: Int, defaultRes: Int) = obtainAttrString(this, attr, defaultRes)\n\n    /**\n     * 抽离提示方法\n     */\n    fun handleCauseTips(\n        message: String = \"\", @IncapableCause.Form form: Int = IncapableCause.TOAST,\n        title: String = \"\", dismissLoading: Boolean = true\n    ) {\n        handleCause(activity, IncapableCause(form, title, message, dismissLoading))\n    }\n\n\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/BasePreviewActivity.kt",
    "content": "package com.matisse.ui.activity\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.View\nimport android.view.WindowManager\nimport androidx.viewpager.widget.ViewPager\nimport com.matisse.R\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.model.SelectedItemCollection\nimport com.matisse.ucrop.UCrop\nimport com.matisse.ui.adapter.PreviewPagerAdapter\nimport com.matisse.ui.view.PicturePreviewItemFragment\nimport com.matisse.utils.*\nimport com.matisse.widget.CheckView\nimport kotlinx.android.synthetic.main.activity_media_preview.*\nimport kotlinx.android.synthetic.main.include_view_bottom.*\n\n/**\n * desc：BasePreviewActivity</br>\n * time: 2018/9/6-11:15</br>\n * author：liubo </br>\n * since V 1.0.0 </br>\n */\nopen class BasePreviewActivity : BaseActivity(), View.OnClickListener,\n    ViewPager.OnPageChangeListener {\n\n    lateinit var selectedCollection: SelectedItemCollection\n    var adapter: PreviewPagerAdapter? = null\n    var previousPos = -1\n    private var originalEnable = false\n\n\n    override fun configActivity() {\n        super.configActivity()\n        spec?.statusBarFuture?.invoke(this, null)\n\n        if (Platform.hasKitKat19()) {\n            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)\n        }\n\n        selectedCollection = SelectedItemCollection(this)\n        originalEnable = if (instanceState == null) {\n            selectedCollection.onCreate(intent.getBundleExtra(ConstValue.EXTRA_DEFAULT_BUNDLE))\n            intent.getBooleanExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, false)\n        } else {\n            selectedCollection.onCreate(instanceState)\n            instanceState!!.getBoolean(ConstValue.CHECK_STATE)\n        }\n    }\n\n    override fun getResourceLayoutId() = R.layout.activity_media_preview\n\n    override fun setViewData() {\n        button_preview.setText(getAttrString(R.attr.Preview_Back_text, R.string.button_back))\n\n        adapter = PreviewPagerAdapter(supportFragmentManager, null)\n        pager?.adapter = adapter\n        check_view.setCountable(spec?.isCountable() == true)\n        updateApplyButton()\n    }\n\n    override fun initListener() {\n        setOnClickListener(this, button_preview, button_apply, check_view, original_layout)\n        pager?.addOnPageChangeListener(this)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        selectedCollection.onSaveInstanceState(outState)\n        outState.putBoolean(ConstValue.CHECK_STATE, originalEnable)\n        super.onSaveInstanceState(outState)\n    }\n\n    override fun onBackPressed() {\n        finishIntentFromPreviewApply(activity, false, selectedCollection, originalEnable)\n        super.onBackPressed()\n    }\n\n    private fun updateApplyButton() {\n        val selectedCount = selectedCollection.count()\n\n        setApplyText(selectedCount)\n\n        if (spec?.originalable == true) {\n            setViewVisible(true, original_layout)\n            updateOriginalState()\n        } else {\n            setViewVisible(false, original_layout)\n        }\n    }\n\n    private fun setApplyText(selectedCount: Int) {\n        button_apply.apply {\n            when (selectedCount) {\n                0 -> {\n                    text = getString(\n                        getAttrString(R.attr.Preview_Confirm_text, R.string.button_sure_default)\n                    )\n                    isEnabled = false\n                }\n                1 -> {\n                    isEnabled = true\n\n                    text = if (spec?.singleSelectionModeEnabled() == true) {\n                        getString(R.string.button_sure_default)\n                    } else {\n                        getString(\n                            getAttrString(\n                                R.attr.Preview_Confirm_text, R.string.button_sure_default\n                            )\n                        ).plus(\"(\").plus(selectedCount.toString()).plus(\")\")\n                    }\n                }\n                else -> {\n                    isEnabled = true\n                    text = getString(\n                        getAttrString(R.attr.Preview_Confirm_text, R.string.button_sure_default),\n                        \"($selectedCount)\"\n                    )\n                }\n            }\n        }\n    }\n\n    private fun updateOriginalState() {\n        original?.setChecked(originalEnable)\n        if (countOverMaxSize(selectedCollection) > 0 || originalEnable) {\n            handleCauseTips(\n                getString(R.string.error_over_original_size, spec?.originalMaxSize),\n                IncapableCause.DIALOG\n            )\n            original?.setChecked(false)\n            originalEnable = false\n        }\n    }\n\n    override fun onPageScrollStateChanged(state: Int) {\n    }\n\n    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {\n    }\n\n    override fun onPageSelected(position: Int) {\n        val adapter = pager?.adapter as PreviewPagerAdapter\n\n        check_view.apply {\n            if (previousPos != -1 && previousPos != position) {\n                (adapter.instantiateItem(\n                    pager,\n                    previousPos\n                ) as PicturePreviewItemFragment).resetView()\n                val item = adapter.getMediaItem(position)\n                if (spec?.isCountable() == true) {\n                    val checkedNum = selectedCollection.checkedNumOf(item)\n                    setCheckedNum(checkedNum)\n                    if (checkedNum > 0) {\n                        setEnable(true)\n                    } else {\n                        setEnable(!selectedCollection.maxSelectableReached(item))\n                    }\n                } else {\n                    val checked = selectedCollection.isSelected(item)\n                    setChecked(checked)\n                    if (checked)\n                        setEnable(true)\n                    else\n                        setEnable(!selectedCollection.maxSelectableReached(item))\n                }\n                updateSize(item)\n            }\n        }\n\n        previousPos = position\n    }\n\n    fun updateSize(item: Item?) {\n        item?.apply {\n            tv_size.apply {\n                if (isGif()) {\n                    setViewVisible(true, this)\n                    text = String.format(\n                        getString(R.string.picture_size), PhotoMetadataUtils.getSizeInMB(size)\n                    )\n                } else {\n                    setViewVisible(false, this)\n                }\n            }\n\n            original_layout?.apply {\n                if (isVideo()) {\n                    setViewVisible(false, this)\n                } else if (spec?.originalable == true) {\n                    setViewVisible(true, this)\n                }\n            }\n        }\n    }\n\n    override fun onClick(v: View?) {\n        when (v) {\n            button_preview -> onBackPressed()\n            button_apply -> {\n                if (spec?.openCrop() == true) {\n                    val item = selectedCollection.items()[0]\n\n                    if (spec?.isSupportCrop(item) == true) {\n                        item.getContentUri().apply {\n                            gotoImageCrop(this@BasePreviewActivity, arrayListOf(this))\n                        }\n                    } else {\n                        finishIntentFromPreviewApply(\n                            activity, true, selectedCollection, originalEnable\n                        )\n                    }\n                } else {\n                    finishIntentFromPreviewApply(activity, true, selectedCollection, originalEnable)\n                }\n            }\n\n            original_layout -> {\n                val count = countOverMaxSize(selectedCollection)\n                if (count <= 0) {\n                    originalEnable = !originalEnable\n                    original?.setChecked(originalEnable)\n                    spec?.onCheckedListener?.onCheck(originalEnable)\n                    return\n                }\n\n                handleCauseTips(\n                    getString(R.string.error_over_original_count, count, spec?.originalMaxSize),\n                    IncapableCause.DIALOG\n                )\n            }\n\n            check_view -> {\n                val item = adapter?.getMediaItem(pager.currentItem)\n                if (selectedCollection.isSelected(item)) {\n                    selectedCollection.remove(item)\n                    if (spec?.isCountable() == true) {\n                        check_view.setCheckedNum(CheckView.UNCHECKED)\n                    } else {\n                        check_view.setChecked(false)\n                    }\n                } else {\n                    if (assertAddSelection(item)) {\n                        selectedCollection.add(item)\n                        if (spec?.isCountable() == true) {\n                            check_view.setCheckedNum(selectedCollection.checkedNumOf(item))\n                        } else {\n                            check_view.setChecked(true)\n                        }\n                    }\n                }\n\n                updateApplyButton()\n\n                spec?.onSelectedListener?.onSelected(\n                    selectedCollection.asListOfUri(), selectedCollection.asListOfString()\n                )\n            }\n        }\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (resultCode != Activity.RESULT_OK) return\n\n        when (requestCode) {\n            ConstValue.REQUEST_CODE_CROP -> {\n                data?.run {\n                    val resultUri = UCrop.getOutput(data) ?: return@run\n                    finishIntentFromCropSuccess(activity, resultUri)\n                }\n            }\n        }\n    }\n\n    private fun assertAddSelection(item: Item?): Boolean {\n        val cause = selectedCollection.isAcceptable(item)\n        IncapableCause.handleCause(this, cause)\n        return cause == null\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/SelectedPreviewActivity.kt",
    "content": "package com.matisse.ui.activity\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.Item\nimport kotlinx.android.synthetic.main.activity_media_preview.*\n\n/**\n * desc：图片选中预览</br>\n * time: 2019/9/11-14:17</br>\n * author：Leo </br>\n * since V 1.0.0 </br>\n */\nclass SelectedPreviewActivity : BasePreviewActivity() {\n\n    companion object {\n        fun instance(context: Context, bundle: Bundle, mOriginalEnable: Boolean) {\n            val intent = Intent(context, SelectedPreviewActivity::class.java)\n            intent.putExtra(ConstValue.EXTRA_DEFAULT_BUNDLE, bundle)\n                .putExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable)\n            (context as Activity).startActivityForResult(intent, ConstValue.REQUEST_CODE_PREVIEW)\n        }\n    }\n\n    override fun setViewData() {\n        super.setViewData()\n        val bundle = intent.getBundleExtra(ConstValue.EXTRA_DEFAULT_BUNDLE)\n        val selected = bundle?.getParcelableArrayList<Item>(ConstValue.STATE_SELECTION)\n        selected?.apply {\n            adapter?.addAll(this)\n            adapter?.notifyDataSetChanged()\n            check_view?.apply {\n                if (spec?.isCountable() == true) {\n                    setCheckedNum(1)\n                } else {\n                    setChecked(true)\n                }\n            }\n            previousPos = 0\n            updateSize(this[0])\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/matisse/AlbumFolderSheetHelper.kt",
    "content": "package com.matisse.ui.activity.matisse\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Environment\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.ui.view.FolderBottomSheet\n\nclass AlbumFolderSheetHelper(\n    private var context: Context, private var sheetCallback: FolderBottomSheet.BottomSheetCallback\n) {\n    private var albumFolderCursor: Cursor? = null\n    private var albumFolderList: ArrayList<Album>? = null\n    private var bottomSheet: FolderBottomSheet? = null\n    private var lastFolderCheckedPosition = 0\n\n    fun createFolderSheetDialog() {\n        bottomSheet = FolderBottomSheet.instance(\n            context, lastFolderCheckedPosition, \"Folder\"\n        )\n\n        bottomSheet?.callback = sheetCallback\n    }\n\n    fun readAlbumFromCursor(): ArrayList<Album>? {\n        if (albumFolderList?.size ?: 0 > 0) return albumFolderList\n\n        if (albumFolderCursor == null) return null\n\n        var allFolderCoverPath: Uri? = null\n        var allFolderCount = 0L\n        if (albumFolderList == null) {\n            albumFolderList = arrayListOf()\n        }\n\n        albumFolderCursor?.moveToFirst()\n        while (albumFolderCursor!!.moveToNext()) {\n            val album = Album.valueOf(albumFolderCursor!!)\n            if (albumFolderList?.size == 0) {\n                allFolderCoverPath = album.getCoverPath()\n            }\n            albumFolderList?.add(album)\n            allFolderCount += album.getCount()\n        }\n        albumFolderList?.add(\n            0, Album(allFolderCoverPath, context.getString(R.string.album_name_all), allFolderCount)\n        )\n        return albumFolderList\n    }\n\n    fun insetAlbumToFolder(capturePath: Uri) {\n        readAlbumFromCursor()\n\n        albumFolderList?.apply {\n            // 全部相册需添加一张\n            this[0].addCaptureCount()\n            this[0].setCoverPath(capturePath)\n\n            /**\n             * 拍照后图片保存在Pictures目录下\n             * Pictures为空时，需手动创建\n             */\n            // TODO 2019/10/28 Leo 查询相册下图片需指定id，无法手动生成\n//            val listDCIM: List<Album>? =\n//                filter { Environment.DIRECTORY_PICTURES == it.getDisplayName(context) }\n//            if (listDCIM == null || listDCIM.isEmpty()) {\n//                albumFolderList?.add(Album(Environment.DIRECTORY_PICTURES, 0))\n//            }\n\n            // Pictures目录手动添加一张图片\n            filter { Environment.DIRECTORY_PICTURES == it.getDisplayName(context) }.forEach {\n                it.addCaptureCount()\n                it.setCoverPath(capturePath)\n            }\n        }\n    }\n\n    /**\n     * 记录上次选中位置\n     * @return true=记录成功   false=记录失败\n     */\n    fun setLastFolderCheckedPosition(lastPosition: Int): Boolean {\n        if (lastFolderCheckedPosition == lastPosition) return false\n        lastFolderCheckedPosition = lastPosition\n        return true\n    }\n\n    fun setAlbumFolderCursor(cursor: Cursor) {\n        albumFolderCursor = cursor\n        readAlbumFromCursor()\n    }\n\n    fun getAlbumFolderList() = albumFolderList\n\n    fun clearFolderSheetDialog() {\n        if (bottomSheet != null && bottomSheet?.adapter != null) {\n            albumFolderCursor = null\n            bottomSheet?.adapter?.setListData(null)\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/matisse/AlbumLoadHelper.kt",
    "content": "package com.matisse.ui.activity.matisse\n\nimport android.os.Bundle\nimport com.matisse.model.AlbumCallbacks\nimport com.matisse.model.AlbumCollection\n\nclass AlbumLoadHelper(\n    private var activity: MatisseActivity, private var albumLoadCallback: AlbumCallbacks\n) {\n\n    private var albumCollection: AlbumCollection? = null\n\n    init {\n        albumCollection = AlbumCollection()\n        loadAlbumData()\n    }\n\n    fun loadAlbumData() {\n        albumCollection?.apply {\n            onCreate(activity, albumLoadCallback)\n            activity.instanceState?.apply {\n                albumCollection?.onRestoreInstanceState(this)\n            }\n            loadAlbums()\n        }\n    }\n\n    fun onSaveInstanceState(outState: Bundle) {\n        albumCollection?.onSaveInstanceState(outState)\n    }\n\n    /**\n     * 设置当前选中位置，用于数据回收后恢复\n     */\n    fun setStateCurrentSelection(position: Int) {\n        albumCollection?.setStateCurrentSelection(position)\n    }\n\n    fun onDestroy() {\n        albumCollection?.onDestroy()\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/matisse/IAlbumLoad.kt",
    "content": "package com.matisse.ui.activity.matisse\n\nimport android.database.Cursor\n\ninterface IAlbumLoad {\n\n    /**\n     * 相册查询完成回调\n     */\n    fun onAlbumLoad(cursor: Cursor)\n\n    /**\n     *\n     */\n    fun onAlbumReset()\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/activity/matisse/MatisseActivity.kt",
    "content": "package com.matisse.ui.activity.matisse\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.database.Cursor\nimport android.media.MediaScannerConnection\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport android.os.Parcelable\nimport android.view.View\nimport com.matisse.Matisse\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.model.AlbumCallbacks\nimport com.matisse.model.SelectedItemCollection\nimport com.matisse.ucrop.UCrop\nimport com.matisse.ucrop.UCropMulti\nimport com.matisse.ui.activity.AlbumPreviewActivity\nimport com.matisse.ui.activity.BaseActivity\nimport com.matisse.ui.activity.SelectedPreviewActivity\nimport com.matisse.ui.adapter.AlbumMediaAdapter\nimport com.matisse.ui.adapter.FolderItemMediaAdapter\nimport com.matisse.ui.view.FolderBottomSheet\nimport com.matisse.ui.view.MediaSelectionFragment\nimport com.matisse.utils.*\nimport kotlinx.android.synthetic.main.activity_matisse.*\nimport kotlinx.android.synthetic.main.include_view_bottom.*\nimport kotlinx.android.synthetic.main.include_view_navigation.*\n\n/**\n * desc：入口</br>\n * time: 2019/9/11-14:17</br>\n * author：Leo </br>\n * since V 1.0.0 </br>\n */\nclass MatisseActivity : BaseActivity(),\n    MediaSelectionFragment.SelectionProvider,\n    AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener,\n    AlbumMediaAdapter.OnPhotoCapture, View.OnClickListener {\n\n    private var mediaStoreCompat: MediaStoreCompat? = null\n    private var originalEnable = false\n    private var allAlbum: Album? = null\n    private var albumLoadHelper: AlbumLoadHelper? = null\n    private lateinit var selectedCollection: SelectedItemCollection\n    private lateinit var albumFolderSheetHelper: AlbumFolderSheetHelper\n\n\n    override fun configActivity() {\n        super.configActivity()\n        spec?.statusBarFuture?.invoke(this, toolbar)\n\n        if (spec?.capture == true) {\n            mediaStoreCompat = MediaStoreCompat(this)\n            if (spec?.captureStrategy == null)\n                throw RuntimeException(\"Don't forget to set CaptureStrategy.\")\n            mediaStoreCompat?.setCaptureStrategy(spec?.captureStrategy!!)\n        }\n    }\n\n    override fun getResourceLayoutId() = R.layout.activity_matisse\n\n    override fun setViewData() {\n        button_apply.setText(getAttrString(R.attr.Media_Album_text, R.string.album_name_all))\n        selectedCollection = SelectedItemCollection(this).apply { onCreate(instanceState) }\n        albumLoadHelper = AlbumLoadHelper(this, albumCallback)\n        albumFolderSheetHelper = AlbumFolderSheetHelper(this, albumSheetCallback)\n        updateBottomToolbar()\n    }\n\n    override fun initListener() {\n        setOnClickListener(\n            this, button_apply, button_preview,\n            original_layout, button_complete, button_back\n        )\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        selectedCollection.onSaveInstanceState(outState)\n        albumLoadHelper?.onSaveInstanceState(outState)\n        outState.putBoolean(ConstValue.CHECK_STATE, originalEnable)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        albumLoadHelper?.onDestroy()\n        spec?.onCheckedListener = null\n        spec?.onSelectedListener = null\n    }\n\n    override fun onBackPressed() {\n        setResult(Activity.RESULT_CANCELED)\n        super.onBackPressed()\n    }\n\n    override fun onSelectUpdate() {\n        updateBottomToolbar()\n        spec?.onSelectedListener?.onSelected(\n            selectedCollection.asListOfUri(), selectedCollection.asListOfString()\n        )\n    }\n\n    override fun capture() {\n        mediaStoreCompat?.dispatchCaptureIntent(this, ConstValue.REQUEST_CODE_CAPTURE)\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n\n        when (requestCode) {\n            ConstValue.REQUEST_CODE_PREVIEW -> {\n                if (resultCode != Activity.RESULT_OK) return\n                val cropPath = Matisse.obtainCropResult(data)\n\n                // 裁剪带回数据，则认为图片经过裁剪流程\n                if (cropPath != null) finishIntentFromCrop(activity, cropPath)\n                else doActivityResultFromPreview(data)\n            }\n            ConstValue.REQUEST_CODE_CAPTURE -> doActivityResultFromCapture()\n            ConstValue.REQUEST_CODE_CROP -> {\n                data?.run {\n                    val resultUri = UCrop.getOutput(data)\n                    finishIntentFromCrop(activity, resultUri)\n                }\n            }\n            ConstValue.REQUEST_CODE_CROP_ERROR -> {\n                data?.run {\n                    val cropError = UCrop.getError(data)?.message ?: \"\"\n                    IncapableCause.handleCause(activity, IncapableCause(cropError))\n                }\n            }\n        }\n    }\n\n    override fun onClick(v: View?) {\n        when (v) {\n            button_back -> onBackPressed()\n            button_preview -> {\n                if (selectedCollection.count() == 0) {\n                    handleCauseTips(getString(R.string.please_select_media_resource))\n                    return\n                }\n\n                SelectedPreviewActivity.instance(\n                    activity, selectedCollection.getDataWithBundle(), originalEnable\n                )\n            }\n            button_complete -> {\n                if (selectedCollection.count() == 0) {\n                    handleCauseTips(getString(R.string.please_select_media_resource))\n                    return\n                }\n\n                val item = selectedCollection.asList()[0]\n                if (spec?.openCrop() == true && spec?.isSupportCrop(item) == true) {\n                    gotoImageCrop(this, selectedCollection.asListOfUri() as ArrayList<Uri>)\n                    return\n                }\n\n                handleIntentFromPreview(activity, originalEnable, selectedCollection.items())\n            }\n\n            original_layout -> {\n                val count = countOverMaxSize(selectedCollection)\n                if (count <= 0) {\n                    originalEnable = !originalEnable\n                    original.setChecked(originalEnable)\n                    spec?.onCheckedListener?.onCheck(originalEnable)\n                    return\n                }\n\n                handleCauseTips(\n                    getString(R.string.error_over_original_count, count, spec?.originalMaxSize),\n                    IncapableCause.DIALOG\n                )\n            }\n\n            button_apply -> {\n                if (allAlbum?.isAll() == true && allAlbum?.isEmpty() == true) {\n                    handleCauseTips(getString(R.string.empty_album))\n                    return\n                }\n\n                albumFolderSheetHelper.createFolderSheetDialog()\n            }\n        }\n    }\n\n    override fun provideSelectedItemCollection() = selectedCollection\n\n    override fun onMediaClick(album: Album?, item: Item, adapterPosition: Int) {\n        val intent = Intent(this, AlbumPreviewActivity::class.java)\n            .putExtra(ConstValue.EXTRA_ALBUM, album as Parcelable)\n            .putExtra(ConstValue.EXTRA_ITEM, item)\n            .putExtra(ConstValue.EXTRA_DEFAULT_BUNDLE, selectedCollection.getDataWithBundle())\n            .putExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, originalEnable)\n\n        startActivityForResult(intent, ConstValue.REQUEST_CODE_PREVIEW)\n    }\n\n    /**\n     * 处理预览的[onActivityResult]\n     */\n    private fun doActivityResultFromPreview(data: Intent?) {\n        data?.apply {\n\n            originalEnable = getBooleanExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, false)\n            val isApplyData = getBooleanExtra(ConstValue.EXTRA_RESULT_APPLY, false)\n            handlePreviewIntent(activity, data, originalEnable, isApplyData, selectedCollection)\n\n            if (!isApplyData) {\n                val mediaSelectionFragment = supportFragmentManager.findFragmentByTag(\n                    MediaSelectionFragment::class.java.simpleName\n                )\n                if (mediaSelectionFragment is MediaSelectionFragment) {\n                    mediaSelectionFragment.refreshMediaGrid()\n                }\n                updateBottomToolbar()\n            }\n        }\n    }\n\n    /**\n     * 处理拍照的[onActivityResult]\n     */\n    private fun doActivityResultFromCapture() {\n        val capturePathUri = mediaStoreCompat?.getCurrentPhotoUri() ?: return\n        val capturePath = mediaStoreCompat?.getCurrentPhotoPath() ?: return\n        // 刷新系统相册\n        MediaScannerConnection.scanFile(this, arrayOf(capturePath), null, null)\n        // 重新获取相册数据\n        albumLoadHelper?.loadAlbumData()\n        // 手动插入到相册列表\n        albumFolderSheetHelper.insetAlbumToFolder(capturePathUri)\n        // 重新load所有资源\n        albumFolderSheetHelper.getAlbumFolderList()?.apply { onAlbumSelected(this[0]) }\n\n        // Check is Crop first\n        if (spec?.openCrop() == true) {\n            gotoImageCrop(this, arrayListOf(capturePathUri))\n        }\n    }\n\n    private fun updateBottomToolbar() {\n        val selectedCount = selectedCollection.count()\n        setCompleteText(selectedCount)\n\n        if (spec?.originalable == true) {\n            setViewVisible(true, original_layout)\n            updateOriginalState()\n        } else {\n            setViewVisible(false, original_layout)\n        }\n    }\n\n    private fun setCompleteText(selectedCount: Int) {\n        if (selectedCount == 0) {\n            button_complete.setText(getAttrString(R.attr.Media_Sure_text, R.string.button_sure))\n\n        } else if (selectedCount == 1 && spec?.singleSelectionModeEnabled() == true) {\n            button_complete.setText(getAttrString(R.attr.Media_Sure_text, R.string.button_sure))\n\n        } else {\n            button_complete.text =\n                getString(getAttrString(R.attr.Media_Sure_text, R.string.button_sure))\n                    .plus(\"(\").plus(selectedCount.toString()).plus(\")\")\n        }\n    }\n\n    private fun updateOriginalState() {\n        original.setChecked(originalEnable)\n        if (countOverMaxSize(selectedCollection) > 0 || originalEnable) {\n            handleCauseTips(\n                getString(R.string.error_over_original_size, spec?.originalMaxSize),\n                IncapableCause.DIALOG\n            )\n\n            original.setChecked(false)\n            originalEnable = false\n        }\n    }\n\n    private fun onAlbumSelected(album: Album) {\n        if (album.isAll() && album.isEmpty()) {\n            setViewVisible(true, empty_view)\n            setViewVisible(false, container)\n        } else {\n            setViewVisible(false, empty_view)\n            setViewVisible(true, container)\n            val fragment = MediaSelectionFragment.newInstance(album)\n            supportFragmentManager.beginTransaction()\n                .replace(container.id, fragment, MediaSelectionFragment::class.java.simpleName)\n                .commitAllowingStateLoss()\n        }\n    }\n\n    private var albumCallback = object : AlbumCallbacks {\n        override fun onAlbumStart() {\n            // do nothing\n        }\n\n        override fun onAlbumLoad(cursor: Cursor) {\n            albumFolderSheetHelper.setAlbumFolderCursor(cursor)\n\n            Handler(Looper.getMainLooper()).post {\n                if (cursor.moveToFirst()) {\n                    allAlbum = Album.valueOf(cursor).apply { onAlbumSelected(this) }\n                }\n            }\n        }\n\n        override fun onAlbumReset() {\n            albumFolderSheetHelper.clearFolderSheetDialog()\n        }\n    }\n\n    private var albumSheetCallback = object : FolderBottomSheet.BottomSheetCallback {\n        override fun initData(adapter: FolderItemMediaAdapter) {\n            adapter.setListData(albumFolderSheetHelper.readAlbumFromCursor())\n        }\n\n        override fun onItemClick(album: Album, position: Int) {\n            if (!albumFolderSheetHelper.setLastFolderCheckedPosition(position)) return\n            albumLoadHelper?.setStateCurrentSelection(position)\n\n            button_apply.text = album.getDisplayName(activity)\n            onAlbumSelected(album)\n        }\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/adapter/AlbumMediaAdapter.kt",
    "content": "package com.matisse.ui.adapter\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.graphics.drawable.Drawable\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.model.SelectedItemCollection\nimport com.matisse.utils.handleCause\nimport com.matisse.utils.setTextDrawable\nimport com.matisse.widget.CheckView\nimport com.matisse.widget.MediaGrid\n\nclass AlbumMediaAdapter(\n    private var context: Context, private var selectedCollection: SelectedItemCollection,\n    private var recyclerView: RecyclerView\n) : RecyclerViewCursorAdapter<RecyclerView.ViewHolder>(null), MediaGrid.OnMediaGridClickListener {\n\n    private var placeholder: Drawable? = null\n    private var selectionSpec: SelectionSpec = SelectionSpec.getInstance()\n    var checkStateListener: CheckStateListener? = null\n    var onMediaClickListener: OnMediaClickListener? = null\n    private var imageResize = 0\n    private var layoutInflater: LayoutInflater\n\n    init {\n        val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.Item_placeholder))\n        placeholder = ta.getDrawable(0)\n        ta.recycle()\n\n        layoutInflater = LayoutInflater.from(context)\n    }\n\n    companion object {\n        const val VIEW_TYPE_CAPTURE = 0X01\n        const val VIEW_TYPE_MEDIA = 0X02\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {\n        return when (viewType) {\n            VIEW_TYPE_CAPTURE -> {\n                val v = layoutInflater.inflate(R.layout.item_photo_capture, parent, false)\n                CaptureViewHolder(v).run {\n                    itemView.setOnClickListener {\n                        if (it.context is OnPhotoCapture) (it.context as OnPhotoCapture).capture()\n                    }\n                    this\n                }\n            }\n            else -> {\n                val v = layoutInflater.inflate(R.layout.item_media_grid, parent, false)\n                MediaViewHolder(v)\n            }\n        }\n    }\n\n    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, cursor: Cursor, position: Int) {\n        holder.apply {\n            when (this) {\n                is CaptureViewHolder ->\n                    setTextDrawable(itemView.context, hint, R.attr.Media_Camera_textColor)\n                is MediaViewHolder -> {\n                    val item = Item.valueOf(cursor, position)\n                    mediaGrid.preBindMedia(\n                        MediaGrid.PreBindInfo(\n                            getImageResize(mediaGrid.context), placeholder,\n                            selectionSpec.isCountable(), holder\n                        )\n                    )\n                    item?.let {\n                        mediaGrid.bindMedia(it)\n                        mediaGrid.listener = this@AlbumMediaAdapter\n                        setCheckStatus(it, mediaGrid)\n                    }\n                }\n            }\n        }\n    }\n\n    override fun getItemViewType(position: Int, cursor: Cursor) =\n        if (Item.valueOf(cursor)?.isCapture() == true) VIEW_TYPE_CAPTURE else VIEW_TYPE_MEDIA\n\n    private fun getImageResize(context: Context): Int {\n        if (imageResize != 0) return imageResize\n\n        val layoutManager = recyclerView.layoutManager as GridLayoutManager\n        val spanCount = layoutManager.spanCount\n        val screenWidth = context.resources.displayMetrics.widthPixels\n        val availableWidth = screenWidth - context.resources.getDimensionPixelSize(\n            R.dimen.media_grid_spacing\n        ) * (spanCount - 1)\n\n        imageResize = availableWidth / spanCount\n        imageResize = (imageResize * selectionSpec.thumbnailScale).toInt()\n        return imageResize\n    }\n\n    /**\n     * 初始化选择框选中状态\n     */\n    private fun setCheckStatus(item: Item, mediaGrid: MediaGrid) {\n        // 初始化时 添加上次选中的图片\n        setLastChooseItems(item)\n        if (selectionSpec.isCountable()) {\n            val checkedNum = selectedCollection.checkedNumOf(item)\n\n            if (checkedNum > 0) {\n                mediaGrid.setCheckedNum(checkedNum)\n            } else {\n                mediaGrid.setCheckedNum(\n                    if (selectedCollection.maxSelectableReached(item)) CheckView.UNCHECKED else checkedNum\n                )\n            }\n        } else {\n            mediaGrid.setChecked(selectedCollection.isSelected(item))\n        }\n    }\n\n    override fun onThumbnailClicked(\n        thumbnail: ImageView, item: Item, holder: RecyclerView.ViewHolder\n    ) {\n        onMediaClickListener?.onMediaClick(null, item, holder.adapterPosition)\n    }\n\n    /**\n     * 单选：\n     *     a.选中：刷新当前项与上次选择项\n     *     b.取消选中：刷新当前项与上次选择项\n     *\n     * 多选：\n     *      1. 按序号计数\n     *          a.选中：仅刷新选中的item\n     *          b.取消选中：\n     *              取消最后一位：仅刷新当前操作的item\n     *              取消非最后一位：刷新所有选中的item\n     *      2. 无序号计数\n     *          a.选中：仅刷新选中的item\n     *          b.取消选中：仅刷新选中的item\n     */\n    override fun onCheckViewClicked(\n        checkView: CheckView, item: Item, holder: RecyclerView.ViewHolder\n    ) {\n        if (selectionSpec.isSingleChoose()) {\n            notifySingleChooseData(item)\n        } else {\n            notifyMultiChooseData(item)\n        }\n    }\n\n    /**\n     * 单选刷新数据\n     */\n    private fun notifySingleChooseData(item: Item) {\n        if (selectedCollection.isSelected(item)) {\n            selectedCollection.remove(item)\n            notifyItemChanged(item.positionInList)\n        } else {\n            notifyLastItem()\n            if (!addItem(item)) return\n            notifyItemChanged(item.positionInList)\n        }\n        notifyCheckStateChanged()\n    }\n\n    private fun notifyLastItem() {\n        val itemLists = selectedCollection.asList()\n        if (itemLists.size > 0) {\n            selectedCollection.remove(itemLists[0])\n            notifyItemChanged(itemLists[0].positionInList)\n        }\n    }\n\n    /**\n     * 多选刷新数据\n     *      1. 按序号计数\n     *          a.选中：仅刷新选中的item\n     *          b.取消选中：\n     *              取消最后一位：仅刷新当前操作的item\n     *              取消非最后一位：刷新所有选中的item\n     *      2. 无序号计数\n     *          a.选中：仅刷新选中的item\n     *          b.取消选中：仅刷新选中的item\n     */\n    private fun notifyMultiChooseData(item: Item) {\n        if (selectionSpec.isCountable()) {\n            if (notifyMultiCountableItem(item)) return\n        } else {\n            if (selectedCollection.isSelected(item)) {\n                selectedCollection.remove(item)\n            } else {\n                if (!addItem(item)) return\n            }\n\n            notifyItemChanged(item.positionInList)\n        }\n\n        notifyCheckStateChanged()\n    }\n\n    /**\n     * @return 是否拦截 true=拦截  false=不拦截\n     */\n    private fun notifyMultiCountableItem(item: Item): Boolean {\n        val checkedNum = selectedCollection.checkedNumOf(item)\n        if (checkedNum == CheckView.UNCHECKED) {\n            if (!addItem(item)) return true\n            notifyItemChanged(item.positionInList)\n        } else {\n            selectedCollection.remove(item)\n            // 取消选中中间序号时，刷新所有选中item\n            if (checkedNum != selectedCollection.count() + 1) {\n                selectedCollection.asList().forEach {\n                    notifyItemChanged(it.positionInList)\n                }\n            }\n            notifyItemChanged(item.positionInList)\n        }\n        return false\n    }\n\n    private fun notifyCheckStateChanged() {\n        checkStateListener?.onSelectUpdate()\n    }\n\n    private fun addItem(item: Item): Boolean {\n        if (!assertAddSelection(context, item)) return false\n        selectedCollection.add(item)\n        return true\n    }\n\n    private fun assertAddSelection(context: Context, item: Item): Boolean {\n        val cause = selectedCollection.isAcceptable(item)\n        handleCause(context, cause)\n        return cause == null\n    }\n\n    /**\n     * 初始化外部传入上次选中的图片\n     */\n    private fun setLastChooseItems(item: Item) {\n        if (selectionSpec.lastChoosePictureIdsOrUris == null) return\n\n        selectionSpec.lastChoosePictureIdsOrUris?.forEachIndexed { index, s ->\n            if (s == item.id.toString() || s == item.getContentUri().toString()) {\n                selectedCollection.add(item)\n                selectionSpec.lastChoosePictureIdsOrUris!![index] = \"\"\n            }\n        }\n    }\n\n    interface CheckStateListener {\n        fun onSelectUpdate()\n    }\n\n    interface OnMediaClickListener {\n        fun onMediaClick(album: Album?, item: Item, adapterPosition: Int)\n    }\n\n    interface OnPhotoCapture {\n        fun capture()\n    }\n\n    class MediaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        var mediaGrid: MediaGrid = itemView as MediaGrid\n    }\n\n    class CaptureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        var hint: TextView = itemView.findViewById(R.id.hint)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/adapter/FolderItemMediaAdapter.kt",
    "content": "package com.matisse.ui.adapter\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.widget.CheckRadioView\n\nclass FolderItemMediaAdapter(var context: Context, var mCurrentPosition: Int) :\n    RecyclerView.Adapter<FolderItemMediaAdapter.FolderViewHolder>() {\n\n    var albumList = arrayListOf<Album>()\n    private var inflater: LayoutInflater\n    var itemClickListener: OnItemClickListener? = null\n    private var placeholder: Drawable?\n\n    init {\n        val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.Item_placeholder))\n        placeholder = ta.getDrawable(0)\n        ta.recycle()\n\n        inflater = LayoutInflater.from(context)\n    }\n\n    override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {\n\n        val album = albumList[position]\n\n        holder.tvBucketName.text = String.format(\n            context.getString(R.string.folder_count),\n            album.getDisplayName(holder.tvBucketName.context), album.getCount()\n        )\n\n        setRbSelectChecked(holder.rbSelected, position == mCurrentPosition)\n\n        // do not need to load animated Gif\n        val mContext = holder.ivBucketCover.context\n        SelectionSpec.getInstance().imageEngine?.loadThumbnail(\n            mContext, mContext.resources.getDimensionPixelSize(R.dimen.media_grid_size),\n            placeholder, holder.ivBucketCover, album.getCoverPath()\n        )\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderViewHolder(\n        parent, inflater.inflate(R.layout.item_album_folder, parent, false)\n    )\n\n    override fun getItemCount() = albumList.size\n\n    fun setListData(list: MutableList<Album>?) {\n        albumList.clear()\n        list?.apply { albumList.addAll(this) }\n        notifyDataSetChanged()\n    }\n\n    inner class FolderViewHolder(private val mParentView: ViewGroup, itemView: View) :\n        RecyclerView.ViewHolder(itemView), View.OnClickListener {\n\n        var tvBucketName: TextView = itemView.findViewById(R.id.tv_bucket_name)\n        var ivBucketCover: ImageView = itemView.findViewById(R.id.iv_bucket_cover)\n        var rbSelected: CheckRadioView = itemView.findViewById(R.id.rb_selected)\n\n        init {\n            itemView.setOnClickListener(this)\n        }\n\n        override fun onClick(v: View) {\n            itemClickListener?.onItemClick(v, layoutPosition)\n            mCurrentPosition = layoutPosition\n            setRadioDisChecked(mParentView)\n            setRbSelectChecked(rbSelected, true)\n        }\n\n        /**\n         * 设置未所有Item为未选中\n         */\n        private fun setRadioDisChecked(parentView: ViewGroup?) {\n            if (parentView == null || parentView.childCount < 1) return\n\n            for (i in 0 until parentView.childCount) {\n                val itemView = parentView.getChildAt(i)\n                val rbSelect: CheckRadioView = itemView.findViewById(R.id.rb_selected)\n                setRbSelectChecked(rbSelect, false)\n            }\n        }\n    }\n\n    private fun setRbSelectChecked(rbSelect: CheckRadioView?, checked: Boolean) {\n        rbSelect?.apply {\n            scaleX = if (checked) 1f else 0f\n            scaleY = if (checked) 1f else 0f\n            setChecked(checked)\n        }\n    }\n\n    interface OnItemClickListener {\n        fun onItemClick(view: View, position: Int)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/adapter/PicturePreviewPagerAdapter.kt",
    "content": "package com.matisse.ui.adapter\n\nimport android.util.SparseArray\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.viewpager.widget.PagerAdapter\nimport com.matisse.R\nimport com.matisse.entity.Item\n\nclass PicturePreviewPagerAdapter(listener: OnPrimaryItemSetListener?) : PagerAdapter() {\n\n    /**\n     * 最大缓存图片数量\n     */\n    private val MAX_CACHE_SIZE = 18\n    /**\n     * 缓存view\n     */\n    private var mCacheView: SparseArray<View>? = null\n\n    fun clear() {\n        if (null != mCacheView) {\n            mCacheView!!.clear()\n            mCacheView = null\n        }\n    }\n\n    var items: ArrayList<Item> = ArrayList()\n    var kListener: OnPrimaryItemSetListener? = null\n\n    init {\n        this.kListener = listener\n    }\n\n    override fun getCount() = items.size\n\n    override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {\n        container.removeView(`object` as View)\n        if (mCacheView?.size() ?: 0 > MAX_CACHE_SIZE) {\n            mCacheView?.remove(position)\n        }\n    }\n\n    override fun isViewFromObject(view: View, `object`: Any) = view == `object`\n\n    override fun instantiateItem(container: ViewGroup, position: Int): View {\n        var contentView = mCacheView?.get(position)\n        if (contentView == null) {\n            contentView = LayoutInflater.from(container.context)\n                .inflate(R.layout.fragment_preview_item, container, false)\n            items[position].run {\n\n            }\n            mCacheView?.put(position, contentView)\n        }\n        container.addView(contentView, 0)\n        return contentView!!\n    }\n\n    override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {\n        super.setPrimaryItem(container, position, `object`)\n        kListener?.onPrimaryItemSet(position)\n    }\n\n    fun getMediaItem(position: Int): Item? {\n        if (count > position) {\n            return items[position]\n        }\n\n        return null\n    }\n\n    fun addAll(items: List<Item>) {\n        this.items.addAll(items)\n    }\n\n    interface OnPrimaryItemSetListener {\n        fun onPrimaryItemSet(position: Int)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/adapter/PreviewPagerAdapter.kt",
    "content": "package com.matisse.ui.adapter\n\nimport android.view.ViewGroup\nimport androidx.fragment.app.FragmentManager\nimport androidx.fragment.app.FragmentStatePagerAdapter\nimport com.matisse.entity.Item\nimport com.matisse.ui.view.PicturePreviewItemFragment\n\n/**\n * Created by liubo on 2018/9/6.\n */\nclass PreviewPagerAdapter(manager: FragmentManager, listener: OnPrimaryItemSetListener?) :\n    FragmentStatePagerAdapter(manager) {\n\n    var items: ArrayList<Item> = ArrayList()\n    var kListener: OnPrimaryItemSetListener? = null\n\n    init {\n        this.kListener = listener\n    }\n\n    override fun getCount() = items.size\n\n    override fun getItem(position: Int) = PicturePreviewItemFragment.newInstance(items[position])\n\n    override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {\n        super.setPrimaryItem(container, position, `object`)\n        kListener?.onPrimaryItemSet(position)\n    }\n\n    fun getMediaItem(position: Int): Item? {\n        if (count > position) {\n            return items[position]\n        }\n\n        return null\n    }\n\n    fun addAll(items: List<Item>) {\n        this.items.addAll(items)\n    }\n\n    interface OnPrimaryItemSetListener {\n        fun onPrimaryItemSet(position: Int)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/adapter/RecyclerViewCursorAdapter.kt",
    "content": "package com.matisse.ui.adapter\n\nimport android.database.Cursor\nimport android.provider.MediaStore\nimport androidx.recyclerview.widget.RecyclerView\n\nabstract class RecyclerViewCursorAdapter<VH : RecyclerView.ViewHolder>(c: Cursor?) :\n    RecyclerView.Adapter<VH>() {\n\n    private var cursor: Cursor? = null\n    private var rowIDColumn = 0\n\n    init {\n        setHasStableIds(true)\n        swapCursor(c)\n    }\n\n    abstract fun onBindViewHolder(holder: VH, cursor: Cursor, position: Int)\n\n    override fun onBindViewHolder(holder: VH, position: Int) {\n        check(isDataValid(cursor)) {\n            \"Cannot bind view holder when cursor is in invalid state.\"\n        }\n\n        check(cursor?.moveToPosition(position)!!) {\n            \"Could not move cursor to position $position when trying to bind view holder\"\n        }\n\n        onBindViewHolder(holder, cursor!!, position)\n    }\n\n    override fun getItemViewType(position: Int): Int {\n        check(cursor?.moveToPosition(position)!!) {\n            \"Could not move cursor to position $position when trying to get item view type.\"\n        }\n        return getItemViewType(position, cursor!!)\n    }\n\n    abstract fun getItemViewType(position: Int, cursor: Cursor): Int\n\n    override fun getItemCount() = if (isDataValid(cursor)) cursor?.count!! else 0\n\n    override fun getItemId(position: Int): Long {\n        check(isDataValid(cursor)) { \"Cannot lookup item id when cursor is in invalid state.\" }\n        check(cursor?.moveToPosition(position)!!) {\n            \"Could not move cursor to position $position when trying to get an item id\"\n        }\n\n        return cursor?.getLong(rowIDColumn) ?: 0\n    }\n\n    fun swapCursor(newCursor: Cursor?) {\n        if (newCursor == cursor) return\n\n        if (newCursor == null) {\n            notifyItemRangeRemoved(0, itemCount)\n            cursor = null\n            rowIDColumn = -1\n        } else {\n            cursor = newCursor\n            rowIDColumn = cursor?.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) ?: 0\n            // notify the observers about the new cursor\n            notifyDataSetChanged()\n        }\n    }\n\n    private fun isDataValid(cursor: Cursor?) = cursor != null && !cursor.isClosed\n\n    fun getCursor() = cursor\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/view/BottomSheetDialogFragment.kt",
    "content": "package com.matisse.ui.view\n\nimport android.os.Bundle\nimport android.util.DisplayMetrics\nimport android.view.*\nimport android.widget.FrameLayout\nimport androidx.appcompat.app.AppCompatDialogFragment\nimport com.google.android.material.bottomsheet.BottomSheetBehavior\nimport com.matisse.R\n\nabstract class BottomSheetDialogFragment : AppCompatDialogFragment() {\n\n    private lateinit var kBehavior: BottomSheetBehavior<*>\n    private var coordinator: ViewGroup? = null\n    private var bottomSheet: FrameLayout? = null\n    private var contentView: View? = null\n    private var defaultHeight = -1\n    private var kCancelable = true\n\n    private var mBottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {\n        override fun onSlide(bottomSheet: View, slideOffset: Float) {\n            // do noting\n        }\n\n        override fun onStateChanged(bottomSheet: View, newState: Int) {\n            if (newState == BottomSheetBehavior.STATE_HIDDEN) dismiss()\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?\n    ): View? {\n        coordinator = inflater.inflate(R.layout.dialog_bottom_sheet, container) as ViewGroup\n        bottomSheet = coordinator?.findViewById(R.id.design_bottom_sheet)\n\n        kBehavior = BottomSheetBehavior.from(bottomSheet)\n        kBehavior.setBottomSheetCallback(mBottomSheetCallback)\n        kBehavior.isHideable = kCancelable\n\n        contentView = getContentView(inflater, coordinator!!)\n        bottomSheet?.addView(contentView)\n\n        if (defaultHeight != -1) {\n            setDefaultHeight(defaultHeight)\n        }\n\n        // 设置 dialog 位于屏幕底部，并且设置出入动画\n        setBottomLayout()\n        setPeekHeight()\n        initBackAction()\n\n        return coordinator\n    }\n\n    fun setDefaultHeight(defaultHeight: Int) {\n        this.defaultHeight = defaultHeight\n        if (bottomSheet != null) {\n            bottomSheet?.layoutParams?.width = -1\n            bottomSheet?.layoutParams?.height = defaultHeight\n        }\n    }\n\n    private fun setPeekHeight() {\n        val dm = DisplayMetrics()\n        //取得窗口属性\n        activity?.windowManager?.defaultDisplay?.getMetrics(dm)\n        //窗口高度\n        val screenHeight = dm.heightPixels\n        kBehavior.peekHeight = screenHeight\n    }\n\n    private fun initBackAction() {\n        dialog?.setOnKeyListener { _, keyCode, event ->\n            if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {\n                backAction()\n            } else false\n        }\n    }\n\n    override fun setCancelable(cancelable: Boolean) {\n        super.setCancelable(cancelable)\n        if (kCancelable != cancelable) {\n            kCancelable = cancelable\n            kBehavior.isHideable = cancelable\n        }\n    }\n\n    private fun setBottomLayout() {\n        dialog?.window?.apply {\n            setBackgroundDrawableResource(R.drawable.transparent)\n            decorView.setPadding(0, 0, 0, 0)\n            attributes.width = WindowManager.LayoutParams.MATCH_PARENT\n            attributes.height = WindowManager.LayoutParams.WRAP_CONTENT\n            // dialog 布局位于底部\n            setGravity(Gravity.BOTTOM)\n            // 设置进出场动画\n            setWindowAnimations(R.style.Animation_Bottom)\n        }\n    }\n\n    abstract fun getContentView(inflater: LayoutInflater, container: ViewGroup): View\n\n    open fun backAction() = false\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/view/FolderBottomSheet.kt",
    "content": "package com.matisse.ui.view\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.FragmentActivity\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.entity.ConstValue\nimport com.matisse.ui.adapter.FolderItemMediaAdapter\nimport com.matisse.utils.getScreenHeight\n\nclass FolderBottomSheet : BottomSheetDialogFragment() {\n\n    private var kParentView: View? = null\n    private lateinit var recyclerView: RecyclerView\n    var adapter: FolderItemMediaAdapter? = null\n    var callback: BottomSheetCallback? = null\n    private var currentPosition = 0\n\n    companion object {\n        fun instance(context: Context, currentPos: Int, tag: String): FolderBottomSheet {\n            val bottomSheet = FolderBottomSheet()\n            val bundle = Bundle()\n            bundle.putInt(ConstValue.FOLDER_CHECK_POSITION, currentPos)\n            bottomSheet.arguments = bundle\n            bottomSheet.show((context as FragmentActivity).supportFragmentManager, tag)\n            return bottomSheet\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        currentPosition = arguments?.getInt(ConstValue.FOLDER_CHECK_POSITION, 0) ?: 0\n    }\n\n    override fun getContentView(inflater: LayoutInflater, container: ViewGroup): View {\n        if (kParentView == null) {\n            kParentView = inflater.inflate(R.layout.dialog_bottom_sheet_folder, container, false)\n            setDefaultHeight(getScreenHeight(context!!) / 2)\n            initView()\n        } else {\n            if (kParentView?.parent != null) {\n                val parent = kParentView?.parent as ViewGroup\n                parent.removeView(view)\n            }\n        }\n        return kParentView!!\n    }\n\n    private fun initView() {\n        recyclerView = kParentView?.findViewById(R.id.recyclerview)!!\n        recyclerView.layoutManager = LinearLayoutManager(context)\n        recyclerView.setHasFixedSize(true)\n        setRecyclerViewHeight()\n        adapter = FolderItemMediaAdapter(context!!, currentPosition).apply {\n            recyclerView.adapter = this\n            callback?.initData(this)\n\n            itemClickListener = object : FolderItemMediaAdapter.OnItemClickListener {\n                override fun onItemClick(view: View, position: Int) {\n                    dismiss()\n                    callback?.onItemClick(albumList[position], position)\n                }\n            }\n        }\n    }\n\n    private fun setRecyclerViewHeight() {\n        recyclerView.layoutParams.height = getScreenHeight(context!!) / 2\n    }\n\n    interface BottomSheetCallback {\n        fun initData(adapter: FolderItemMediaAdapter)\n        /**\n         * 点击回调\n         * @param album 当前选中的相册\n         * @param position 当前选中的位置\n         */\n        fun onItemClick(album: Album, position: Int)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/view/MediaSelectionFragment.kt",
    "content": "package com.matisse.ui.view\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.GridLayoutManager\nimport com.matisse.R\nimport com.matisse.entity.Album\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.model.AlbumCallbacks\nimport com.matisse.model.AlbumMediaCollection\nimport com.matisse.model.SelectedItemCollection\nimport com.matisse.ui.adapter.AlbumMediaAdapter\nimport com.matisse.utils.MAX_SPAN_COUNT\nimport com.matisse.utils.spanCount\nimport com.matisse.widget.MediaGridInset\nimport kotlinx.android.synthetic.main.fragment_media_selection.*\nimport kotlin.math.max\nimport kotlin.math.min\n\nclass MediaSelectionFragment : Fragment(), AlbumCallbacks, AlbumMediaAdapter.CheckStateListener,\n    AlbumMediaAdapter.OnMediaClickListener {\n\n    private val albumMediaCollection = AlbumMediaCollection()\n    private lateinit var adapter: AlbumMediaAdapter\n    private lateinit var album: Album\n    private lateinit var selectionProvider: SelectionProvider\n    private lateinit var checkStateListener: AlbumMediaAdapter.CheckStateListener\n    private lateinit var onMediaClickListener: AlbumMediaAdapter.OnMediaClickListener\n\n    companion object {\n        fun newInstance(album: Album): MediaSelectionFragment {\n            val fragment = MediaSelectionFragment()\n            fragment.arguments = Bundle().apply { putParcelable(ConstValue.EXTRA_ALBUM, album) }\n            return fragment\n        }\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        if (context is SelectionProvider) {\n            selectionProvider = context\n        } else {\n            throw IllegalStateException(\"Context must implement SelectionProvider.\")\n        }\n\n        if (context is AlbumMediaAdapter.CheckStateListener) checkStateListener = context\n\n        if (context is AlbumMediaAdapter.OnMediaClickListener) onMediaClickListener = context\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?\n    ): View = inflater.inflate(R.layout.fragment_media_selection, container, false)\n\n    override fun onActivityCreated(savedInstanceState: Bundle?) {\n        super.onActivityCreated(savedInstanceState)\n        album = arguments?.getParcelable(ConstValue.EXTRA_ALBUM)!!\n        adapter = AlbumMediaAdapter(\n            context!!, selectionProvider.provideSelectedItemCollection(), recyclerview\n        )\n        adapter.checkStateListener = this\n        adapter.onMediaClickListener = this\n        recyclerview.setHasFixedSize(true)\n\n        val selectionSpec = SelectionSpec.getInstance()\n        val spanCount = if (selectionSpec.gridExpectedSize > 0) {\n            spanCount(context!!, selectionSpec.gridExpectedSize)\n        } else {\n            max(min(selectionSpec.spanCount, MAX_SPAN_COUNT), 1)\n        }\n\n        recyclerview.layoutManager = GridLayoutManager(context!!, spanCount)\n        val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing)\n        recyclerview.addItemDecoration(MediaGridInset(spanCount, spacing, false))\n        recyclerview.itemAnimator?.changeDuration = 0\n        recyclerview.adapter = adapter\n        albumMediaCollection.onCreate(activity!!, this)\n        albumMediaCollection.load(album, selectionSpec.capture)\n    }\n\n    fun refreshMediaGrid() {\n        adapter.notifyDataSetChanged()\n    }\n\n    override fun onMediaClick(album: Album?, item: Item, adapterPosition: Int) {\n        onMediaClickListener.onMediaClick(this.album, item, adapterPosition)\n    }\n\n    override fun onSelectUpdate() {\n        checkStateListener.onSelectUpdate()\n    }\n\n    override fun onAlbumStart() {\n        // do nothing\n    }\n\n    override fun onAlbumLoad(cursor: Cursor) {\n        adapter.swapCursor(cursor)\n    }\n\n    override fun onAlbumReset() {\n        adapter.swapCursor(null)\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        albumMediaCollection.onDestroy()\n    }\n\n    interface SelectionProvider {\n        fun provideSelectedItemCollection(): SelectedItemCollection\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/view/PicturePreviewItemFragment.kt",
    "content": "package com.matisse.ui.view\n\nimport android.content.Intent\nimport android.graphics.Point\nimport android.graphics.PointF\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\nimport com.matisse.R\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.photoview.PhotoView\nimport com.matisse.utils.PhotoMetadataUtils\nimport com.matisse.widget.longimage.ImageSource\nimport com.matisse.widget.longimage.ImageViewState\nimport com.matisse.widget.longimage.SubsamplingScaleImageView\nimport it.sephiroth.android.library.imagezoom.ImageViewTouch\n\n/**\n * desc: 预览界面真正载体 </br>\n * time: 2020-03-30-20:05 </br>\n * author: Leo </br>\n * since V 2.1 </br>\n */\nclass PicturePreviewItemFragment : Fragment() {\n\n    companion object {\n        private const val ARGS_ITEM = \"args_item\"\n\n        fun newInstance(item: Item): PicturePreviewItemFragment {\n            val fragment = PicturePreviewItemFragment()\n            val bundle = Bundle()\n            bundle.putParcelable(ARGS_ITEM, item)\n            fragment.arguments = bundle\n            return fragment\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?\n    ): View = inflater.inflate(R.layout.fragment_picture_preview_item, container, false)\n\n    override fun onViewCreated(contentView: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(contentView, savedInstanceState)\n        val media: Item = arguments!!.getParcelable(ARGS_ITEM) ?: return\n\n        val videoPlayButton: View = contentView.findViewById(R.id.video_play_button)\n        if (media.isVideo()) {\n            videoPlayButton.visibility = View.VISIBLE\n            videoPlayButton.setOnClickListener {\n                val intent = Intent(Intent.ACTION_VIEW)\n                intent.setDataAndType(media.getContentUri(), \"video/*\")\n                if (intent.resolveActivity(activity!!.packageManager) != null) startActivity(intent)\n                else Toast.makeText(\n                    context, R.string.error_no_video_activity, Toast.LENGTH_SHORT\n                ).show()\n            }\n        } else {\n            videoPlayButton.visibility = View.GONE\n        }\n\n        // 常规图控件\n        val imageView: PhotoView = contentView.findViewById(R.id.preview_image)\n        // 长图控件\n        val longImg: SubsamplingScaleImageView = contentView.findViewById(R.id.longImg)\n\n        val size: Point = PhotoMetadataUtils.getBitmapSize(media.getContentUri(), activity)\n        // TODO Leo 2020-03-30 长图判断不能这么写，后续完善\n        val isLongImg = PhotoMetadataUtils.isLongImg(size)\n        val isGifImg = media.isGif()\n\n        imageView.visibility = if (isLongImg && !isGifImg) View.GONE else View.VISIBLE\n        longImg.visibility = if (isLongImg && !isGifImg) View.VISIBLE else View.GONE\n\n        if (isGifImg) {\n            SelectionSpec.getInstance().imageEngine?.loadGifImage(\n                context!!, size.x, size.y, imageView, media.getContentUri()\n            )\n        } else {\n            if (isLongImg) {\n                displayLongPic(media.getContentUri(), longImg)\n            } else {\n                SelectionSpec.getInstance().imageEngine?.loadImage(\n                    context!!, size.x, size.y, imageView, media.getContentUri()\n                )\n            }\n        }\n    }\n\n    fun resetView() {\n        val image: ImageViewTouch? = view?.findViewById(R.id.image_view)\n        image?.resetMatrix()\n    }\n\n    /**\n     * 加载长图\n     *\n     * @param uri\n     * @param longImg\n     */\n    private fun displayLongPic(uri: Uri, longImg: SubsamplingScaleImageView) {\n        longImg.isQuickScaleEnabled = true\n        longImg.isZoomEnabled = true\n        longImg.isPanEnabled = true\n        longImg.setDoubleTapZoomDuration(100)\n        longImg.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)\n        longImg.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)\n        longImg.setImage(ImageSource.uri(uri), ImageViewState(0f, PointF(0f, 0f), 0))\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/ui/view/PreviewItemFragment.kt",
    "content": "package com.matisse.ui.view\n\nimport android.content.Intent\nimport android.graphics.Point\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\nimport com.matisse.R\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.PhotoMetadataUtils\nimport it.sephiroth.android.library.imagezoom.ImageViewTouch\nimport it.sephiroth.android.library.imagezoom.ImageViewTouchBase\n\n/**\n * desc：预览界面真正载体</br>\n * time: 2018/9/6-9:40</br>\n * author：Leo </br>\n * since V 1.8.0 </br>\n */\nclass PreviewItemFragment : Fragment() {\n\n    companion object {\n        private const val ARGS_ITEM = \"args_item\"\n\n        fun newInstance(item: Item): PreviewItemFragment {\n            val fragment = PreviewItemFragment()\n            val bundle = Bundle()\n            bundle.putParcelable(ARGS_ITEM, item)\n            fragment.arguments = bundle\n            return fragment\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?\n    ): View = inflater.inflate(R.layout.fragment_preview_item, container, false)\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val item: Item = arguments!!.getParcelable(ARGS_ITEM) ?: return\n\n        val videoPlayButton: View = view.findViewById(R.id.video_play_button)\n        if (item.isVideo()) {\n            videoPlayButton.visibility = View.VISIBLE\n            videoPlayButton.setOnClickListener {\n                val intent = Intent(Intent.ACTION_VIEW)\n                intent.setDataAndType(item.getContentUri(), \"video/*\")\n                if (intent.resolveActivity(activity!!.packageManager) != null) startActivity(intent)\n                else Toast.makeText(\n                    context, R.string.error_no_video_activity, Toast.LENGTH_SHORT\n                ).show()\n            }\n        } else {\n            videoPlayButton.visibility = View.GONE\n        }\n\n        val image: ImageViewTouch = view.findViewById(R.id.image_view)\n        image.displayType = ImageViewTouchBase.DisplayType.FIT_TO_SCREEN\n        val size: Point = PhotoMetadataUtils.getBitmapSize(item.getContentUri(), activity)\n        if (item.isGif()) {\n            SelectionSpec.getInstance().imageEngine?.loadGifImage(\n                context!!, size.x, size.y, image, item.getContentUri()\n            )\n        } else {\n            SelectionSpec.getInstance().imageEngine?.loadImage(\n                context!!, size.x, size.y, image, item.getContentUri()\n            )\n        }\n    }\n\n    fun resetView() {\n        val image: ImageViewTouch? = view?.findViewById(R.id.image_view)\n        image?.resetMatrix()\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/BitmapUtils.kt",
    "content": "@file:JvmName(\"BitmapUtils\")\n\npackage com.matisse.utils\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.media.ExifInterface\nimport android.net.Uri\nimport java.io.IOException\n\n/**\n * 获取图片的旋转角度\n *\n * @param path 图片绝对路径\n * @return 图片的旋转角度\n */\nfun getBitmapDegree(path: String?): Int {\n    if (path == null) return 0\n    var degree = 0\n    try {\n        // 从指定路径下读取图片，并获取其EXIF信息\n        val exifInterface = ExifInterface(path)\n        // 获取图片的旋转信息\n        val orientation = exifInterface.getAttributeInt(\n            ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL\n        )\n        when (orientation) {\n            ExifInterface.ORIENTATION_ROTATE_90 -> degree = 90\n            ExifInterface.ORIENTATION_ROTATE_180 -> degree = 180\n            ExifInterface.ORIENTATION_ROTATE_270 -> degree = 270\n        }\n    } catch (e: IOException) {\n        e.printStackTrace()\n    }\n\n    return degree\n}\n\n// 通过uri加载图片\nfun getBitmapFromUri(context: Context, uri: Uri, opts: BitmapFactory.Options): Bitmap? {\n    return try {\n        // mode：\"r\" 表示只读 \"w\"表示只写\n        val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, \"r\")\n        val fileDescriptor = parcelFileDescriptor?.fileDescriptor\n        val image = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts)\n        parcelFileDescriptor?.close()\n        image\n    } catch (e: Exception) {\n        null\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/ExifInterfaceCompat.kt",
    "content": "package com.matisse.utils\n\nimport android.media.ExifInterface\n\n/**\n * Created by liubo on 2018/9/6.\n */\nobject ExifInterfaceCompat {\n    fun newInstance(fileName: String?): ExifInterface? {\n        if (fileName == null) {\n            throw  NullPointerException(\"filename should not be null\")\n        }\n        return ExifInterface(fileName)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/IntentUtils.kt",
    "content": "@file:JvmName(\"IntentUtils\")\n\npackage com.matisse.utils\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport com.matisse.entity.ConstValue\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.model.SelectedItemCollection\nimport com.matisse.ucrop.UCrop\nimport com.matisse.utils.Platform.aboveAndroidTen\nimport java.io.File\n\n/**\n * 打开裁剪界面\n */\nfun gotoImageCrop(activity: Activity, selectedPath: ArrayList<Uri>?) {\n    if (selectedPath == null || selectedPath.isEmpty()) return\n\n    startCrop(activity, selectedPath[0])\n}\n\n/**\n * 去裁剪\n *\n * @param originalPath\n */\nfun startCrop(activity: Activity, originalPath: Uri) {\n\n    val path = getPath(activity, originalPath) ?: \"\"\n\n    val spec = SelectionSpec.getInstance()\n\n    val options = UCrop.Options()\n        .setCircleDimmedLayer(spec.isCircleCrop)\n        .setDragFrameEnabled(true)\n        .setCompressionQuality(50)\n        .setFreeStyleCropEnabled(true)\n        .setShowCropFrame(true)\n        .setShowCropGrid(!spec.isCircleCrop)\n\n    val isAndroidQ = aboveAndroidTen()\n    val imgType = if (isAndroidQ)\n        getLastImgSuffix(getMimeType(activity, originalPath))\n    else {\n        getLastImgType(path)\n    }\n\n    val file = File(\n        getDiskCacheDir(activity), getCreateFileName(\"IMG_\") + imgType\n    )\n\n    UCrop.of(originalPath, Uri.fromFile(file))\n        .withAspectRatio(1f, 1f)\n        .withOptions(options)\n        .start(activity)\n}\n\n/**\n * 处理预览界面提交返回选中结果\n * @param originalEnable 是否原图\n * @param selectedItems 选中的资源Item\n */\nfun handleIntentFromPreview(\n    activity: Activity, originalEnable: Boolean, selectedItems: List<Item>?\n) {\n    if (selectedItems == null) return\n\n    val selectedUris = arrayListOf<Uri>()\n    val selectedId = arrayListOf<String>()\n    selectedItems.forEach {\n        selectedUris.add(it.getContentUri())\n        selectedId.add(it.id.toString())\n    }\n\n    finishIntentToMain(\n        activity, selectedUris, selectedId, originalEnable\n    )\n}\n\n/**\n * 处理预览界面提交返回选中结果\n * @param selectedUris 选中的资源uri\n * @param selectedId 选中的资源id\n */\nprivate fun finishIntentToMain(\n    activity: Activity, selectedUris: ArrayList<Uri>,\n    selectedId: ArrayList<String>, originalEnable: Boolean\n) {\n    Intent().apply {\n        putParcelableArrayListExtra(ConstValue.EXTRA_RESULT_SELECTION, selectedUris)\n        putStringArrayListExtra(ConstValue.EXTRA_RESULT_SELECTION_ID, selectedId)\n        putExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, originalEnable)\n        activity.setResult(Activity.RESULT_OK, this)\n    }\n    activity.finish()\n}\n\n/**\n * 裁剪完成返回裁剪结果\n * @param cropUri 需裁剪的图片路径\n */\nfun finishIntentFromCrop(activity: Activity, cropUri: Uri?) {\n    cropUri?.run {\n        Intent().apply {\n            putParcelableArrayListExtra(ConstValue.EXTRA_RESULT_SELECTION, arrayListOf(cropUri))\n            activity.setResult(Activity.RESULT_OK, this)\n            activity.finish()\n        }\n    }\n}\n\n/**\n * 预览界面提交或者返回时的Intent\n */\nfun finishIntentFromPreviewApply(\n    activity: Activity, apply: Boolean,\n    selectedCollection: SelectedItemCollection, originalEnable: Boolean\n) {\n    Intent().apply {\n        putExtra(ConstValue.EXTRA_RESULT_BUNDLE, selectedCollection.getDataWithBundle())\n        putExtra(ConstValue.EXTRA_RESULT_APPLY, apply)\n        putExtra(ConstValue.EXTRA_RESULT_ORIGINAL_ENABLE, originalEnable)\n        activity.setResult(Activity.RESULT_OK, this)\n    }\n    if (apply) activity.finish()\n}\n\n/**\n * 裁剪成功带回裁剪结果\n */\nfun finishIntentFromCropSuccess(activity: Activity, cropResultUri: Uri) {\n    Intent().apply {\n        putExtra(ConstValue.EXTRA_RESULT_CROP_BACK_BUNDLE, cropResultUri)\n        activity.setResult(Activity.RESULT_OK, this)\n    }\n    activity.finish()\n}\n\n/**\n * 处理预览返回数据刷新\n * @param isApplyData 正常返回/提交带回 true=提交带回  false=正常返回\n */\nfun handlePreviewIntent(\n    activity: Activity, data: Intent?, originalEnable: Boolean,\n    isApplyData: Boolean, selectedCollection: SelectedItemCollection\n) {\n    data?.apply {\n        val resultBundle = getBundleExtra(ConstValue.EXTRA_RESULT_BUNDLE)\n        resultBundle?.apply {\n            val collectionType = getInt(ConstValue.STATE_COLLECTION_TYPE)\n            val selected: ArrayList<Item>? = getParcelableArrayList(ConstValue.STATE_SELECTION)\n            selected?.apply {\n                if (isApplyData) {\n                    // 从预览界面确认提交过来\n                    handleIntentFromPreview(activity, originalEnable, this)\n                } else {\n                    // 从预览界面返回过来\n                    selectedCollection.overwrite(this, collectionType)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/ItemSelectUtils.kt",
    "content": "@file:JvmName(\"ItemSelectUtils\")\n\npackage com.matisse.utils\n\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.model.SelectedItemCollection\n\n/**\n * 返回选中图片中，超过原图大小上限的图片数量\n * @param selectedCollection 资源选中操作类\n */\nfun countOverMaxSize(selectedCollection: SelectedItemCollection): Int {\n    var count = 0\n    selectedCollection.asList().filter { it.isImage() }.forEach {\n        val size = PhotoMetadataUtils.getSizeInMB(it.size)\n        if (size > SelectionSpec.getInstance().originalMaxSize) count++\n    }\n    return count\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/MediaStoreCompat.kt",
    "content": "package com.matisse.utils\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Build\nimport android.provider.MediaStore\nimport androidx.fragment.app.Fragment\nimport com.matisse.entity.CaptureStrategy\nimport java.lang.ref.WeakReference\n\nclass MediaStoreCompat {\n\n    private var kContext: WeakReference<Activity>\n    private var kFragment: WeakReference<Fragment>?\n    private var captureStrategy: CaptureStrategy? = null\n    private var currentPhotoUri: Uri? = null\n    private var currentPhotoPath: String? = null\n\n    companion object {\n        fun hasCameraFeature(context: Context): Boolean {\n            val pm = context.applicationContext.packageManager\n            return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)\n        }\n    }\n\n    constructor(activity: Activity) : this(activity, null)\n    constructor(activity: Activity, fragment: Fragment?) {\n        kContext = WeakReference(activity)\n        kFragment = if (fragment == null) null else WeakReference(fragment)\n    }\n\n    fun setCaptureStrategy(strategy: CaptureStrategy) {\n        captureStrategy = strategy\n    }\n\n    fun dispatchCaptureIntent(context: Context, requestCode: Int) {\n        val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)\n\n        if (captureIntent.resolveActivity(context.packageManager) != null) {\n            // 创建uri\n            createCurrentPhotoUri()\n\n            captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri)\n            captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n                val resInfoList = context.packageManager\n                    .queryIntentActivities(captureIntent, PackageManager.MATCH_DEFAULT_ONLY)\n                for (resolveInfo in resInfoList) {\n                    val packageName = resolveInfo.activityInfo.packageName\n                    context.grantUriPermission(\n                        packageName, currentPhotoUri,\n                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION\n                    )\n                }\n            }\n\n            if (kFragment != null) {\n                kFragment?.get()?.startActivityForResult(captureIntent, requestCode)\n            } else {\n                kContext.get()?.startActivityForResult(captureIntent, requestCode)\n            }\n\n        }\n    }\n\n    private fun createCurrentPhotoUri() {\n        currentPhotoUri = if (Platform.beforeAndroidTen())\n            createImageFile(\n                kContext.get()!!, captureStrategy?.authority ?: \"\"\n            ) { currentPhotoPath = it }\n        else\n            createImageFileForQ(kContext.get()!!) {\n                currentPhotoPath = getPath(kContext.get(), it)\n            }\n    }\n\n    fun getCurrentPhotoUri() = currentPhotoUri\n\n    fun getCurrentPhotoPath() = currentPhotoPath\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/PathUtils.kt",
    "content": "@file:JvmName(\"PathUtils\")\n\npackage com.matisse.utils\n\nimport android.content.ContentResolver\nimport android.content.ContentUris\nimport android.content.ContentValues\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Environment\nimport android.provider.DocumentsContract\nimport android.provider.MediaStore\nimport android.text.TextUtils\nimport androidx.core.content.FileProvider\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.*\n\n/**\n * Describe : http://stackoverflow.com/a/27271131/4739220\n * Created by Leo on 2018/9/5 on 14:07.\n */\n\nfun getSimpleDateFormat(): String = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.getDefault()).format(Date())\n\nfun getPath(context: Context?, uri: Uri?): String? {\n    if (uri == null || context == null) return \"\"\n\n    // DocumentProvider\n    if (Platform.hasKitKat19() && DocumentsContract.isDocumentUri(context, uri)) {\n        // ExternalStorageProvider\n        if (isExternalStorageDocument(uri)) {\n            val docId = DocumentsContract.getDocumentId(uri)\n            val split = docId.split(\":\")\n            val type = split[0]\n\n            if (\"primary\".equals(type, true)) {\n                return Environment.getExternalStorageDirectory().toString() + \"/\" + split[1]\n            }\n        } else if (isDownloadsDocument(uri)) {\n            val id = DocumentsContract.getDocumentId(uri)\n            val contentUri = ContentUris.withAppendedId(\n                Uri.parse(\"content://downloads/public_downloads\"), java.lang.Long.valueOf(id)\n            )\n\n            return getRealFilePath(context, contentUri, null, null)\n        } else if (isMediaDocument(uri)) {\n            val docId = DocumentsContract.getDocumentId(uri)\n            val split = docId.split(\":\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()\n            val type = split[0]\n\n            var contentUri: Uri? = null\n            when (type) {\n                \"image\" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n                \"video\" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI\n                \"audio\" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI\n            }\n\n            val selection = \"_id=?\"\n            val selectionArgs = arrayOf(split[1])\n\n            return getRealFilePath(context, contentUri, selection, selectionArgs)\n        }\n    } else if (\"content\".equals(uri.scheme, true)) {\n        return getRealFilePath(context, uri, null, null)\n    } else if (\"file\".equals(uri.scheme, true)) { // File\n        return uri.path\n    }\n\n    return null\n}\n\n/**\n * Get the value of the data column for this Uri. This is useful for\n * MediaStore Uris, and other file-based ContentProviders.\n *\n * @param context       The context.\n * @param uri           The Uri to query.\n * @param selection     (Optional) Filter used in the query.\n * @param selectionArgs (Optional) Selection arguments used in the query.\n * @return The value of the _data column, which is typically a file path.\n */\nfun getRealFilePath(\n    context: Context, uri: Uri?,\n    selection: String? = null, selectionArgs: Array<String>? = null\n): String? {\n    if (null == uri) return null\n\n    val scheme = uri.scheme\n    var realPath: String? = \"\"\n\n    when (scheme) {\n        null, ContentResolver.SCHEME_FILE -> realPath = uri.path\n        ContentResolver.SCHEME_CONTENT -> {\n            context.contentResolver.query(\n                uri, arrayOf(MediaStore.Images.ImageColumns.DATA), selection, selectionArgs, null\n            )?.run {\n                if (moveToFirst()) {\n                    val index = getColumnIndex(MediaStore.Images.ImageColumns.DATA)\n                    if (index > -1) realPath = getString(index)\n                }\n                close()\n            }\n        }\n    }\n\n    if (TextUtils.isEmpty(realPath)) {\n        val uriString = uri.toString()\n        val index = uriString.lastIndexOf(\"/\")\n        val imageName = uriString.substring(index)\n        var storageDir = Environment.getExternalStoragePublicDirectory(\n            Environment.DIRECTORY_PICTURES\n        )\n        val file = File(storageDir, imageName)\n        if (file.exists()) {\n            realPath = file.absolutePath\n        } else {\n            storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)\n            realPath = File(storageDir, imageName).absolutePath\n        }\n    }\n    return realPath\n}\n\n/**\n * @param uri The Uri to check.\n * @return Whether the Uri authority is ExternalStorageProvider.\n */\nprivate fun isExternalStorageDocument(uri: Uri): Boolean {\n    return \"com.android.externalstorage.documents\" == uri.authority\n}\n\n/**\n * @param uri The Uri to check.\n * @return Whether the Uri authority is DownloadsProvider.\n */\nprivate fun isDownloadsDocument(uri: Uri): Boolean {\n    return \"com.android.providers.downloads.documents\" == uri.authority\n}\n\n/**\n * @param uri The Uri to check.\n * @return Whether the Uri authority is MediaProvider.\n */\nprivate fun isMediaDocument(uri: Uri): Boolean {\n    return \"com.android.providers.media.documents\" == uri.authority\n}\n\nfun createImageFile(\n    context: Context, authority: String, otherEvent: ((absolutePath: String) -> Unit)? = null\n): Uri {\n    val imageFileName = String.format(\"JPEG_%s.jpg\", getSimpleDateFormat())\n    val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)\n    if (!storageDir.exists()) storageDir.mkdirs()\n    val tempFile = File(storageDir, imageFileName)\n    otherEvent?.invoke(tempFile.absolutePath)\n    return FileProvider.getUriForFile(context, authority, tempFile)\n}\n\nfun createImageFileForQ(\n    context: Context, otherEvent: ((uri: Uri?) -> Unit)? = null\n): Uri? {\n    val imageFileName = String.format(\"JPEG_%s.jpg\", getSimpleDateFormat())\n\n    val resolver = context.contentResolver\n    val contentValues = ContentValues().apply {\n        put(MediaStore.MediaColumns.DISPLAY_NAME, imageFileName)\n        put(MediaStore.Images.Media.MIME_TYPE, \"image/jpeg\")\n        put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)\n    }\n\n    val uri = resolver?.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)\n    otherEvent?.invoke(uri)\n    return uri\n}\n\n/**\n * 根据时间戳创建文件名\n *\n * @param prefix 前缀名\n * @return\n */\nfun getCreateFileName(prefix: String): String {\n    val millis = System.currentTimeMillis()\n    return prefix + SimpleDateFormat(\"yyyyMMdd_HHmmssSS\").format(millis)\n}\n\n/**\n * @param ctx\n * @return\n */\nfun getDiskCacheDir(ctx: Context): String {\n    return ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.path\n}\n\n/**\n * 获取图片后缀\n *\n * @param path\n * @return\n */\nfun getLastImgType(path: String): String {\n    try {\n        val index = path.lastIndexOf(\".\")\n        if (index > 0) {\n            val imageType = path.substring(index)\n            when (imageType) {\n                \".png\", \".PNG\", \".jpg\", \".jpeg\", \".JPEG\", \".WEBP\", \".bmp\", \".BMP\", \".webp\", \".gif\", \".GIF\" -> return imageType\n                else -> return \".png\"\n            }\n        } else {\n            return \".png\"\n        }\n    } catch (e: Exception) {\n        e.printStackTrace()\n        return \".png\"\n    }\n\n}\n\n/**\n * 获取图片后缀\n *\n * @param mineType\n * @return\n */\nfun getLastImgSuffix(mineType: String): String {\n    val defaultSuffix = \".png\"\n    try {\n        val index = mineType.lastIndexOf(\"/\") + 1\n        if (index > 0) {\n            return \".\" + mineType.substring(index)\n        }\n    } catch (e: Exception) {\n        e.printStackTrace()\n        return defaultSuffix\n    }\n\n    return defaultSuffix\n}\n\n/**\n * 根据uri获取MIME_TYPE\n *\n * @param uri\n * @return\n */\nfun getMimeType(context: Context, uri: Uri): String {\n    if (ContentResolver.SCHEME_CONTENT == uri.scheme) {\n        val cursor = context.applicationContext.contentResolver.query(\n            uri,\n            arrayOf(MediaStore.Files.FileColumns.MIME_TYPE), null, null, null\n        )\n        if (cursor != null) {\n            if (cursor.moveToFirst()) {\n                val columnIndex =\n                    cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE)\n                if (columnIndex > -1) {\n                    return cursor.getString(columnIndex)\n                }\n            }\n            cursor.close()\n        }\n    }\n    return \"image/jpeg\"\n}\n\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/PhotoMetadataUtils.kt",
    "content": "package com.matisse.utils\n\nimport android.app.Activity\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.database.Cursor\nimport android.graphics.BitmapFactory\nimport android.graphics.Point\nimport android.media.ExifInterface\nimport android.net.Uri\nimport android.provider.MediaStore\nimport android.util.DisplayMetrics\nimport com.matisse.MimeTypeManager\nimport com.matisse.R\nimport com.matisse.entity.IncapableCause\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport java.io.FileNotFoundException\nimport java.io.IOException\nimport java.io.InputStream\nimport java.text.DecimalFormat\nimport java.text.NumberFormat\nimport java.util.*\nimport kotlin.math.log10\nimport kotlin.math.pow\n\n/**\n * Created by Leo on 2018/8/29 on 15:24.\n */\nobject PhotoMetadataUtils {\n\n    private const val MAX_WIDTH = 1600\n\n    /**\n     * 遍历外部自定义过滤器\n     */\n    fun isAcceptable(context: Context, item: Item?): IncapableCause? {\n        if (!isSelectableType(context, item))\n            return IncapableCause(context.getString(R.string.error_file_type))\n\n        if (SelectionSpec.getInstance().filters != null) {\n            SelectionSpec.getInstance().filters?.forEach {\n                return it.filter(context, item)\n            }\n        }\n        return null\n    }\n\n    private fun isSelectableType(context: Context?, item: Item?): Boolean {\n        val mimeTypeSet = SelectionSpec.getInstance().mimeTypeSet\n\n        if (context == null || mimeTypeSet == null) return false\n\n        for (type in mimeTypeSet) {\n            if (MimeTypeManager.checkType(context, item?.getContentUri(), type.getValue())) {\n                return true\n            }\n        }\n        return false\n    }\n\n    fun getSizeInMB(sizeInBytes: Long): Float {\n        val df = NumberFormat.getNumberInstance(Locale.US) as DecimalFormat\n        df.applyPattern(\"0.0\")\n        var result = df.format((sizeInBytes.toFloat() / 1024f / 1024f).toDouble())\n        result = result.replace(\",\".toRegex(), \".\") // in some case , 0.0 will be 0,0\n        return java.lang.Float.valueOf(result)\n    }\n\n    fun getReadableFileSize(size: Long): String {\n        if (size <= 0) return \"0\"\n\n        val units = arrayOf(\"B\", \"KB\", \"MB\", \"GB\", \"TB\")\n        val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()\n        return DecimalFormat(\"#,##0.#\")\n            .format(size / 1024.0.pow(digitGroups.toDouble())) + \" \" + units[digitGroups]\n    }\n\n    fun getBitmapSize(uri: Uri?, activity: Activity?): Point {\n        val resolver = activity!!.contentResolver\n        val imageSize = getBitmapBounds(resolver, uri!!)\n        var w = imageSize.x\n        var h = imageSize.y\n        if (shouldRotate(activity, uri)) {\n            w = imageSize.y\n            h = imageSize.x\n        }\n        if (h == 0) return Point(MAX_WIDTH, MAX_WIDTH)\n        val metrics = DisplayMetrics()\n        activity.windowManager.defaultDisplay.getMetrics(metrics)\n        val screenWidth = metrics.widthPixels\n        val screenHeight = metrics.heightPixels\n        val widthScale = screenWidth / w\n        val heightScale = screenHeight / h\n        if (widthScale > heightScale) return Point((w * widthScale), (h * heightScale))\n\n        return Point((w * widthScale), (h * heightScale))\n    }\n\n    private fun shouldRotate(context: Context, uri: Uri): Boolean {\n        val exif: ExifInterface?\n        try {\n            exif = ExifInterfaceCompat.newInstance(getPath(context, uri))\n        } catch (e: IOException) {\n            return false\n        }\n        val orientation = exif!!.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)\n        return orientation == ExifInterface.ORIENTATION_ROTATE_90\n                || orientation == ExifInterface.ORIENTATION_ROTATE_270\n    }\n\n    private fun getBitmapBounds(resolver: ContentResolver?, uri: Uri): Point {\n        var inStream: InputStream? = null\n        try {\n            val options = BitmapFactory.Options()\n            options.inJustDecodeBounds = true\n            inStream = resolver!!.openInputStream(uri)\n            BitmapFactory.decodeStream(inStream, null, options)\n            val width = options.outWidth\n            val height = options.outHeight\n            return Point(width, height)\n        } catch (e: FileNotFoundException) {\n            return Point(0, 0)\n        } finally {\n            if (inStream != null) {\n                try {\n                    inStream.close()\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n            }\n        }\n    }\n\n    /**\n     * 是否是长图\n     *\n     * @param size\n     * @return true 是 or false 不是\n     */\n    fun isLongImg(size: Point?): Boolean {\n        if (null != size) {\n            val width = size.x\n            val height = size.y\n            val h = width * 3\n            return height > h\n        }\n        return false\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/Platform.kt",
    "content": "package com.matisse.utils\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.os.Build\n\n/**\n * Created by Leo on 2018/9/5 on 14:09.\n */\nobject Platform {\n\n    fun hasKitKat19() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT\n\n    fun hasKitO26() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O\n\n    fun beforeAndroidTen() = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q\n\n    fun aboveAndroidTen() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q\n\n    fun getPackageName(context: Context?): String? {\n        if (context == null) return \"\"\n\n        val manager = context.packageManager\n\n        try {\n            val info = manager.getPackageInfo(context.packageName, 0)\n            return info.packageName\n        } catch (e: PackageManager.NameNotFoundException) {\n            e.printStackTrace()\n        }\n\n        return \"\"\n    }\n\n    fun isClassExists(classFullName: String): Boolean {\n        return try {\n            Class.forName(classFullName)\n            true\n        } catch (e: ClassNotFoundException) {\n            false\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/utils/UIUtils.kt",
    "content": "@file:JvmName(\"UIUtils\")\n\npackage com.matisse.utils\n\nimport android.content.Context\nimport android.content.res.Resources\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffColorFilter\nimport android.util.DisplayMetrics\nimport android.util.TypedValue\nimport android.util.TypedValue.applyDimension\nimport android.view.View\nimport android.view.WindowManager\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.fragment.app.FragmentActivity\nimport com.matisse.R\nimport com.matisse.entity.IncapableCause\nimport com.matisse.widget.IncapableDialog\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\nconst val MIN_GRID_WIDTH = 200              // min width of media grid\nconst val MAX_SPAN_COUNT = 6                // max span of media grid\n\nfun handleCause(context: Context, cause: IncapableCause?) {\n    if (cause?.noticeEvent != null) {\n        cause.noticeEvent?.invoke(\n            context, cause.form, cause.title ?: \"\", cause.message ?: \"\"\n        )\n        return\n    }\n\n    when (cause?.form) {\n        IncapableCause.DIALOG -> {\n            val incapableDialog = IncapableDialog.newInstance(cause.title, cause.message)\n            incapableDialog.show(\n                (context as FragmentActivity).supportFragmentManager,\n                IncapableDialog::class.java.name\n            )\n        }\n\n        IncapableCause.TOAST -> {\n            Toast.makeText(context, cause.message, Toast.LENGTH_SHORT).show()\n        }\n    }\n}\n\nfun spanCount(context: Context, gridExpectedSize: Int): Int {\n    if (gridExpectedSize < MIN_GRID_WIDTH) {\n        return MAX_SPAN_COUNT\n    }\n\n    val screenWidth = context.resources.displayMetrics.widthPixels\n    val expected = screenWidth / gridExpectedSize\n    var spanCount = expected.toFloat().roundToInt()\n    spanCount = min(spanCount, MAX_SPAN_COUNT)\n    if (spanCount == 0) spanCount = 1\n\n    return spanCount\n}\n\nfun setTextDrawable(context: Context, textView: TextView?, attr: Int) {\n    if (textView == null) return\n\n    val drawables = textView.compoundDrawables\n    val ta = context.theme.obtainStyledAttributes(intArrayOf(attr))\n    val color = ta.getColor(0, 0)\n    ta.recycle()\n\n    for (i in drawables.indices) {\n        val drawable = drawables[i]\n        if (drawable != null) {\n            val state = drawable.constantState ?: continue\n\n            drawables[i] = state.newDrawable().mutate().apply {\n                colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)\n                bounds = drawable.bounds\n            }\n        }\n    }\n\n    textView.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])\n}\n\n/**\n * 根据attr获取外部文字资源\n */\nfun obtainAttrString(context: Context, attr: Int, defaultRes: Int = R.string.button_null): Int {\n    val ta = context.theme.obtainStyledAttributes(intArrayOf(attr)) ?: return defaultRes\n    val stringRes = ta.getResourceId(0, defaultRes)\n    ta.recycle()\n\n    return stringRes\n}\n\n/**\n * 设置控件显示隐藏\n * 避免控件重复设置，统一提前添加判断\n *\n * @param isVisible true visible\n * @param view      targetView\n */\nfun setViewVisible(isVisible: Boolean, view: View?) {\n    if (view == null) return\n    val visibleFlag = if (isVisible) View.VISIBLE else View.GONE\n\n    if (view.visibility != visibleFlag) {\n        view.visibility = visibleFlag\n    }\n}\n\nfun dp2px(context: Context, dipValue: Float): Float {\n    val mDisplayMetrics = getDisplayMetrics(context)\n    return applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, mDisplayMetrics)\n}\n\n/**\n * 获取屏幕尺寸与密度.\n * @param context the context\n * @return mDisplayMetrics\n */\nprivate fun getDisplayMetrics(context: Context?): DisplayMetrics {\n    val mResources: Resources = if (context == null) {\n        Resources.getSystem()\n    } else {\n        context.resources\n    }\n    return mResources.displayMetrics\n}\n\n/**\n * 获取屏幕的宽度px\n *\n * @param context 上下文\n * @return 屏幕宽px\n */\nfun getScreenWidth(context: Context): Int {\n    val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager\n    val outMetrics = DisplayMetrics()// 创建了一张白纸\n    windowManager.defaultDisplay.getMetrics(outMetrics)// 给白纸设置宽高\n    return outMetrics.widthPixels\n}\n\n/**\n * 获取屏幕的高度px\n * @param context 上下文\n * @return 屏幕高px\n */\nfun getScreenHeight(context: Context): Int {\n    val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager\n    val outMetrics = DisplayMetrics()// 创建了一张白纸\n    windowManager.defaultDisplay.getMetrics(outMetrics)// 给白纸设置宽高\n    return outMetrics.heightPixels\n}\n\nfun setOnClickListener(clickListener: View.OnClickListener, vararg view: View) {\n    view.forEach {\n        it.setOnClickListener(clickListener)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/CheckRadioView.kt",
    "content": "package com.matisse.widget\n\nimport android.content.Context\nimport android.content.res.TypedArray\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffColorFilter\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport androidx.appcompat.widget.AppCompatImageView\nimport androidx.core.content.res.ResourcesCompat\nimport com.matisse.R\n\nclass CheckRadioView : AppCompatImageView {\n\n    private var mDrawable: Drawable? = null\n    private lateinit var selectedColorFilter: PorterDuffColorFilter\n    private lateinit var unSelectUdColorFilter: PorterDuffColorFilter\n\n\n    constructor(context: Context) : this(context, null)\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {\n        init()\n    }\n\n    private fun init() {\n        val ta: TypedArray = context?.theme\n            ?.obtainStyledAttributes(intArrayOf(R.attr.Item_checkRadio)) ?: return\n        val selectedColor = ta.getColor(\n            0, ResourcesCompat.getColor(\n                resources, R.color.selector_base_text, context.theme\n            )\n        )\n        val unSelectUdColor = ResourcesCompat.getColor(\n            resources, R.color.check_original_radio_disable, context.theme\n        )\n        ta.recycle()\n\n        selectedColorFilter = PorterDuffColorFilter(selectedColor, PorterDuff.Mode.SRC_IN)\n        unSelectUdColorFilter = PorterDuffColorFilter(unSelectUdColor, PorterDuff.Mode.SRC_IN)\n        setChecked(false)\n    }\n\n    fun setChecked(enable: Boolean) {\n        if (enable) {\n            setImageResource(R.drawable.ic_preview_radio_on)\n            mDrawable = drawable\n            mDrawable?.colorFilter = selectedColorFilter\n        } else {\n            setImageResource(R.drawable.ic_preview_radio_off)\n            mDrawable = drawable\n            mDrawable?.colorFilter = unSelectUdColorFilter\n        }\n    }\n\n    fun setColor(color: Int) {\n        if (mDrawable == null) {\n            mDrawable = drawable\n        }\n        mDrawable?.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/CheckView.kt",
    "content": "package com.matisse.widget\n\nimport android.content.Context\nimport android.content.res.TypedArray\nimport android.graphics.*\nimport android.graphics.drawable.Drawable\nimport android.text.TextPaint\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.res.ResourcesCompat\nimport com.matisse.R\n\n/**\n * Created by liubo on 2018/9/4.\n */\nclass CheckView : View {\n\n    companion object {\n        const val UNCHECKED = Integer.MIN_VALUE\n        private const val STROKE_WIDTH = 3.0f           // 圆环宽度\n        private const val SHADOW_WIDTH = 6.0f           // 阴影宽度\n        private const val SIZE = 30\n        private const val STROKE_RADIUS = 11.5f\n        private const val BG_RADIUS = 11.0f\n        private const val CONTENT_SIZE = 16\n    }\n\n    private var countable = false\n    private var checked = false\n    private var checkedNum = 0\n    private var strokePaint: Paint? = null\n    private var backgroundPaint: Paint? = null\n    private var textPaint: Paint? = null\n    private var shadowPaint: Paint? = null\n    private var checkDrawable: Drawable? = null\n    private var kdensity = 0f\n    private var checkRect: Rect? = null\n    private var enable = true\n    private var halfDensitySize = 0f\n\n    constructor(context: Context?) : this(context, null, 0)\n    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context, attrs, defStyleAttr\n    ) {\n        initParams()\n    }\n\n    private fun initParams() {\n        kdensity = context.resources?.displayMetrics?.density ?: 0f\n        halfDensitySize = kdensity * SIZE / 2f\n\n        strokePaint = Paint().run {\n            isAntiAlias = true\n            style = Paint.Style.STROKE\n            xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)\n            strokeWidth = STROKE_WIDTH * kdensity\n            this\n        }\n\n        val ta: TypedArray =\n            context.theme.obtainStyledAttributes(intArrayOf(R.attr.Item_checkCircle_borderColor))\n        val defaultColor = ResourcesCompat.getColor(\n            context.resources, R.color.item_checkCircle_borderColor, context.theme\n        )\n        val color = ta.getColor(0, defaultColor)\n        ta.recycle()\n        strokePaint?.color = color\n\n        checkDrawable = ResourcesCompat.getDrawable(\n            context.resources, R.drawable.ic_check_white_18dp, context.theme\n        )\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        val sizeSpec = MeasureSpec.makeMeasureSpec((kdensity * SIZE).toInt(), MeasureSpec.EXACTLY)\n        super.onMeasure(sizeSpec, sizeSpec)\n    }\n\n    fun setEnable(enable: Boolean) {\n        if (this.enable != enable) {\n            this.enable = enable\n            invalidate()\n        }\n    }\n\n    fun setCountable(boolean: Boolean) {\n        if (countable != boolean) {\n            countable = boolean\n            invalidate()\n        }\n    }\n\n    fun setChecked(boolean: Boolean) {\n        if (countable) {\n            throw IllegalStateException(\"CheckView is countable, call setCheckedNum() instead.\")\n        }\n\n        checked = boolean\n        invalidate()\n    }\n\n    fun setCheckedNum(num: Int) {\n        if (!countable) {\n            throw IllegalStateException(\"CheckView is not countable, call setChecked() instead.\")\n        }\n        if (num != UNCHECKED && num < 0) {\n            throw  IllegalStateException(\"the num can't be negative\")\n        }\n        checkedNum = num\n        invalidate()\n    }\n\n    override fun onDraw(canvas: Canvas?) {\n        super.onDraw(canvas)\n\n        // draw outer and inner shadow\n        initShadowPaint()\n        shadowPaint?.apply {\n            canvas?.drawCircle(\n                halfDensitySize, halfDensitySize,\n                kdensity.times(STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH), this\n            )\n        }\n\n        // draw white stroke\n        strokePaint?.apply {\n            canvas?.drawCircle(\n                halfDensitySize, halfDensitySize, kdensity.times(STROKE_RADIUS), this\n            )\n        }\n\n        // draw content\n        if (countable) {\n            if (checkedNum != UNCHECKED) {\n                initBackgroundPaint()\n                backgroundPaint?.apply {\n                    canvas?.drawCircle(\n                        halfDensitySize, halfDensitySize, kdensity.times(BG_RADIUS), this\n                    )\n                }\n                initTextPaint()\n                textPaint?.apply {\n                    val text = checkedNum.toString()\n                    val baseX = (width - measureText(text)) / 2\n                    val baseY = (height - descent() - ascent()) / 2\n                    canvas?.drawText(text, baseX, baseY, this)\n                }\n            }\n        } else {\n            if (checked) {\n                initBackgroundPaint()\n                backgroundPaint?.apply {\n                    canvas?.drawCircle(\n                        halfDensitySize, halfDensitySize, BG_RADIUS * kdensity, this\n                    )\n                }\n                if (canvas != null) {\n                    checkDrawable?.bounds = getCheckRect()\n                    checkDrawable?.draw(canvas)\n                }\n            }\n        }\n        alpha = if (enable) 1.0f else 0.5f\n    }\n\n    private fun getCheckRect(): Rect {\n        if (checkRect == null) {\n            val rectPadding = (halfDensitySize - CONTENT_SIZE * kdensity / 2).toInt()\n            checkRect = Rect(\n                rectPadding, rectPadding,\n                (SIZE * kdensity - rectPadding).toInt(), (SIZE * kdensity - rectPadding).toInt()\n            )\n        }\n        return checkRect!!\n    }\n\n    private fun initTextPaint() {\n        if (textPaint == null) {\n            textPaint = TextPaint().run {\n                isAntiAlias = true\n                color = Color.WHITE\n                typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)\n                textSize = 12.0f * kdensity\n                this\n            }\n        }\n    }\n\n    private fun initBackgroundPaint() {\n        if (backgroundPaint == null) {\n            backgroundPaint = Paint()\n            backgroundPaint?.isAntiAlias = true\n            backgroundPaint?.style = Paint.Style.FILL\n            val ta: TypedArray =\n                context.theme.obtainStyledAttributes(intArrayOf(R.attr.Item_checkCircle_bgColor))\n            val defaultColor = ResourcesCompat.getColor(\n                context.resources, R.color.selector_base_text, context.theme\n            )\n            val color = ta.getColor(0, defaultColor)\n            ta.recycle()\n            backgroundPaint?.color = color\n        }\n    }\n\n    private fun initShadowPaint() {\n        if (shadowPaint == null) {\n            shadowPaint = Paint()\n            shadowPaint?.isAntiAlias = true\n            val outerRadius: Float = STROKE_RADIUS + STROKE_WIDTH / 2\n            val innerRadius = outerRadius - STROKE_WIDTH\n            val gradientRadius = outerRadius + SHADOW_WIDTH\n            val stop0 = (innerRadius - STROKE_WIDTH) / gradientRadius\n            val stop1 = innerRadius / gradientRadius\n            val stop2 = outerRadius / gradientRadius\n            val stop3 = 1f\n\n            val shadow = ContextCompat.getColor(context, R.color.shadow)\n            val shadowHint = ContextCompat.getColor(context, R.color.shadow_hint)\n            shadowPaint?.shader = (RadialGradient(\n                halfDensitySize, halfDensitySize, kdensity.times(gradientRadius),\n                intArrayOf(shadowHint, shadow, shadow, shadowHint),\n                floatArrayOf(stop0, stop1, stop2, stop3), Shader.TileMode.CLAMP\n            ))\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/IncapableDialog.kt",
    "content": "package com.matisse.widget\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.DialogFragment\nimport com.matisse.R\n\nclass IncapableDialog : DialogFragment() {\n\n    companion object {\n        private const val EXTRA_TITLE = \"extra_title\"\n        private const val EXTRA_MESSAGE = \"extra_message\"\n\n        fun newInstance(title: String?, message: String?): IncapableDialog {\n            val dialog = IncapableDialog()\n            val bundle = Bundle()\n            bundle.putString(EXTRA_TITLE, title)\n            bundle.putString(EXTRA_MESSAGE, message)\n            dialog.arguments = bundle\n            return dialog\n        }\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog {\n        val title = arguments?.getString(EXTRA_TITLE) ?: \"\"\n        val message = arguments?.getString(EXTRA_MESSAGE) ?: \"\"\n\n        val builder = activity?.let { AlertDialog.Builder(it) }\n        if (title.isNotEmpty()) builder?.setTitle(title)\n\n        if (message.isNotEmpty()) {\n            builder?.setMessage(message)\n        }\n\n        builder?.setPositiveButton(R.string.button_ok) { dialog, _ -> dialog?.dismiss() }\n\n        return builder?.create()!!\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/MediaGrid.kt",
    "content": "package com.matisse.widget\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.text.format.DateUtils\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.ImageView\nimport androidx.recyclerview.widget.RecyclerView\nimport com.matisse.R\nimport com.matisse.entity.Item\nimport com.matisse.internal.entity.SelectionSpec\nimport com.matisse.utils.setViewVisible\nimport kotlinx.android.synthetic.main.view_media_grid_content.view.*\n\nclass MediaGrid : SquareFrameLayout, View.OnClickListener {\n\n    private lateinit var media: Item\n    private lateinit var preBindInfo: PreBindInfo\n    lateinit var listener: OnMediaGridClickListener\n\n    constructor(context: Context?) : this(context, null, 0)\n    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context, attrs, defStyleAttr\n    ) {\n        LayoutInflater.from(context).inflate(R.layout.view_media_grid_content, this, true)\n\n        media_thumbnail.setOnClickListener(this)\n        check_view.setOnClickListener(this)\n    }\n\n    override fun onClick(v: View?) {\n        when (v) {\n            media_thumbnail -> listener.onThumbnailClicked(\n                media_thumbnail, media, preBindInfo.viewHolder\n            )\n            check_view -> listener.onCheckViewClicked(check_view, media, preBindInfo.viewHolder)\n        }\n    }\n\n    fun preBindMedia(info: PreBindInfo) {\n        preBindInfo = info\n    }\n\n    fun bindMedia(item: Item) {\n        media = item\n        setGifTag()\n        initCheckView()\n        setImage()\n        setVideoDuration()\n    }\n\n    private fun setGifTag() {\n        setViewVisible(media.isGif(), gif)\n    }\n\n    private fun initCheckView() {\n        check_view.setCountable(preBindInfo.checkViewCountable)\n    }\n\n    fun setCheckedNum(checkedNum: Int) {\n        check_view.setCheckedNum(checkedNum)\n    }\n\n    fun setChecked(checked: Boolean) {\n        check_view.setChecked(checked)\n    }\n\n    private fun setImage() {\n        if (media.isGif()) {\n            SelectionSpec.getInstance().imageEngine?.loadGifThumbnail(\n                context, preBindInfo.resize, preBindInfo.placeholder,\n                media_thumbnail, media.getContentUri()\n            )\n        } else {\n            SelectionSpec.getInstance().imageEngine?.loadThumbnail(\n                context, preBindInfo.resize, preBindInfo.placeholder,\n                media_thumbnail, media.getContentUri()\n            )\n        }\n    }\n\n    private fun setVideoDuration() {\n        if (media.isVideo()) {\n            setViewVisible(true, video_duration)\n            video_duration.text = DateUtils.formatElapsedTime(media.duration / 1000)\n        } else {\n            setViewVisible(false, video_duration)\n        }\n    }\n\n    interface OnMediaGridClickListener {\n        fun onThumbnailClicked(thumbnail: ImageView, item: Item, holder: RecyclerView.ViewHolder)\n        fun onCheckViewClicked(checkView: CheckView, item: Item, holder: RecyclerView.ViewHolder)\n    }\n\n    class PreBindInfo(\n        var resize: Int, var placeholder: Drawable?,\n        var checkViewCountable: Boolean, var viewHolder: RecyclerView.ViewHolder\n    )\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/MediaGridInset.kt",
    "content": "package com.matisse.widget\n\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\n\nclass MediaGridInset(\n    private var spanCount: Int, private var spacing: Int, private var includeEdge: Boolean\n) : RecyclerView.ItemDecoration() {\n\n    override fun getItemOffsets(\n        outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State\n    ) {\n        val position = parent.getChildAdapterPosition(view)\n        val column = position % spanCount\n\n        outRect.apply {\n            if (includeEdge) {\n                left = spacing - column * spacing / spanCount\n                right = (column + 1) * spacing / spanCount\n\n                if (position < spanCount) top = spacing\n                bottom = spacing\n            } else {\n                left = column * spacing / spanCount\n                right = spacing - (column + 1) * spacing / spanCount\n\n                if (position >= spanCount) top = spacing\n            }\n        }\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/PreviewViewPager.kt",
    "content": "package com.matisse.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.viewpager.widget.ViewPager\nimport it.sephiroth.android.library.imagezoom.ImageViewTouch\n\n/**\n * Created by liubo on 2018/9/10.\n */\nclass PreviewViewPager(context: Context, attributes: AttributeSet) :\n    ViewPager(context, attributes) {\n\n    override fun canScroll(v: View?, checkV: Boolean, dx: Int, x: Int, y: Int): Boolean {\n        if (v is ImageViewTouch) {\n            return v.canScroll(dx) || super.canScroll(v, checkV, dx, x, y)\n        }\n        return super.canScroll(v, checkV, dx, x, y)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/SquareFrameLayout.kt",
    "content": "package com.matisse.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\n\nopen class SquareFrameLayout : FrameLayout {\n    constructor(context: Context?) : this(context, null, 0)\n    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context!!, attrs, defStyleAttr\n    )\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        super.onMeasure(widthMeasureSpec, widthMeasureSpec)\n    }\n}"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/CompatDecoderFactory.java",
    "content": "package com.matisse.widget.longimage;\n\n\nimport androidx.annotation.NonNull;\n\n/**\n * Compatibility factory to instantiate decoders with empty public constructors.\n * @param <T> The base type of the decoder this factory will produce.\n */\npublic class CompatDecoderFactory <T> implements DecoderFactory<T> {\n  private Class<? extends T> clazz;\n\n  public CompatDecoderFactory(@NonNull Class<? extends T> clazz) {\n    this.clazz = clazz;\n  }\n\n  @Override\n  public T make() throws IllegalAccessException, InstantiationException {\n    return clazz.newInstance();\n  }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/DecoderFactory.java",
    "content": "package com.matisse.widget.longimage;\n\n/**\n * Interface for decoder (and region decoder) factories.\n * @param <T> the class of decoder that will be produced.\n */\npublic interface DecoderFactory<T> {\n  /**\n   * Produce a new instance of a decoder with type {@link T}.\n   * @return a new instance of your decoder.\n   */\n  T make() throws IllegalAccessException, InstantiationException;\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/ImageDecoder.java",
    "content": "package com.matisse.widget.longimage;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\n\n/**\n * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder}\n * based on the Skia library to be replaced with a custom class.\n */\npublic interface ImageDecoder {\n\n    /**\n     * Decode an image. When possible, initial setup work once in this method. This method\n     * must return the dimensions of the image. The URI can be in one of the following formats:\n     * File: file:///scard/picture.jpg\n     * Asset: file:///android_asset/picture.png\n     * Resource: android.resource://com.example.app/drawable/picture\n     * @param context Application context. A reference may be held, but must be cleared on recycle.\n     * @param uri URI of the image.\n     * @return Dimensions of the image.\n     * @throws Exception if initialisation fails.\n     */\n    Bitmap decode(Context context, Uri uri) throws Exception;\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/ImageRegionDecoder.java",
    "content": "package com.matisse.widget.longimage;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Point;\nimport android.graphics.Rect;\nimport android.net.Uri;\n\n/**\n * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder}\n * based on the Skia library to be replaced with a custom class.\n */\npublic interface ImageRegionDecoder {\n\n    /**\n     * Initialise the decoder. When possible, initial setup work once in this method. This method\n     * must return the dimensions of the image. The URI can be in one of the following formats:\n     * File: file:///scard/picture.jpg\n     * Asset: file:///android_asset/picture.png\n     * Resource: android.resource://com.example.app/drawable/picture\n     * @param context Application context. A reference may be held, but must be cleared on recycle.\n     * @param uri URI of the image.\n     * @return Dimensions of the image.\n     * @throws Exception if initialisation fails.\n     */\n    Point init(Context context, Uri uri) throws Exception;\n\n    /**\n     * Decode a region of the image with the given sample size. This method is called off the UI thread so it can safely\n     * load the image on the current thread. It is called from an {@link android.os.AsyncTask} running in a single\n     * threaded executor, and while a synchronization lock is held on this object, so will never be called concurrently\n     * even if the decoder implementation supports it.\n     * @param sRect Source image rectangle to decode.\n     * @param sampleSize Sample size.\n     * @return The decoded region. It is safe to return null if decoding fails.\n     */\n    Bitmap decodeRegion(Rect sRect, int sampleSize);\n\n    /**\n     * Status check. Should return false before initialisation and after recycle.\n     * @return true if the decoder is ready to be used.\n     */\n    boolean isReady();\n\n    /**\n     * This method will be called when the decoder is no longer required. It should clean up any resources still in use.\n     */\n    void recycle();\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/ImageSource.java",
    "content": "package com.matisse.widget.longimage;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.net.Uri;\n\nimport java.io.File;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\n\n/**\n * Helper class used to set the source and additional attributes from a variety of sources. Supports\n * use of a bitmap, asset, resource, external file or any other URI.\n *\n * When you are using a preview image, you must set the dimensions of the full size image on the\n * ImageSource object for the full size image using the {@link #dimensions(int, int)} method.\n */\npublic final class ImageSource {\n\n    static final String FILE_SCHEME = \"file:///\";\n    static final String ASSET_SCHEME = \"file:///android_asset/\";\n\n    private final Uri uri;\n    private final Bitmap bitmap;\n    private final Integer resource;\n    private boolean tile;\n    private int sWidth;\n    private int sHeight;\n    private Rect sRegion;\n    private boolean cached;\n\n    private ImageSource(Bitmap bitmap, boolean cached) {\n        this.bitmap = bitmap;\n        this.uri = null;\n        this.resource = null;\n        this.tile = false;\n        this.sWidth = bitmap.getWidth();\n        this.sHeight = bitmap.getHeight();\n        this.cached = cached;\n    }\n\n    private ImageSource(Uri uri) {\n        // #114 If file doesn't exist, attempt to url decode the URI and try again\n        String uriString = uri.toString();\n        if (uriString.startsWith(FILE_SCHEME)) {\n            File uriFile = new File(uriString.substring(FILE_SCHEME.length() - 1));\n            if (!uriFile.exists()) {\n                try {\n                    uri = Uri.parse(URLDecoder.decode(uriString, \"UTF-8\"));\n                } catch (UnsupportedEncodingException e) {\n                    // Fallback to encoded URI. This exception is not expected.\n                }\n            }\n        }\n        this.bitmap = null;\n        this.uri = uri;\n        this.resource = null;\n        this.tile = true;\n    }\n\n    private ImageSource(int resource) {\n        this.bitmap = null;\n        this.uri = null;\n        this.resource = resource;\n        this.tile = true;\n    }\n\n    /**\n     * Create an instance from a resource. The correct resource for the device screen resolution will be used.\n     * @param resId resource ID.\n     */\n    public static ImageSource resource(int resId) {\n        return new ImageSource(resId);\n    }\n\n    /**\n     * Create an instance from an asset name.\n     * @param assetName asset name.\n     */\n    public static ImageSource asset(String assetName) {\n        if (assetName == null) {\n            throw new NullPointerException(\"Asset name must not be null\");\n        }\n        return uri(ASSET_SCHEME + assetName);\n    }\n\n    /**\n     * Create an instance from a URI. If the URI does not start with a scheme, it's assumed to be the URI\n     * of a file.\n     * @param uri image URI.\n     */\n    public static ImageSource uri(String uri) {\n        if (uri == null) {\n            throw new NullPointerException(\"Uri must not be null\");\n        }\n        if (!uri.contains(\"://\")) {\n            if (uri.startsWith(\"/\")) {\n                uri = uri.substring(1);\n            }\n            uri = FILE_SCHEME + uri;\n        }\n        return new ImageSource(Uri.parse(uri));\n    }\n\n    /**\n     * Create an instance from a URI.\n     * @param uri image URI.\n     */\n    public static ImageSource uri(Uri uri) {\n        if (uri == null) {\n            throw new NullPointerException(\"Uri must not be null\");\n        }\n        return new ImageSource(uri);\n    }\n\n    /**\n     * Provide a loaded bitmap for display.\n     * @param bitmap bitmap to be displayed.\n     */\n    public static ImageSource bitmap(Bitmap bitmap) {\n        if (bitmap == null) {\n            throw new NullPointerException(\"Bitmap must not be null\");\n        }\n        return new ImageSource(bitmap, false);\n    }\n\n    /**\n     * Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no\n     * longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso\n     * or Volley.\n     * @param bitmap bitmap to be displayed.\n     */\n    public static ImageSource cachedBitmap(Bitmap bitmap) {\n        if (bitmap == null) {\n            throw new NullPointerException(\"Bitmap must not be null\");\n        }\n        return new ImageSource(bitmap, true);\n    }\n\n    /**\n     * Enable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap.,\n     * and tiling cannot be disabled when displaying a region of the source image.\n     * @return this instance for chaining.\n     */\n    public ImageSource tilingEnabled() {\n        return tiling(true);\n    }\n\n    /**\n     * Disable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap,\n     * and tiling cannot be disabled when displaying a region of the source image.\n     * @return this instance for chaining.\n     */\n    public ImageSource tilingDisabled() {\n        return tiling(false);\n    }\n\n    /**\n     * Enable or disable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap,\n     * and tiling cannot be disabled when displaying a region of the source image.\n     * @return this instance for chaining.\n     */\n    public ImageSource tiling(boolean tile) {\n        this.tile = tile;\n        return this;\n    }\n\n    /**\n     * Use a region of the source image. Region must be set independently for the full size image and the preview if\n     * you are using one.\n     * @return this instance for chaining.\n     */\n    public ImageSource region(Rect sRegion) {\n        this.sRegion = sRegion;\n        setInvariants();\n        return this;\n    }\n\n    /**\n     * Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI\n     * and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare\n     * the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.\n     * @return this instance for chaining.\n     */\n    public ImageSource dimensions(int sWidth, int sHeight) {\n        if (bitmap == null) {\n            this.sWidth = sWidth;\n            this.sHeight = sHeight;\n        }\n        setInvariants();\n        return this;\n    }\n\n    private void setInvariants() {\n        if (this.sRegion != null) {\n            this.tile = true;\n            this.sWidth = this.sRegion.width();\n            this.sHeight = this.sRegion.height();\n        }\n    }\n\n    protected final Uri getUri() {\n        return uri;\n    }\n\n    protected final Bitmap getBitmap() {\n        return bitmap;\n    }\n\n    protected final Integer getResource() {\n        return resource;\n    }\n\n    protected final boolean getTile() {\n        return tile;\n    }\n\n    protected final int getSWidth() {\n        return sWidth;\n    }\n\n    protected final int getSHeight() {\n        return sHeight;\n    }\n\n    protected final Rect getSRegion() {\n        return sRegion;\n    }\n\n    protected final boolean isCached() {\n        return cached;\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/ImageViewState.java",
    "content": "/*\nCopyright 2014 David Morrissey\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage com.matisse.widget.longimage;\n\nimport android.graphics.PointF;\n\nimport java.io.Serializable;\n\n/**\n * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.\n */\npublic class ImageViewState implements Serializable {\n\n    private float scale;\n\n    private float centerX;\n\n    private float centerY;\n\n    private int orientation;\n\n    public ImageViewState(float scale, PointF center, int orientation) {\n        this.scale = scale;\n        this.centerX = center.x;\n        this.centerY = center.y;\n        this.orientation = orientation;\n    }\n\n    public float getScale() {\n        return scale;\n    }\n\n    public PointF getCenter() {\n        return new PointF(centerX, centerY);\n    }\n\n    public int getOrientation() {\n        return orientation;\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/SkiaImageDecoder.java",
    "content": "package com.matisse.widget.longimage;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport java.io.InputStream;\nimport java.util.List;\n\n/**\n * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageDecoder}\n * using Android's {@link BitmapFactory}, based on the Skia library. This\n * works well in most circumstances and has reasonable performance, however it has some problems\n * with grayscale, indexed and CMYK images.\n */\npublic class SkiaImageDecoder implements ImageDecoder {\n\n    private static final String FILE_PREFIX = \"file://\";\n    private static final String ASSET_PREFIX = FILE_PREFIX + \"/android_asset/\";\n    private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + \"://\";\n\n    @Override\n    public Bitmap decode(Context context, Uri uri) throws Exception {\n        String uriString = uri.toString();\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        Bitmap bitmap;\n        options.inPreferredConfig = Bitmap.Config.RGB_565;\n        if (uriString.startsWith(RESOURCE_PREFIX)) {\n            Resources res;\n            String packageName = uri.getAuthority();\n            if (context.getPackageName().equals(packageName)) {\n                res = context.getResources();\n            } else {\n                PackageManager pm = context.getPackageManager();\n                res = pm.getResourcesForApplication(packageName);\n            }\n\n            int id = 0;\n            List<String> segments = uri.getPathSegments();\n            int size = segments.size();\n            if (size == 2 && segments.get(0).equals(\"drawable\")) {\n                String resName = segments.get(1);\n                id = res.getIdentifier(resName, \"drawable\", packageName);\n            } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {\n                try {\n                    id = Integer.parseInt(segments.get(0));\n                } catch (NumberFormatException ignored) {\n                }\n            }\n\n            bitmap = BitmapFactory.decodeResource(context.getResources(), id, options);\n        } else if (uriString.startsWith(ASSET_PREFIX)) {\n            String assetName = uriString.substring(ASSET_PREFIX.length());\n            bitmap = BitmapFactory.decodeStream(context.getAssets().open(assetName), null, options);\n        } else if (uriString.startsWith(FILE_PREFIX)) {\n            bitmap = BitmapFactory.decodeFile(uriString.substring(FILE_PREFIX.length()), options);\n        } else {\n            InputStream inputStream = null;\n            try {\n                ContentResolver contentResolver = context.getContentResolver();\n                inputStream = contentResolver.openInputStream(uri);\n                bitmap = BitmapFactory.decodeStream(inputStream, null, options);\n            } finally {\n                if (inputStream != null) {\n                    try { inputStream.close(); } catch (Exception e) { }\n                }\n            }\n        }\n        if (bitmap == null) {\n            throw new RuntimeException(\"Skia image region decoder returned null bitmap - image format may not be supported\");\n        }\n        return bitmap;\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/SkiaImageRegionDecoder.java",
    "content": "package com.matisse.widget.longimage;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.content.res.AssetManager;\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.Config;\nimport android.graphics.BitmapFactory;\nimport android.graphics.BitmapRegionDecoder;\nimport android.graphics.Point;\nimport android.graphics.Rect;\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport java.io.InputStream;\nimport java.util.List;\n\n/**\n * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder}\n * using Android's {@link BitmapRegionDecoder}, based on the Skia library. This\n * works well in most circumstances and has reasonable performance due to the cached decoder instance,\n * however it has some problems with grayscale, indexed and CMYK images.\n */\npublic class SkiaImageRegionDecoder implements ImageRegionDecoder {\n\n    private BitmapRegionDecoder decoder;\n    private final Object decoderLock = new Object();\n\n    private static final String FILE_PREFIX = \"file://\";\n    private static final String ASSET_PREFIX = FILE_PREFIX + \"/android_asset/\";\n    private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + \"://\";\n\n    @Override\n    public Point init(Context context, Uri uri) throws Exception {\n        String uriString = uri.toString();\n        if (uriString.startsWith(RESOURCE_PREFIX)) {\n            Resources res;\n            String packageName = uri.getAuthority();\n            if (context.getPackageName().equals(packageName)) {\n                res = context.getResources();\n            } else {\n                PackageManager pm = context.getPackageManager();\n                res = pm.getResourcesForApplication(packageName);\n            }\n\n            int id = 0;\n            List<String> segments = uri.getPathSegments();\n            int size = segments.size();\n            if (size == 2 && segments.get(0).equals(\"drawable\")) {\n                String resName = segments.get(1);\n                id = res.getIdentifier(resName, \"drawable\", packageName);\n            } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {\n                try {\n                    id = Integer.parseInt(segments.get(0));\n                } catch (NumberFormatException ignored) {\n                }\n            }\n\n            decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false);\n        } else if (uriString.startsWith(ASSET_PREFIX)) {\n            String assetName = uriString.substring(ASSET_PREFIX.length());\n            decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false);\n        } else if (uriString.startsWith(FILE_PREFIX)) {\n            decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false);\n        } else {\n            InputStream inputStream = null;\n            try {\n                ContentResolver contentResolver = context.getContentResolver();\n                inputStream = contentResolver.openInputStream(uri);\n                decoder = BitmapRegionDecoder.newInstance(inputStream, false);\n            } finally {\n                if (inputStream != null) {\n                    try { inputStream.close(); } catch (Exception e) { }\n                }\n            }\n        }\n        return new Point(decoder.getWidth(), decoder.getHeight());\n    }\n\n    @Override\n    public Bitmap decodeRegion(Rect sRect, int sampleSize) {\n        synchronized (decoderLock) {\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inSampleSize = sampleSize;\n            options.inPreferredConfig = Config.RGB_565;\n            Bitmap bitmap = decoder.decodeRegion(sRect, options);\n            if (bitmap == null) {\n                throw new RuntimeException(\"Skia image decoder returned null bitmap - image format may not be supported\");\n            }\n            return bitmap;\n        }\n    }\n\n    @Override\n    public boolean isReady() {\n        return decoder != null && !decoder.isRecycled();\n    }\n\n    @Override\n    public void recycle() {\n        decoder.recycle();\n    }\n}\n"
  },
  {
    "path": "matisse/src/main/java/com/matisse/widget/longimage/SubsamplingScaleImageView.java",
    "content": "/*\nCopyright 2013-2015 David Morrissey\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage com.matisse.widget.longimage;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.database.Cursor;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.graphics.Paint.Style;\nimport android.graphics.Point;\nimport android.graphics.PointF;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.media.ExifInterface;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build.VERSION;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.provider.MediaStore;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewParent;\n\nimport androidx.annotation.AnyThread;\nimport androidx.annotation.NonNull;\n\nimport com.matisse.R;\n\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\n/**\n * Displays an image subsampled as necessary to avoid loading too much image data into memory. After a pinch to zoom in,\n * a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pinch and\n * zoom, tiles off screen or higher/lower resolution than required are discarded from memory.\n *\n * Tiles are no larger than the max supported bitmap size, so with large images tiling may be used even when zoomed out.\n *\n * v prefixes - coordinates, translations and distances measured in screen (view) pixels\n * s prefixes - coordinates, translations and distances measured in source image pixels (scaled)\n */\n@SuppressWarnings(\"unused\")\npublic class SubsamplingScaleImageView extends View {\n\n    private static final String TAG = SubsamplingScaleImageView.class.getSimpleName();\n\n    /** Attempt to use EXIF information on the image to rotate it. Works for external files only. */\n    public static final int ORIENTATION_USE_EXIF = -1;\n    /** Display the image file in its native orientation. */\n    public static final int ORIENTATION_0 = 0;\n    /** Rotate the image 90 degrees clockwise. */\n    public static final int ORIENTATION_90 = 90;\n    /** Rotate the image 180 degrees. */\n    public static final int ORIENTATION_180 = 180;\n    /** Rotate the image 270 degrees clockwise. */\n    public static final int ORIENTATION_270 = 270;\n\n    private static final List<Integer> VALID_ORIENTATIONS = Arrays.asList(ORIENTATION_0, ORIENTATION_90, ORIENTATION_180, ORIENTATION_270, ORIENTATION_USE_EXIF);\n\n    /** During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. */\n    public static final int ZOOM_FOCUS_FIXED = 1;\n    /** During zoom animation, move the point of the image that was tapped to the center of the screen. */\n    public static final int ZOOM_FOCUS_CENTER = 2;\n    /** Zoom in to and center the tapped point immediately without animating. */\n    public static final int ZOOM_FOCUS_CENTER_IMMEDIATE = 3;\n\n    private static final List<Integer> VALID_ZOOM_STYLES = Arrays.asList(ZOOM_FOCUS_FIXED, ZOOM_FOCUS_CENTER, ZOOM_FOCUS_CENTER_IMMEDIATE);\n\n    /** Quadratic ease out. Not recommended for scale animation, but good for panning. */\n    public static final int EASE_OUT_QUAD = 1;\n    /** Quadratic ease in and out. */\n    public static final int EASE_IN_OUT_QUAD = 2;\n\n    private static final List<Integer> VALID_EASING_STYLES = Arrays.asList(EASE_IN_OUT_QUAD, EASE_OUT_QUAD);\n\n    /** Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. */\n    public static final int PAN_LIMIT_INSIDE = 1;\n    /** Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. */\n    public static final int PAN_LIMIT_OUTSIDE = 2;\n    /** Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. */\n    public static final int PAN_LIMIT_CENTER = 3;\n\n    private static final List<Integer> VALID_PAN_LIMITS = Arrays.asList(PAN_LIMIT_INSIDE, PAN_LIMIT_OUTSIDE, PAN_LIMIT_CENTER);\n\n    /** Scale the image so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. The image is then centered in the view. This is the default behaviour and best for galleries. */\n    public static final int SCALE_TYPE_CENTER_INSIDE = 1;\n    /** Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view. */\n    public static final int SCALE_TYPE_CENTER_CROP = 2;\n    /** Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view. */\n    public static final int SCALE_TYPE_CUSTOM = 3;\n\n    private static final List<Integer> VALID_SCALE_TYPES = Arrays.asList(SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE, SCALE_TYPE_CUSTOM);\n\n    /** State change originated from animation. */\n    public static final int ORIGIN_ANIM = 1;\n    /** State change originated from touch gesture. */\n    public static final int ORIGIN_TOUCH = 2;\n    /** State change originated from a fling momentum anim. */\n    public static final int ORIGIN_FLING = 3;\n    /** State change originated from a double tap zoom anim. */\n    public static final int ORIGIN_DOUBLE_TAP_ZOOM = 4;\n\n    // Bitmap (preview or full image)\n    private Bitmap bitmap;\n\n    // Whether the bitmap is a preview image\n    private boolean bitmapIsPreview;\n\n    // Specifies if a cache handler is also referencing the bitmap. Do not recycle if so.\n    private boolean bitmapIsCached;\n\n    // Uri of full size image\n    private Uri uri;\n\n    // Sample size used to display the whole image when fully zoomed out\n    private int fullImageSampleSize;\n\n    // Map of zoom level to tile grid\n    private Map<Integer, List<Tile>> tileMap;\n\n    // Overlay tile boundaries and other info\n    private boolean debug;\n\n    // Image orientation setting\n    private int orientation = ORIENTATION_0;\n\n    // Max scale allowed (prevent infinite zoom)\n    private float maxScale = 2F;\n\n    // Min scale allowed (prevent infinite zoom)\n    private float minScale = minScale();\n\n    // Density to reach before loading higher resolution tiles\n    private int minimumTileDpi = -1;\n\n    // Pan limiting style\n    private int panLimit = PAN_LIMIT_INSIDE;\n\n    // Minimum scale type\n    private int minimumScaleType = SCALE_TYPE_CENTER_INSIDE;\n\n    // overrides for the dimensions of the generated tiles\n    public static int TILE_SIZE_AUTO = Integer.MAX_VALUE;\n    private int maxTileWidth = TILE_SIZE_AUTO;\n    private int maxTileHeight = TILE_SIZE_AUTO;\n\n    // Whether to use the thread pool executor to load tiles\n    private boolean parallelLoadingEnabled;\n\n    // Gesture detection settings\n    private boolean panEnabled = true;\n    private boolean zoomEnabled = true;\n    private boolean quickScaleEnabled = true;\n\n    // Double tap zoom behaviour\n    private float doubleTapZoomScale = 1F;\n    private int doubleTapZoomStyle = ZOOM_FOCUS_FIXED;\n    private int doubleTapZoomDuration = 500;\n\n    // Current scale and scale at start of zoom\n    private float scale;\n    private float scaleStart;\n\n    // Screen coordinate of top-left corner of source image\n    private PointF vTranslate;\n    private PointF vTranslateStart;\n    private PointF vTranslateBefore;\n\n    // Source coordinate to center on, used when new position is set externally before view is ready\n    private Float pendingScale;\n    private PointF sPendingCenter;\n    private PointF sRequestedCenter;\n\n    // Source image dimensions and orientation - dimensions relate to the unrotated image\n    private int sWidth;\n    private int sHeight;\n    private int sOrientation;\n    private Rect sRegion;\n    private Rect pRegion;\n\n    // Is two-finger zooming in progress\n    private boolean isZooming;\n    // Is one-finger panning in progress\n    private boolean isPanning;\n    // Is quick-scale gesture in progress\n    private boolean isQuickScaling;\n    // Max touches used in current gesture\n    private int maxTouchCount;\n\n    // Fling detector\n    private GestureDetector detector;\n\n    // Tile and image decoding\n    private ImageRegionDecoder decoder;\n    private final Object decoderLock = new Object();\n    private DecoderFactory<? extends ImageDecoder> bitmapDecoderFactory = new CompatDecoderFactory<ImageDecoder>(SkiaImageDecoder.class);\n    private DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory = new CompatDecoderFactory<ImageRegionDecoder>(SkiaImageRegionDecoder.class);\n\n    // Debug values\n    private PointF vCenterStart;\n    private float vDistStart;\n\n    // Current quickscale state\n    private final float quickScaleThreshold;\n    private float quickScaleLastDistance;\n    private boolean quickScaleMoved;\n    private PointF quickScaleVLastPoint;\n    private PointF quickScaleSCenter;\n    private PointF quickScaleVStart;\n\n    // Scale and center animation tracking\n    private Anim anim;\n\n    // Whether a ready notification has been sent to subclasses\n    private boolean readySent;\n    // Whether a base layer loaded notification has been sent to subclasses\n    private boolean imageLoadedSent;\n\n    // Event listener\n    private OnImageEventListener onImageEventListener;\n\n    // Scale and center listener\n    private OnStateChangedListener onStateChangedListener;\n\n    // Long click listener\n    private OnLongClickListener onLongClickListener;\n\n    // Long click handler\n    private Handler handler;\n    private static final int MESSAGE_LONG_CLICK = 1;\n\n    // Paint objects created once and reused for efficiency\n    private Paint bitmapPaint;\n    private Paint debugPaint;\n    private Paint tileBgPaint;\n\n    // Volatile fields used to reduce object creation\n    private ScaleAndTranslate satTemp;\n    private Matrix matrix;\n    private RectF sRect;\n    private float[] srcArray = new float[8];\n    private float[] dstArray = new float[8];\n\n    //The logical density of the display\n    private float density;\n\n\n    public SubsamplingScaleImageView(Context context, AttributeSet attr) {\n        super(context, attr);\n        density = getResources().getDisplayMetrics().density;\n        setMinimumDpi(160);\n        setDoubleTapZoomDpi(160);\n        setGestureDetector(context);\n        this.handler = new Handler(new Handler.Callback() {\n            public boolean handleMessage(Message message) {\n                if (message.what == MESSAGE_LONG_CLICK && onLongClickListener != null) {\n                    maxTouchCount = 0;\n                    SubsamplingScaleImageView.super.setOnLongClickListener(onLongClickListener);\n                    performLongClick();\n                    SubsamplingScaleImageView.super.setOnLongClickListener(null);\n                }\n                return true;\n            }\n        });\n        // Handle XML attributes\n        if (attr != null) {\n            TypedArray typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView);\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {\n                String assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName);\n                if (assetName != null && assetName.length() > 0) {\n                    setImage(ImageSource.asset(assetName).tilingEnabled());\n                }\n            }\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {\n                int resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0);\n                if (resId > 0) {\n                    setImage(ImageSource.resource(resId).tilingEnabled());\n                }\n            }\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {\n                setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true));\n            }\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {\n                setZoomEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true));\n            }\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {\n                setQuickScaleEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true));\n            }\n            if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {\n                setTileBackgroundColor(typedAttr.getColor(R.styleable.SubsamplingScaleImageView_tileBackgroundColor, Color.argb(0, 0, 0, 0)));\n            }\n            typedAttr.recycle();\n        }\n\n        quickScaleThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());\n    }\n\n    public SubsamplingScaleImageView(Context context) {\n        this(context, null);\n    }\n\n    /**\n     * Sets the image orientation. It's best to call this before setting the image file or asset, because it may waste\n     * loading of tiles. However, this can be freely called at any time.\n     */\n    public final void setOrientation(int orientation) {\n        if (!VALID_ORIENTATIONS.contains(orientation)) {\n            throw new IllegalArgumentException(\"Invalid orientation: \" + orientation);\n        }\n        this.orientation = orientation;\n        reset(false);\n        invalidate();\n        requestLayout();\n    }\n\n    /**\n     * Set the image source from a bitmap, resource, asset, file or other URI.\n     * @param imageSource Image source.\n     */\n    public final void setImage(ImageSource imageSource) {\n        setImage(imageSource, null, null);\n    }\n\n    /**\n     * Set the image source from a bitmap, resource, asset, file or other URI, starting with a given orientation\n     * setting, scale and center. This is the best method to use when you want scale and center to be restored\n     * after screen orientation change; it avoids any redundant loading of tiles in the wrong orientation.\n     * @param imageSource Image source.\n     * @param state State to be restored. Nullable.\n     */\n    public final void setImage(ImageSource imageSource, ImageViewState state) {\n        setImage(imageSource, null, state);\n    }\n\n    /**\n     * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be\n     * displayed until the full size image is loaded.\n     *\n     * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)}\n     * on the imageSource object. The preview source will be ignored if you don't provide dimensions,\n     * and if you provide a bitmap for the full size image.\n     * @param imageSource Image source. Dimensions must be declared.\n     * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads.\n     */\n    public final void setImage(ImageSource imageSource, ImageSource previewSource) {\n        setImage(imageSource, previewSource, null);\n    }\n\n    /**\n     * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be\n     * displayed until the full size image is loaded, starting with a given orientation setting, scale and center.\n     * This is the best method to use when you want scale and center to be restored after screen orientation change;\n     * it avoids any redundant loading of tiles in the wrong orientation.\n     *\n     * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)}\n     * on the imageSource object. The preview source will be ignored if you don't provide dimensions,\n     * and if you provide a bitmap for the full size image.\n     * @param imageSource Image source. Dimensions must be declared.\n     * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads.\n     * @param state State to be restored. Nullable.\n     */\n    public final void setImage(ImageSource imageSource, ImageSource previewSource, ImageViewState state) {\n        if (imageSource == null) {\n            throw new NullPointerException(\"imageSource must not be null\");\n        }\n\n        reset(true);\n        if (state != null) { restoreState(state); }\n\n        if (previewSource != null) {\n            if (imageSource.getBitmap() != null) {\n                throw new IllegalArgumentException(\"Preview image cannot be used when a bitmap is provided for the main image\");\n            }\n            if (imageSource.getSWidth() <= 0 || imageSource.getSHeight() <= 0) {\n                throw new IllegalArgumentException(\"Preview image cannot be used unless dimensions are provided for the main image\");\n            }\n            this.sWidth = imageSource.getSWidth();\n            this.sHeight = imageSource.getSHeight();\n            this.pRegion = previewSource.getSRegion();\n            if (previewSource.getBitmap() != null) {\n                this.bitmapIsCached = previewSource.isCached();\n                onPreviewLoaded(previewSource.getBitmap());\n            } else {\n                Uri uri = previewSource.getUri();\n                if (uri == null && previewSource.getResource() != null) {\n                    uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + \"://\" + getContext().getPackageName() + \"/\" + previewSource.getResource());\n                }\n                BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, true);\n                execute(task);\n            }\n        }\n\n        if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {\n            onImageLoaded(Bitmap.createBitmap(imageSource.getBitmap(), imageSource.getSRegion().left, imageSource.getSRegion().top, imageSource.getSRegion().width(), imageSource.getSRegion().height()), ORIENTATION_0, false);\n        } else if (imageSource.getBitmap() != null) {\n            onImageLoaded(imageSource.getBitmap(), ORIENTATION_0, imageSource.isCached());\n        } else {\n            sRegion = imageSource.getSRegion();\n            uri = imageSource.getUri();\n            if (uri == null && imageSource.getResource() != null) {\n                uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + \"://\" + getContext().getPackageName() + \"/\" + imageSource.getResource());\n            }\n            if (imageSource.getTile() || sRegion != null) {\n                // Load the bitmap using tile decoding.\n                TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);\n                execute(task);\n            } else {\n                // Load the bitmap as a single image.\n                BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);\n                execute(task);\n            }\n        }\n    }\n\n    /**\n     * Reset all state before setting/changing image or setting new rotation.\n     */\n    private void reset(boolean newImage) {\n        debug(\"reset newImage=\" + newImage);\n        scale = 0f;\n        scaleStart = 0f;\n        vTranslate = null;\n        vTranslateStart = null;\n        vTranslateBefore = null;\n        pendingScale = 0f;\n        sPendingCenter = null;\n        sRequestedCenter = null;\n        isZooming = false;\n        isPanning = false;\n        isQuickScaling = false;\n        maxTouchCount = 0;\n        fullImageSampleSize = 0;\n        vCenterStart = null;\n        vDistStart = 0;\n        quickScaleLastDistance = 0f;\n        quickScaleMoved = false;\n        quickScaleSCenter = null;\n        quickScaleVLastPoint = null;\n        quickScaleVStart = null;\n        anim = null;\n        satTemp = null;\n        matrix = null;\n        sRect = null;\n        if (newImage) {\n            uri = null;\n            if (decoder != null) {\n                synchronized (decoderLock) {\n                    decoder.recycle();\n                    decoder = null;\n                }\n            }\n            if (bitmap != null && !bitmapIsCached) {\n                bitmap.recycle();\n            }\n            if (bitmap != null && bitmapIsCached && onImageEventListener != null) {\n                onImageEventListener.onPreviewReleased();\n            }\n            sWidth = 0;\n            sHeight = 0;\n            sOrientation = 0;\n            sRegion = null;\n            pRegion = null;\n            readySent = false;\n            imageLoadedSent = false;\n            bitmap = null;\n            bitmapIsPreview = false;\n            bitmapIsCached = false;\n        }\n        if (tileMap != null) {\n            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {\n                for (Tile tile : tileMapEntry.getValue()) {\n                    tile.visible = false;\n                    if (tile.bitmap != null) {\n                        tile.bitmap.recycle();\n                        tile.bitmap = null;\n                    }\n                }\n            }\n            tileMap = null;\n        }\n        setGestureDetector(getContext());\n    }\n\n    private void setGestureDetector(final Context context) {\n        this.detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {\n\n            @Override\n            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {\n                if (panEnabled && readySent && vTranslate != null && e1 != null && e2 != null && (Math.abs(e1.getX() - e2.getX()) > 50 || Math.abs(e1.getY() - e2.getY()) > 50) && (Math.abs(velocityX) > 500 || Math.abs(velocityY) > 500) && !isZooming) {\n                    PointF vTranslateEnd = new PointF(vTranslate.x + (velocityX * 0.25f), vTranslate.y + (velocityY * 0.25f));\n                    float sCenterXEnd = ((getWidth()/2) - vTranslateEnd.x)/scale;\n                    float sCenterYEnd = ((getHeight()/2) - vTranslateEnd.y)/scale;\n                    new AnimationBuilder(new PointF(sCenterXEnd, sCenterYEnd)).withEasing(EASE_OUT_QUAD).withPanLimited(false).withOrigin(ORIGIN_FLING).start();\n                    return true;\n                }\n                return super.onFling(e1, e2, velocityX, velocityY);\n            }\n\n            @Override\n            public boolean onSingleTapConfirmed(MotionEvent e) {\n                performClick();\n                return true;\n            }\n\n            @Override\n            public boolean onDoubleTap(MotionEvent e) {\n                if (zoomEnabled && readySent && vTranslate != null) {\n                    // Hacky solution for #15 - after a double tap the GestureDetector gets in a state\n                    // where the next fling is ignored, so here we replace it with a new one.\n                    setGestureDetector(context);\n                    if (quickScaleEnabled) {\n                        // Store quick scale params. This will become either a double tap zoom or a\n                        // quick scale depending on whether the user swipes.\n                        vCenterStart = new PointF(e.getX(), e.getY());\n                        vTranslateStart = new PointF(vTranslate.x, vTranslate.y);\n                        scaleStart = scale;\n                        isQuickScaling = true;\n                        isZooming = true;\n                        quickScaleLastDistance = -1F;\n                        quickScaleSCenter = viewToSourceCoord(vCenterStart);\n                        quickScaleVStart = new PointF(e.getX(), e.getY());\n                        quickScaleVLastPoint = new PointF(quickScaleSCenter.x, quickScaleSCenter.y);\n                        quickScaleMoved = false;\n                        // We need to get events in onTouchEvent after this.\n                        return false;\n                    } else {\n                        // Start double tap zoom animation.\n                        doubleTapZoom(viewToSourceCoord(new PointF(e.getX(), e.getY())), new PointF(e.getX(), e.getY()));\n                        return true;\n                    }\n                }\n                return super.onDoubleTapEvent(e);\n            }\n        });\n    }\n\n    /**\n     * On resize, preserve center and scale. Various behaviours are possible, override this method to use another.\n     */\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        debug(\"onSizeChanged %dx%d -> %dx%d\", oldw, oldh, w, h);\n        PointF sCenter = getCenter();\n        if (readySent && sCenter != null) {\n            this.anim = null;\n            this.pendingScale = scale;\n            this.sPendingCenter = sCenter;\n        }\n    }\n\n    /**\n     * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is\n     * used. The image will scale within this box, not resizing the view as it is zoomed.\n     */\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);\n        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);\n        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);\n        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);\n        boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;\n        boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;\n        int width = parentWidth;\n        int height = parentHeight;\n        if (sWidth > 0 && sHeight > 0) {\n            if (resizeWidth && resizeHeight) {\n                width = sWidth();\n                height = sHeight();\n            } else if (resizeHeight) {\n                height = (int)((((double)sHeight()/(double)sWidth()) * width));\n            } else if (resizeWidth) {\n                width = (int)((((double)sWidth()/(double)sHeight()) * height));\n            }\n        }\n        width = Math.max(width, getSuggestedMinimumWidth());\n        height = Math.max(height, getSuggestedMinimumHeight());\n        setMeasuredDimension(width, height);\n    }\n\n    /**\n     * Handle touch events. One finger pans, and two finger pinch and zoom plus panning.\n     */\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        // During non-interruptible anims, ignore all touch events\n        if (anim != null && !anim.interruptible) {\n            requestDisallowInterceptTouchEvent(true);\n            return true;\n        } else {\n            if (anim != null && anim.listener != null) {\n                try {\n                    anim.listener.onInterruptedByUser();\n                } catch (Exception e) {\n                    Log.w(TAG, \"Error thrown by animation listener\", e);\n                }\n            }\n            anim = null;\n        }\n\n        // Abort if not ready\n        if (vTranslate == null) {\n            return true;\n        }\n        // Detect flings, taps and double taps\n        if (!isQuickScaling && (detector == null || detector.onTouchEvent(event))) {\n            isZooming = false;\n            isPanning = false;\n            maxTouchCount = 0;\n            return true;\n        }\n\n        if (vTranslateStart == null) { vTranslateStart = new PointF(0, 0); }\n        if (vTranslateBefore == null) { vTranslateBefore = new PointF(0, 0); }\n        if (vCenterStart == null) { vCenterStart = new PointF(0, 0); }\n\n        // Store current values so we can send an event if they change\n        float scaleBefore = scale;\n        vTranslateBefore.set(vTranslate);\n\n        boolean handled = onTouchEventInternal(event);\n        sendStateChanged(scaleBefore, vTranslateBefore, ORIGIN_TOUCH);\n        return handled || super.onTouchEvent(event);\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    private boolean onTouchEventInternal(@NonNull MotionEvent event) {\n        int touchCount = event.getPointerCount();\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n            case MotionEvent.ACTION_POINTER_1_DOWN:\n            case MotionEvent.ACTION_POINTER_2_DOWN:\n                anim = null;\n                requestDisallowInterceptTouchEvent(true);\n                maxTouchCount = Math.max(maxTouchCount, touchCount);\n                if (touchCount >= 2) {\n                    if (zoomEnabled) {\n                        // Start pinch to zoom. Calculate distance between touch points and center point of the pinch.\n                        float distance = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1));\n                        scaleStart = scale;\n                        vDistStart = distance;\n                        vTranslateStart.set(vTranslate.x, vTranslate.y);\n                        vCenterStart.set((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2);\n                    } else {\n                        // Abort all gestures on second touch\n                        maxTouchCount = 0;\n                    }\n                    // Cancel long click timer\n                    handler.removeMessages(MESSAGE_LONG_CLICK);\n                } else if (!isQuickScaling) {\n                    // Start one-finger pan\n                    vTranslateStart.set(vTranslate.x, vTranslate.y);\n                    vCenterStart.set(event.getX(), event.getY());\n\n                    // Start long click timer\n                    handler.sendEmptyMessageDelayed(MESSAGE_LONG_CLICK, 600);\n                }\n                return true;\n            case MotionEvent.ACTION_MOVE:\n                boolean consumed = false;\n                if (maxTouchCount > 0) {\n                    if (touchCount >= 2) {\n                        // Calculate new distance between touch points, to scale and pan relative to start values.\n                        float vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1));\n                        float vCenterEndX = (event.getX(0) + event.getX(1))/2;\n                        float vCenterEndY = (event.getY(0) + event.getY(1))/2;\n\n                        if (zoomEnabled && (distance(vCenterStart.x, vCenterEndX, vCenterStart.y, vCenterEndY) > 5 || Math.abs(vDistEnd - vDistStart) > 5 || isPanning)) {\n                            isZooming = true;\n                            isPanning = true;\n                            consumed = true;\n\n                            double previousScale = scale;\n                            scale = Math.min(maxScale, (vDistEnd / vDistStart) * scaleStart);\n\n                            if (scale <= minScale()) {\n                                // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in.\n                                vDistStart = vDistEnd;\n                                scaleStart = minScale();\n                                vCenterStart.set(vCenterEndX, vCenterEndY);\n                                vTranslateStart.set(vTranslate);\n                            } else if (panEnabled) {\n                                // Translate to place the source image coordinate that was at the center of the pinch at the start\n                                // at the center of the pinch now, to give simultaneous pan + zoom.\n                                float vLeftStart = vCenterStart.x - vTranslateStart.x;\n                                float vTopStart = vCenterStart.y - vTranslateStart.y;\n                                float vLeftNow = vLeftStart * (scale/scaleStart);\n                                float vTopNow = vTopStart * (scale/scaleStart);\n                                vTranslate.x = vCenterEndX - vLeftNow;\n                                vTranslate.y = vCenterEndY - vTopNow;\n                                if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) {\n                                    fitToBounds(true);\n                                    vCenterStart.set(vCenterEndX, vCenterEndY);\n                                    vTranslateStart.set(vTranslate);\n                                    scaleStart = scale;\n                                    vDistStart = vDistEnd;\n                                }\n                            } else if (sRequestedCenter != null) {\n                                // With a center specified from code, zoom around that point.\n                                vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x);\n                                vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y);\n                            } else {\n                                // With no requested center, scale around the image center.\n                                vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2));\n                                vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2));\n                            }\n\n                            fitToBounds(true);\n                            refreshRequiredTiles(false);\n                        }\n                    } else if (isQuickScaling) {\n                        // One finger zoom\n                        // Stole Google's Magical Formula™ to make sure it feels the exact same\n                        float dist = Math.abs(quickScaleVStart.y - event.getY()) * 2 + quickScaleThreshold;\n\n                        if (quickScaleLastDistance == -1f) {\n                            quickScaleLastDistance = dist;\n                        }\n                        boolean isUpwards = event.getY() > quickScaleVLastPoint.y;\n                        quickScaleVLastPoint.set(0, event.getY());\n\n                        float spanDiff = Math.abs(1 - (dist / quickScaleLastDistance)) * 0.5f;\n\n                        if (spanDiff > 0.03f || quickScaleMoved) {\n                            quickScaleMoved = true;\n\n                            float multiplier = 1;\n                            if (quickScaleLastDistance > 0) {\n                                multiplier = isUpwards ? (1 + spanDiff) : (1 - spanDiff);\n                            }\n\n                            double previousScale = scale;\n                            scale = Math.max(minScale(), Math.min(maxScale, scale * multiplier));\n\n                            if (panEnabled) {\n                                float vLeftStart = vCenterStart.x - vTranslateStart.x;\n                                float vTopStart = vCenterStart.y - vTranslateStart.y;\n                                float vLeftNow = vLeftStart * (scale/scaleStart);\n                                float vTopNow = vTopStart * (scale/scaleStart);\n                                vTranslate.x = vCenterStart.x - vLeftNow;\n                                vTranslate.y = vCenterStart.y - vTopNow;\n                                if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) {\n                                    fitToBounds(true);\n                                    vCenterStart.set(sourceToViewCoord(quickScaleSCenter));\n                                    vTranslateStart.set(vTranslate);\n                                    scaleStart = scale;\n                                    dist = 0;\n                                }\n                            } else if (sRequestedCenter != null) {\n                                // With a center specified from code, zoom around that point.\n                                vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x);\n                                vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y);\n                            } else {\n                                // With no requested center, scale around the image center.\n                                vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2));\n                                vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2));\n                            }\n                        }\n\n                        quickScaleLastDistance = dist;\n\n                        fitToBounds(true);\n                        refreshRequiredTiles(false);\n\n                        consumed = true;\n                    } else if (!isZooming) {\n                        // One finger pan - translate the image. We do this calculation even with pan disabled so click\n                        // and long click behaviour is preserved.\n                        float dx = Math.abs(event.getX() - vCenterStart.x);\n                        float dy = Math.abs(event.getY() - vCenterStart.y);\n\n                        //On the Samsung S6 long click event does not work, because the dx > 5 usually true\n                        float offset = density * 5;\n                        if (dx > offset || dy > offset || isPanning) {\n                            consumed = true;\n                            vTranslate.x = vTranslateStart.x + (event.getX() - vCenterStart.x);\n                            vTranslate.y = vTranslateStart.y + (event.getY() - vCenterStart.y);\n\n                            float lastX = vTranslate.x;\n                            float lastY = vTranslate.y;\n                            fitToBounds(true);\n                            boolean atXEdge = lastX != vTranslate.x;\n                            boolean atYEdge = lastY != vTranslate.y;\n                            boolean edgeXSwipe = atXEdge && dx > dy && !isPanning;\n                            boolean edgeYSwipe = atYEdge && dy > dx && !isPanning;\n                            boolean yPan = lastY == vTranslate.y && dy > offset * 3;\n                            if (!edgeXSwipe && !edgeYSwipe && (!atXEdge || !atYEdge || yPan || isPanning)) {\n                                isPanning = true;\n                            } else if (dx > offset || dy > offset) {\n                                // Haven't panned the image, and we're at the left or right edge. Switch to page swipe.\n                                maxTouchCount = 0;\n                                handler.removeMessages(MESSAGE_LONG_CLICK);\n                                requestDisallowInterceptTouchEvent(false);\n                            } \n                            if (!panEnabled) {\n                                vTranslate.x = vTranslateStart.x;\n                                vTranslate.y = vTranslateStart.y;\n                                requestDisallowInterceptTouchEvent(false);\n                            }\n\n                            refreshRequiredTiles(false);\n                        }\n                    }\n                }\n                if (consumed) {\n                    handler.removeMessages(MESSAGE_LONG_CLICK);\n                    invalidate();\n                    return true;\n                }\n                break;\n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_POINTER_UP:\n            case MotionEvent.ACTION_POINTER_2_UP:\n                handler.removeMessages(MESSAGE_LONG_CLICK);\n                if (isQuickScaling) {\n                    isQuickScaling = false;\n                    if (!quickScaleMoved) {\n                        doubleTapZoom(quickScaleSCenter, vCenterStart);\n                    }\n                }\n                if (maxTouchCount > 0 && (isZooming || isPanning)) {\n                    if (isZooming && touchCount == 2) {\n                        // Convert from zoom to pan with remaining touch\n                        isPanning = true;\n                        vTranslateStart.set(vTranslate.x, vTranslate.y);\n                        if (event.getActionIndex() == 1) {\n                            vCenterStart.set(event.getX(0), event.getY(0));\n                        } else {\n                            vCenterStart.set(event.getX(1), event.getY(1));\n                        }\n                    }\n                    if (touchCount < 3) {\n                        // End zooming when only one touch point\n                        isZooming = false;\n                    }\n                    if (touchCount < 2) {\n                        // End panning when no touch points\n                        isPanning = false;\n                        maxTouchCount = 0;\n                    }\n                    // Trigger load of tiles now required\n                    refreshRequiredTiles(true);\n                    return true;\n                }\n                if (touchCount == 1) {\n                    isZooming = false;\n                    isPanning = false;\n                    maxTouchCount = 0;\n                }\n                return true;\n        }\n        return false;\n    }\n\n    private void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {\n        ViewParent parent = getParent();\n        if (parent != null) {\n            parent.requestDisallowInterceptTouchEvent(disallowIntercept);\n        }\n    }\n\n    /**\n     * Double tap zoom handler triggered from gesture detector or on touch, depending on whether\n     * quick scale is enabled.\n     */\n    private void doubleTapZoom(PointF sCenter, PointF vFocus) {\n        if (!panEnabled) {\n            if (sRequestedCenter != null) {\n                // With a center specified from code, zoom around that point.\n                sCenter.x = sRequestedCenter.x;\n                sCenter.y = sRequestedCenter.y;\n            } else {\n                // With no requested center, scale around the image center.\n                sCenter.x = sWidth()/2;\n                sCenter.y = sHeight()/2;\n            }\n        }\n        float doubleTapZoomScale = Math.min(maxScale, SubsamplingScaleImageView.this.doubleTapZoomScale);\n        boolean zoomIn = scale <= doubleTapZoomScale * 0.9;\n        float targetScale = zoomIn ? doubleTapZoomScale : minScale();\n        if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) {\n            setScaleAndCenter(targetScale, sCenter);\n        } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn || !panEnabled) {\n            new AnimationBuilder(targetScale, sCenter).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start();\n        } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) {\n            new AnimationBuilder(targetScale, sCenter, vFocus).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start();\n        }\n        invalidate();\n    }\n\n    /**\n     * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate\n     * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded.\n     */\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        createPaints();\n\n        // If image or view dimensions are not known yet, abort.\n        if (sWidth == 0 || sHeight == 0 || getWidth() == 0 || getHeight() == 0) {\n            return;\n        }\n\n        // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading.\n        if (tileMap == null && decoder != null) {\n            initialiseBaseLayer(getMaxBitmapDimensions(canvas));\n        }\n\n        // If image has been loaded or supplied as a bitmap, onDraw may be the first time the view has\n        // dimensions and therefore the first opportunity to set scale and translate. If this call returns\n        // false there is nothing to be drawn so return immediately.\n        if (!checkReady()) {\n            return;\n        }\n\n        // Set scale and translate before draw.\n        preDraw();\n\n        // If animating scale, calculate current scale and center with easing equations\n        if (anim != null) {\n            // Store current values so we can send an event if they change\n            float scaleBefore = scale;\n            if (vTranslateBefore == null) { vTranslateBefore = new PointF(0, 0); }\n            vTranslateBefore.set(vTranslate);\n\n            long scaleElapsed = System.currentTimeMillis() - anim.time;\n            boolean finished = scaleElapsed > anim.duration;\n            scaleElapsed = Math.min(scaleElapsed, anim.duration);\n            scale = ease(anim.easing, scaleElapsed, anim.scaleStart, anim.scaleEnd - anim.scaleStart, anim.duration);\n\n            // Apply required animation to the focal point\n            float vFocusNowX = ease(anim.easing, scaleElapsed, anim.vFocusStart.x, anim.vFocusEnd.x - anim.vFocusStart.x, anim.duration);\n            float vFocusNowY = ease(anim.easing, scaleElapsed, anim.vFocusStart.y, anim.vFocusEnd.y - anim.vFocusStart.y, anim.duration);\n            // Find out where the focal point is at this scale and adjust its position to follow the animation path\n            vTranslate.x -= sourceToViewX(anim.sCenterEnd.x) - vFocusNowX;\n            vTranslate.y -= sourceToViewY(anim.sCenterEnd.y) - vFocusNowY;\n\n            // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation.\n            fitToBounds(finished || (anim.scaleStart == anim.scaleEnd));\n            sendStateChanged(scaleBefore, vTranslateBefore, anim.origin);\n            refreshRequiredTiles(finished);\n            if (finished) {\n                if (anim.listener != null) {\n                    try {\n                        anim.listener.onComplete();\n                    } catch (Exception e) {\n                        Log.w(TAG, \"Error thrown by animation listener\", e);\n                    }\n                }\n                anim = null;\n            }\n            invalidate();\n        }\n\n        if (tileMap != null && isBaseLayerReady()) {\n\n            // Optimum sample size for current scale\n            int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale));\n\n            // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps\n            boolean hasMissingTiles = false;\n            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {\n                if (tileMapEntry.getKey() == sampleSize) {\n                    for (Tile tile : tileMapEntry.getValue()) {\n                        if (tile.visible && (tile.loading || tile.bitmap == null)) {\n                            hasMissingTiles = true;\n                        }\n                    }\n                }\n            }\n\n            // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath.\n            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {\n                if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {\n                    for (Tile tile : tileMapEntry.getValue()) {\n                        sourceToViewRect(tile.sRect, tile.vRect);\n                        if (!tile.loading && tile.bitmap != null) {\n                            if (tileBgPaint != null) {\n                                canvas.drawRect(tile.vRect, tileBgPaint);\n                            }\n                            if (matrix == null) { matrix = new Matrix(); }\n                            matrix.reset();\n                            setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());\n                            if (getRequiredRotation() == ORIENTATION_0) {\n                                setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom);\n                            } else if (getRequiredRotation() == ORIENTATION_90) {\n                                setMatrixArray(dstArray, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top);\n                            } else if (getRequiredRotation() == ORIENTATION_180) {\n                                setMatrixArray(dstArray, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top);\n                            } else if (getRequiredRotation() == ORIENTATION_270) {\n                                setMatrixArray(dstArray, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom);\n                            }\n                            matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);\n                            canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);\n                            if (debug) {\n                                canvas.drawRect(tile.vRect, debugPaint);\n                            }\n                        } else if (tile.loading && debug) {\n                            canvas.drawText(\"LOADING\", tile.vRect.left + 5, tile.vRect.top + 35, debugPaint);\n                        }\n                        if (tile.visible && debug) {\n                            canvas.drawText(\"ISS \" + tile.sampleSize + \" RECT \" + tile.sRect.top + \",\" + tile.sRect.left + \",\" + tile.sRect.bottom + \",\" + tile.sRect.right, tile.vRect.left + 5, tile.vRect.top + 15, debugPaint);\n                        }\n                    }\n                }\n            }\n\n        } else if (bitmap != null) {\n\n            float xScale = scale, yScale = scale;\n            if (bitmapIsPreview) {\n                xScale = scale * ((float)sWidth/bitmap.getWidth());\n                yScale = scale * ((float)sHeight/bitmap.getHeight());\n            }\n\n            if (matrix == null) { matrix = new Matrix(); }\n            matrix.reset();\n            matrix.postScale(xScale, yScale);\n            matrix.postRotate(getRequiredRotation());\n            matrix.postTranslate(vTranslate.x, vTranslate.y);\n\n            if (getRequiredRotation() == ORIENTATION_180) {\n                matrix.postTranslate(scale * sWidth, scale * sHeight);\n            } else if (getRequiredRotation() == ORIENTATION_90) {\n                matrix.postTranslate(scale * sHeight, 0);\n            } else if (getRequiredRotation() == ORIENTATION_270) {\n                matrix.postTranslate(0, scale * sWidth);\n            }\n\n            if (tileBgPaint != null) {\n                if (sRect == null) { sRect = new RectF(); }\n                sRect.set(0f, 0f, bitmapIsPreview ? bitmap.getWidth() : sWidth, bitmapIsPreview ? bitmap.getHeight() : sHeight);\n                matrix.mapRect(sRect);\n                canvas.drawRect(sRect, tileBgPaint);\n            }\n            canvas.drawBitmap(bitmap, matrix, bitmapPaint);\n\n        }\n\n        if (debug) {\n            canvas.drawText(\"Scale: \" + String.format(Locale.ENGLISH, \"%.2f\", scale), 5, 15, debugPaint);\n            canvas.drawText(\"Translate: \" + String.format(Locale.ENGLISH, \"%.2f\", vTranslate.x) + \":\" + String.format(Locale.ENGLISH, \"%.2f\", vTranslate.y), 5, 35, debugPaint);\n            PointF center = getCenter();\n            canvas.drawText(\"Source center: \" + String.format(Locale.ENGLISH, \"%.2f\", center.x) + \":\" + String.format(Locale.ENGLISH, \"%.2f\", center.y), 5, 55, debugPaint);\n            debugPaint.setStrokeWidth(2f);\n            if (anim != null) {\n                PointF vCenterStart = sourceToViewCoord(anim.sCenterStart);\n                PointF vCenterEndRequested = sourceToViewCoord(anim.sCenterEndRequested);\n                PointF vCenterEnd = sourceToViewCoord(anim.sCenterEnd);\n                canvas.drawCircle(vCenterStart.x, vCenterStart.y, 10, debugPaint);\n                debugPaint.setColor(Color.RED);\n                canvas.drawCircle(vCenterEndRequested.x, vCenterEndRequested.y, 20, debugPaint);\n                debugPaint.setColor(Color.BLUE);\n                canvas.drawCircle(vCenterEnd.x, vCenterEnd.y, 25, debugPaint);\n                debugPaint.setColor(Color.CYAN);\n                canvas.drawCircle(getWidth() / 2, getHeight() / 2, 30, debugPaint);\n            }\n            if (vCenterStart != null) {\n                debugPaint.setColor(Color.RED);\n                canvas.drawCircle(vCenterStart.x, vCenterStart.y, 20, debugPaint);\n            }\n            if (quickScaleSCenter != null) {\n                debugPaint.setColor(Color.BLUE);\n                canvas.drawCircle(sourceToViewX(quickScaleSCenter.x), sourceToViewY(quickScaleSCenter.y), 35, debugPaint);\n            }\n            if (quickScaleVStart != null) {\n                debugPaint.setColor(Color.CYAN);\n                canvas.drawCircle(quickScaleVStart.x, quickScaleVStart.y, 30, debugPaint);\n            }\n            debugPaint.setColor(Color.MAGENTA);\n            debugPaint.setStrokeWidth(1f);\n        }\n    }\n\n    /**\n     * Helper method for setting the values of a tile matrix array.\n     */\n    private void setMatrixArray(float[] array, float f0, float f1, float f2, float f3, float f4, float f5, float f6, float f7) {\n        array[0] = f0;\n        array[1] = f1;\n        array[2] = f2;\n        array[3] = f3;\n        array[4] = f4;\n        array[5] = f5;\n        array[6] = f6;\n        array[7] = f7;\n    }\n\n    /**\n     * Checks whether the base layer of tiles or full size bitmap is ready.\n     */\n    private boolean isBaseLayerReady() {\n        if (bitmap != null && !bitmapIsPreview) {\n            return true;\n        } else if (tileMap != null) {\n            boolean baseLayerReady = true;\n            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {\n                if (tileMapEntry.getKey() == fullImageSampleSize) {\n                    for (Tile tile : tileMapEntry.getValue()) {\n                        if (tile.loading || tile.bitmap == null) {\n                            baseLayerReady = false;\n                        }\n                    }\n                }\n            }\n            return baseLayerReady;\n        }\n        return false;\n    }\n\n    /**\n     * Check whether view and image dimensions are known and either a preview, full size image or\n     * base layer tiles are loaded. First time, send ready event to listener. The next draw will\n     * display an image.\n     */\n    private boolean checkReady() {\n        boolean ready = getWidth() > 0 && getHeight() > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady());\n        if (!readySent && ready) {\n            preDraw();\n            readySent = true;\n            onReady();\n            if (onImageEventListener != null) {\n                onImageEventListener.onReady();\n            }\n        }\n        return ready;\n    }\n\n    /**\n     * Check whether either the full size bitmap or base layer tiles are loaded. First time, send image\n     * loaded event to listener.\n     */\n    private boolean checkImageLoaded() {\n        boolean imageLoaded = isBaseLayerReady();\n        if (!imageLoadedSent && imageLoaded) {\n            preDraw();\n            imageLoadedSent = true;\n            onImageLoaded();\n            if (onImageEventListener != null) {\n                onImageEventListener.onImageLoaded();\n            }\n        }\n        return imageLoaded;\n    }\n\n    /**\n     * Creates Paint objects once when first needed.\n     */\n    private void createPaints() {\n        if (bitmapPaint == null) {\n            bitmapPaint = new Paint();\n            bitmapPaint.setAntiAlias(true);\n            bitmapPaint.setFilterBitmap(true);\n            bitmapPaint.setDither(true);\n        }\n        if (debugPaint == null && debug) {\n            debugPaint = new Paint();\n            debugPaint.setTextSize(18);\n            debugPaint.setColor(Color.MAGENTA);\n            debugPaint.setStyle(Style.STROKE);\n        }\n    }\n\n    /**\n     * Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of\n     * the base layer image - the whole source subsampled as necessary.\n     */\n    private synchronized void initialiseBaseLayer(Point maxTileDimensions) {\n        debug(\"initialiseBaseLayer maxTileDimensions=%dx%d\", maxTileDimensions.x, maxTileDimensions.y);\n\n        satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));\n        fitToBounds(true, satTemp);\n\n        // Load double resolution - next level will be split into four tiles and at the center all four are required,\n        // so don't bother with tiling until the next level 16 tiles are needed.\n        fullImageSampleSize = calculateInSampleSize(satTemp.scale);\n        if (fullImageSampleSize > 1) {\n            fullImageSampleSize /= 2;\n        }\n\n        if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {\n\n            // Whole image is required at native resolution, and is smaller than the canvas max bitmap size.\n            // Use BitmapDecoder for better image support.\n            decoder.recycle();\n            decoder = null;\n            BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);\n            execute(task);\n\n        } else {\n\n            initialiseTileMap(maxTileDimensions);\n\n            List<Tile> baseGrid = tileMap.get(fullImageSampleSize);\n            for (Tile baseTile : baseGrid) {\n                TileLoadTask task = new TileLoadTask(this, decoder, baseTile);\n                execute(task);\n            }\n            refreshRequiredTiles(true);\n\n        }\n\n    }\n\n    /**\n     * Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles\n     * that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen.\n     * @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance.\n     */\n    private void refreshRequiredTiles(boolean load) {\n        if (decoder == null || tileMap == null) { return; }\n\n        int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale));\n\n        // Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher\n        // resolution than required, or lower res than required but not the base layer, so the base layer is always present.\n        for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {\n            for (Tile tile : tileMapEntry.getValue()) {\n                if (tile.sampleSize < sampleSize || (tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize)) {\n                    tile.visible = false;\n                    if (tile.bitmap != null) {\n                        tile.bitmap.recycle();\n                        tile.bitmap = null;\n                    }\n                }\n                if (tile.sampleSize == sampleSize) {\n                    if (tileVisible(tile)) {\n                        tile.visible = true;\n                        if (!tile.loading && tile.bitmap == null && load) {\n                            TileLoadTask task = new TileLoadTask(this, decoder, tile);\n                            execute(task);\n                        }\n                    } else if (tile.sampleSize != fullImageSampleSize) {\n                        tile.visible = false;\n                        if (tile.bitmap != null) {\n                            tile.bitmap.recycle();\n                            tile.bitmap = null;\n                        }\n                    }\n                } else if (tile.sampleSize == fullImageSampleSize) {\n                    tile.visible = true;\n                }\n            }\n        }\n\n    }\n\n    /**\n     * Determine whether tile is visible.\n     */\n    private boolean tileVisible(Tile tile) {\n        float sVisLeft = viewToSourceX(0),\n            sVisRight = viewToSourceX(getWidth()),\n            sVisTop = viewToSourceY(0),\n            sVisBottom = viewToSourceY(getHeight());\n        return !(sVisLeft > tile.sRect.right || tile.sRect.left > sVisRight || sVisTop > tile.sRect.bottom || tile.sRect.top > sVisBottom);\n    }\n\n    /**\n     * Sets scale and translate ready for the next draw.\n     */\n    private void preDraw() {\n        if (getWidth() == 0 || getHeight() == 0 || sWidth <= 0 || sHeight <= 0) {\n            return;\n        }\n\n        // If waiting to translate to new center position, set translate now\n        if (sPendingCenter != null && pendingScale != null) {\n            scale = pendingScale;\n            if (vTranslate == null) {\n                vTranslate = new PointF();\n            }\n            vTranslate.x = (getWidth()/2) - (scale * sPendingCenter.x);\n            vTranslate.y = (getHeight()/2) - (scale * sPendingCenter.y);\n            sPendingCenter = null;\n            pendingScale = null;\n            fitToBounds(true);\n            refreshRequiredTiles(true);\n        }\n\n        // On first display of base image set up position, and in other cases make sure scale is correct.\n        fitToBounds(false);\n    }\n\n    /**\n     * Calculates sample size to fit the source image in given bounds.\n     */\n    private int calculateInSampleSize(float scale) {\n        if (minimumTileDpi > 0) {\n            DisplayMetrics metrics = getResources().getDisplayMetrics();\n            float averageDpi = (metrics.xdpi + metrics.ydpi)/2;\n            scale = (minimumTileDpi/averageDpi) * scale;\n        }\n\n        int reqWidth = (int)(sWidth() * scale);\n        int reqHeight = (int)(sHeight() * scale);\n\n        // Raw height and width of image\n        int inSampleSize = 1;\n        if (reqWidth == 0 || reqHeight == 0) {\n            return 32;\n        }\n\n        if (sHeight() > reqHeight || sWidth() > reqWidth) {\n\n            // Calculate ratios of height and width to requested height and width\n            final int heightRatio = Math.round((float) sHeight() / (float) reqHeight);\n            final int widthRatio = Math.round((float) sWidth() / (float) reqWidth);\n\n            // Choose the smallest ratio as inSampleSize value, this will guarantee\n            // a final image with both dimensions larger than or equal to the\n            // requested height and width.\n            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;\n        }\n\n        // We want the actual sample size that will be used, so round down to nearest power of 2.\n        int power = 1;\n        while (power * 2 < inSampleSize) {\n            power = power * 2;\n        }\n\n        return power;\n    }\n\n    /**\n     * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale\n     * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an\n     * animation should be.\n     * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.\n     * @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid.\n     */\n    private void fitToBounds(boolean center, ScaleAndTranslate sat) {\n        if (panLimit == PAN_LIMIT_OUTSIDE && isReady()) {\n            center = false;\n        }\n\n        PointF vTranslate = sat.vTranslate;\n        float scale = limitedScale(sat.scale);\n        float scaleWidth = scale * sWidth();\n        float scaleHeight = scale * sHeight();\n\n        if (panLimit == PAN_LIMIT_CENTER && isReady()) {\n            vTranslate.x = Math.max(vTranslate.x, getWidth()/2 - scaleWidth);\n            vTranslate.y = Math.max(vTranslate.y, getHeight()/2 - scaleHeight);\n        } else if (center) {\n            vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth);\n            vTranslate.y = Math.max(vTranslate.y, getHeight() - scaleHeight);\n        } else {\n            vTranslate.x = Math.max(vTranslate.x, -scaleWidth);\n            vTranslate.y = Math.max(vTranslate.y, -scaleHeight);\n        }\n\n        // Asymmetric padding adjustments\n        float xPaddingRatio = getPaddingLeft() > 0 || getPaddingRight() > 0 ? getPaddingLeft()/(float)(getPaddingLeft() + getPaddingRight()) : 0.5f;\n        float yPaddingRatio = getPaddingTop() > 0 || getPaddingBottom() > 0 ? getPaddingTop()/(float)(getPaddingTop() + getPaddingBottom()) : 0.5f;\n\n        float maxTx;\n        float maxTy;\n        if (panLimit == PAN_LIMIT_CENTER && isReady()) {\n            maxTx = Math.max(0, getWidth()/2);\n            maxTy = Math.max(0, getHeight()/2);\n        } else if (center) {\n            maxTx = Math.max(0, (getWidth() - scaleWidth) * xPaddingRatio);\n            maxTy = Math.max(0, (getHeight() - scaleHeight) * yPaddingRatio);\n        } else {\n            maxTx = Math.max(0, getWidth());\n            maxTy = Math.max(0, getHeight());\n        }\n\n        vTranslate.x = Math.min(vTranslate.x, maxTx);\n        vTranslate.y = Math.min(vTranslate.y, maxTy);\n\n        sat.scale = scale;\n    }\n\n    /**\n     * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale\n     * is set so one dimension fills the view and the image is centered on the other dimension.\n     * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.\n     */\n    private void fitToBounds(boolean center) {\n        boolean init = false;\n        if (vTranslate == null) {\n            init = true;\n            vTranslate = new PointF(0, 0);\n        }\n        if (satTemp == null) {\n            satTemp = new ScaleAndTranslate(0, new PointF(0, 0));\n        }\n        satTemp.scale = scale;\n        satTemp.vTranslate.set(vTranslate);\n        fitToBounds(center, satTemp);\n        scale = satTemp.scale;\n        vTranslate.set(satTemp.vTranslate);\n        if (init) {\n            vTranslate.set(vTranslateForSCenter(sWidth()/2, sHeight()/2, scale));\n        }\n    }\n\n    /**\n     * Once source image and view dimensions are known, creates a map of sample size to tile grid.\n     */\n    private void initialiseTileMap(Point maxTileDimensions) {\n        debug(\"initialiseTileMap maxTileDimensions=%dx%d\", maxTileDimensions.x, maxTileDimensions.y);\n        this.tileMap = new LinkedHashMap<>();\n        int sampleSize = fullImageSampleSize;\n        int xTiles = 1;\n        int yTiles = 1;\n        while (true) {\n            int sTileWidth = sWidth()/xTiles;\n            int sTileHeight = sHeight()/yTiles;\n            int subTileWidth = sTileWidth/sampleSize;\n            int subTileHeight = sTileHeight/sampleSize;\n            while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) {\n                xTiles += 1;\n                sTileWidth = sWidth()/xTiles;\n                subTileWidth = sTileWidth/sampleSize;\n            }\n            while (subTileHeight + yTiles + 1 > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) {\n                yTiles += 1;\n                sTileHeight = sHeight()/yTiles;\n                subTileHeight = sTileHeight/sampleSize;\n            }\n            List<Tile> tileGrid = new ArrayList<>(xTiles * yTiles);\n            for (int x = 0; x < xTiles; x++) {\n                for (int y = 0; y < yTiles; y++) {\n                    Tile tile = new Tile();\n                    tile.sampleSize = sampleSize;\n                    tile.visible = sampleSize == fullImageSampleSize;\n                    tile.sRect = new Rect(\n                        x * sTileWidth,\n                        y * sTileHeight,\n                        x == xTiles - 1 ? sWidth() : (x + 1) * sTileWidth,\n                        y == yTiles - 1 ? sHeight() : (y + 1) * sTileHeight\n                    );\n                    tile.vRect = new Rect(0, 0, 0, 0);\n                    tile.fileSRect = new Rect(tile.sRect);\n                    tileGrid.add(tile);\n                }\n            }\n            tileMap.put(sampleSize, tileGrid);\n            if (sampleSize == 1) {\n                break;\n            } else {\n                sampleSize /= 2;\n            }\n        }\n    }\n\n    /**\n     * Async task used to get image details without blocking the UI thread.\n     */\n    private static class TilesInitTask extends AsyncTask<Void, Void, int[]> {\n        private final WeakReference<SubsamplingScaleImageView> viewRef;\n        private final WeakReference<Context> contextRef;\n        private final WeakReference<DecoderFactory<? extends ImageRegionDecoder>> decoderFactoryRef;\n        private final Uri source;\n        private ImageRegionDecoder decoder;\n        private Exception exception;\n\n        TilesInitTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageRegionDecoder> decoderFactory, Uri source) {\n            this.viewRef = new WeakReference<>(view);\n            this.contextRef = new WeakReference<>(context);\n            this.decoderFactoryRef = new WeakReference<DecoderFactory<? extends ImageRegionDecoder>>(decoderFactory);\n            this.source = source;\n        }\n\n        @Override\n        protected int[] doInBackground(Void... params) {\n            try {\n                String sourceUri = source.toString();\n                Context context = contextRef.get();\n                DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();\n                SubsamplingScaleImageView view = viewRef.get();\n                if (context != null && decoderFactory != null && view != null) {\n                    view.debug(\"TilesInitTask.doInBackground\");\n                    decoder = decoderFactory.make();\n                    Point dimensions = decoder.init(context, source);\n                    int sWidth = dimensions.x;\n                    int sHeight = dimensions.y;\n                    int exifOrientation = view.getExifOrientation(context, sourceUri);\n                    if (view.sRegion != null) {\n                        sWidth = view.sRegion.width();\n                        sHeight = view.sRegion.height();\n                    }\n                    return new int[] { sWidth, sHeight, exifOrientation };\n                }\n            } catch (Exception e) {\n                Log.e(TAG, \"Failed to initialise bitmap decoder\", e);\n                this.exception = e;\n            }\n            return null;\n        }\n\n        @Override\n        protected void onPostExecute(int[] xyo) {\n            final SubsamplingScaleImageView view = viewRef.get();\n            if (view != null) {\n                if (decoder != null && xyo != null && xyo.length == 3) {\n                    view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);\n                } else if (exception != null && view.onImageEventListener != null) {\n                    view.onImageEventListener.onImageLoadError(exception);\n                }\n            }\n        }\n    }\n\n    /**\n     * Called by worker task when decoder is ready and image size and EXIF orientation is known.\n     */\n    private synchronized void onTilesInited(ImageRegionDecoder decoder, int sWidth, int sHeight, int sOrientation) {\n        debug(\"onTilesInited sWidth=%d, sHeight=%d, sOrientation=%d\", sWidth, sHeight, orientation);\n        // If actual dimensions don't match the declared size, reset everything.\n        if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != sWidth || this.sHeight != sHeight)) {\n            reset(false);\n            if (bitmap != null) {\n                if (!bitmapIsCached) {\n                    bitmap.recycle();\n                }\n                bitmap = null;\n                if (onImageEventListener != null && bitmapIsCached) {\n                    onImageEventListener.onPreviewReleased();\n                }\n                bitmapIsPreview = false;\n                bitmapIsCached = false;\n            }\n        }\n        this.decoder = decoder;\n        this.sWidth = sWidth;\n        this.sHeight = sHeight;\n        this.sOrientation = sOrientation;\n        checkReady();\n        if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && getWidth() > 0 && getHeight() > 0) {\n            initialiseBaseLayer(new Point(maxTileWidth, maxTileHeight));\n        }\n        invalidate();\n        requestLayout();\n    }\n\n    /**\n     * Async task used to load images without blocking the UI thread.\n     */\n    private static class TileLoadTask extends AsyncTask<Void, Void, Bitmap> {\n        private final WeakReference<SubsamplingScaleImageView> viewRef;\n        private final WeakReference<ImageRegionDecoder> decoderRef;\n        private final WeakReference<Tile> tileRef;\n        private Exception exception;\n\n        TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile) {\n            this.viewRef = new WeakReference<>(view);\n            this.decoderRef = new WeakReference<>(decoder);\n            this.tileRef = new WeakReference<>(tile);\n            tile.loading = true;\n        }\n\n        @Override\n        protected Bitmap doInBackground(Void... params) {\n            try {\n                SubsamplingScaleImageView view = viewRef.get();\n                ImageRegionDecoder decoder = decoderRef.get();\n                Tile tile = tileRef.get();\n                if (decoder != null && tile != null && view != null && decoder.isReady() && tile.visible) {\n                    view.debug(\"TileLoadTask.doInBackground, tile.sRect=%s, tile.sampleSize=%d\", tile.sRect, tile.sampleSize);\n                    synchronized (view.decoderLock) {\n                        // Update tile's file sRect according to rotation\n                        view.fileSRect(tile.sRect, tile.fileSRect);\n                        if (view.sRegion != null) {\n                            tile.fileSRect.offset(view.sRegion.left, view.sRegion.top);\n                        }\n                        return decoder.decodeRegion(tile.fileSRect, tile.sampleSize);\n                    }\n                } else if (tile != null) {\n                    tile.loading = false;\n                }\n            } catch (Exception e) {\n                Log.e(TAG, \"Failed to decode tile\", e);\n                this.exception = e;\n            } catch (OutOfMemoryError e) {\n                Log.e(TAG, \"Failed to decode tile - OutOfMemoryError\", e);\n                this.exception = new RuntimeException(e);\n            }\n            return null;\n        }\n\n        @Override\n        protected void onPostExecute(Bitmap bitmap) {\n            final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();\n            final Tile tile = tileRef.get();\n            if (subsamplingScaleImageView != null && tile != null) {\n                if (bitmap != null) {\n                    tile.bitmap = bitmap;\n                    tile.loading = false;\n                    subsamplingScaleImageView.onTileLoaded();\n                } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) {\n                    subsamplingScaleImageView.onImageEventListener.onTileLoadError(exception);\n                }\n            }\n        }\n    }\n\n    /**\n     * Called by worker task when a tile has loaded. Redraws the view.\n     */\n    private synchronized void onTileLoaded() {\n        debug(\"onTileLoaded\");\n        checkReady();\n        checkImageLoaded();\n        if (isBaseLayerReady() && bitmap != null) {\n            if (!bitmapIsCached) {\n                bitmap.recycle();\n            }\n            bitmap = null;\n            if (onImageEventListener != null && bitmapIsCached) {\n                onImageEventListener.onPreviewReleased();\n            }\n            bitmapIsPreview = false;\n            bitmapIsCached = false;\n        }\n        invalidate();\n    }\n\n    /**\n     * Async task used to load bitmap without blocking the UI thread.\n     */\n    private static class BitmapLoadTask extends AsyncTask<Void, Void, Integer> {\n        private final WeakReference<SubsamplingScaleImageView> viewRef;\n        private final WeakReference<Context> contextRef;\n        private final WeakReference<DecoderFactory<? extends ImageDecoder>> decoderFactoryRef;\n        private final Uri source;\n        private final boolean preview;\n        private Bitmap bitmap;\n        private Exception exception;\n\n        BitmapLoadTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageDecoder> decoderFactory, Uri source, boolean preview) {\n            this.viewRef = new WeakReference<>(view);\n            this.contextRef = new WeakReference<>(context);\n            this.decoderFactoryRef = new WeakReference<DecoderFactory<? extends ImageDecoder>>(decoderFactory);\n            this.source = source;\n            this.preview = preview;\n        }\n\n        @Override\n        protected Integer doInBackground(Void... params) {\n            try {\n                String sourceUri = source.toString();\n                Context context = contextRef.get();\n                DecoderFactory<? extends ImageDecoder> decoderFactory = decoderFactoryRef.get();\n                SubsamplingScaleImageView view = viewRef.get();\n                if (context != null && decoderFactory != null && view != null) {\n                    view.debug(\"BitmapLoadTask.doInBackground\");\n                    bitmap = decoderFactory.make().decode(context, source);\n                    return view.getExifOrientation(context, sourceUri);\n                }\n            } catch (Exception e) {\n                Log.e(TAG, \"Failed to load bitmap\", e);\n                this.exception = e;\n            } catch (OutOfMemoryError e) {\n                Log.e(TAG, \"Failed to load bitmap - OutOfMemoryError\", e);\n                this.exception = new RuntimeException(e);\n            }\n            return null;\n        }\n\n        @Override\n        protected void onPostExecute(Integer orientation) {\n            SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();\n            if (subsamplingScaleImageView != null) {\n                if (bitmap != null && orientation != null) {\n                    if (preview) {\n                        subsamplingScaleImageView.onPreviewLoaded(bitmap);\n                    } else {\n                        subsamplingScaleImageView.onImageLoaded(bitmap, orientation, false);\n                    }\n                } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) {\n                    if (preview) {\n                        subsamplingScaleImageView.onImageEventListener.onPreviewLoadError(exception);\n                    } else {\n                        subsamplingScaleImageView.onImageEventListener.onImageLoadError(exception);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Called by worker task when preview image is loaded.\n     */\n    private synchronized void onPreviewLoaded(Bitmap previewBitmap) {\n        debug(\"onPreviewLoaded\");\n        if (bitmap != null || imageLoadedSent) {\n            previewBitmap.recycle();\n            return;\n        }\n        if (pRegion != null) {\n            bitmap = Bitmap.createBitmap(previewBitmap, pRegion.left, pRegion.top, pRegion.width(), pRegion.height());\n        } else {\n            bitmap = previewBitmap;\n        }\n        bitmapIsPreview = true;\n        if (checkReady()) {\n            invalidate();\n            requestLayout();\n        }\n    }\n\n    /**\n     * Called by worker task when full size image bitmap is ready (tiling is disabled).\n     */\n    private synchronized void onImageLoaded(Bitmap bitmap, int sOrientation, boolean bitmapIsCached) {\n        debug(\"onImageLoaded\");\n        // If actual dimensions don't match the declared size, reset everything.\n        if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != bitmap.getWidth() || this.sHeight != bitmap.getHeight())) {\n            reset(false);\n        }\n        if (this.bitmap != null && !this.bitmapIsCached) {\n            this.bitmap.recycle();\n        }\n\n        if (this.bitmap != null && this.bitmapIsCached && onImageEventListener!=null) {\n            onImageEventListener.onPreviewReleased();\n        }\n\n        this.bitmapIsPreview = false;\n        this.bitmapIsCached = bitmapIsCached;\n        this.bitmap = bitmap;\n        this.sWidth = bitmap.getWidth();\n        this.sHeight = bitmap.getHeight();\n        this.sOrientation = sOrientation;\n        boolean ready = checkReady();\n        boolean imageLoaded = checkImageLoaded();\n        if (ready || imageLoaded) {\n            invalidate();\n            requestLayout();\n        }\n    }\n\n    /**\n     * Helper method for load tasks. Examines the EXIF info on the image file to determine the orientation.\n     * This will only work for external files, not assets, resources or other URIs.\n     */\n    @AnyThread\n    private int getExifOrientation(Context context, String sourceUri) {\n        int exifOrientation = ORIENTATION_0;\n        if (sourceUri.startsWith(ContentResolver.SCHEME_CONTENT)) {\n            Cursor cursor = null;\n            try {\n                String[] columns = { MediaStore.Images.Media.ORIENTATION };\n                cursor = context.getContentResolver().query(Uri.parse(sourceUri), columns, null, null, null);\n                if (cursor != null) {\n                    if (cursor.moveToFirst()) {\n                        int orientation = cursor.getInt(0);\n                        if (VALID_ORIENTATIONS.contains(orientation) && orientation != ORIENTATION_USE_EXIF) {\n                            exifOrientation = orientation;\n                        } else {\n                            Log.w(TAG, \"Unsupported orientation: \" + orientation);\n                        }\n                    }\n                }\n            } catch (Exception e) {\n                Log.w(TAG, \"Could not get orientation of image from media store\");\n            } finally {\n                if (cursor != null) {\n                    cursor.close();\n                }\n            }\n        } else if (sourceUri.startsWith(ImageSource.FILE_SCHEME) && !sourceUri.startsWith(ImageSource.ASSET_SCHEME)) {\n            try {\n                ExifInterface exifInterface = new ExifInterface(sourceUri.substring(ImageSource.FILE_SCHEME.length() - 1));\n                int orientationAttr = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);\n                if (orientationAttr == ExifInterface.ORIENTATION_NORMAL || orientationAttr == ExifInterface.ORIENTATION_UNDEFINED) {\n                    exifOrientation = ORIENTATION_0;\n                } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_90) {\n                    exifOrientation = ORIENTATION_90;\n                } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_180) {\n                    exifOrientation = ORIENTATION_180;\n                } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_270) {\n                    exifOrientation = ORIENTATION_270;\n                } else {\n                    Log.w(TAG, \"Unsupported EXIF orientation: \" + orientationAttr);\n                }\n            } catch (Exception e) {\n                Log.w(TAG, \"Could not get EXIF orientation of image\");\n            }\n        }\n        return exifOrientation;\n    }\n\n    private void execute(AsyncTask<Void, Void, ?> asyncTask) {\n        if (parallelLoadingEnabled && VERSION.SDK_INT >= 11) {\n            try {\n                Field executorField = AsyncTask.class.getField(\"THREAD_POOL_EXECUTOR\");\n                Executor executor = (Executor)executorField.get(null);\n                Method executeMethod = AsyncTask.class.getMethod(\"executeOnExecutor\", Executor.class, Object[].class);\n                executeMethod.invoke(asyncTask, executor, null);\n                return;\n            } catch (Exception e) {\n                Log.i(TAG, \"Failed to execute AsyncTask on thread pool executor, falling back to single threaded executor\", e);\n            }\n        }\n        asyncTask.execute();\n    }\n\n    private static class Tile {\n\n        private Rect sRect;\n        private int sampleSize;\n        private Bitmap bitmap;\n        private boolean loading;\n        private boolean visible;\n\n        // Volatile fields instantiated once then updated before use to reduce GC.\n        private Rect vRect;\n        private Rect fileSRect;\n\n    }\n\n    private static class Anim {\n\n        private float scaleStart; // Scale at start of anim\n        private float scaleEnd; // Scale at end of anim (target)\n        private PointF sCenterStart; // Source center point at start\n        private PointF sCenterEnd; // Source center point at end, adjusted for pan limits\n        private PointF sCenterEndRequested; // Source center point that was requested, without adjustment\n        private PointF vFocusStart; // View point that was double tapped\n        private PointF vFocusEnd; // Where the view focal point should be moved to during the anim\n        private long duration = 500; // How long the anim takes\n        private boolean interruptible = true; // Whether the anim can be interrupted by a touch\n        private int easing = EASE_IN_OUT_QUAD; // Easing style\n        private int origin = ORIGIN_ANIM; // Animation origin (API, double tap or fling)\n        private long time = System.currentTimeMillis(); // Start time\n        private OnAnimationEventListener listener; // Event listener\n\n    }\n\n    private static class ScaleAndTranslate {\n        private ScaleAndTranslate(float scale, PointF vTranslate) {\n            this.scale = scale;\n            this.vTranslate = vTranslate;\n        }\n        private float scale;\n        private PointF vTranslate;\n    }\n\n    /**\n     * Set scale, center and orientation from saved state.\n     */\n    private void restoreState(ImageViewState state) {\n        if (state != null && state.getCenter() != null && VALID_ORIENTATIONS.contains(state.getOrientation())) {\n            this.orientation = state.getOrientation();\n            this.pendingScale = state.getScale();\n            this.sPendingCenter = state.getCenter();\n            invalidate();\n        }\n    }\n\n    /**\n     * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour.\n     *\n     * @param maxPixels Maximum tile size X and Y in pixels.\n     */\n    public void setMaxTileSize(int maxPixels) {\n        this.maxTileWidth = maxPixels;\n        this.maxTileHeight = maxPixels;\n    }\n\n    /**\n     * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour.\n     *\n     * @param maxPixelsX Maximum tile width.\n     * @param maxPixelsY Maximum tile height.\n     */\n    public void setMaxTileSize(int maxPixelsX, int maxPixelsY) {\n        this.maxTileWidth = maxPixelsX;\n        this.maxTileHeight = maxPixelsY;\n    }\n\n    /**\n     * In SDK 14 and above, use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling.\n     */\n    private Point getMaxBitmapDimensions(Canvas canvas) {\n        int maxWidth = 2048;\n        int maxHeight = 2048;\n        if (VERSION.SDK_INT >= 14) {\n            try {\n                maxWidth = (Integer)Canvas.class.getMethod(\"getMaximumBitmapWidth\").invoke(canvas);\n                maxHeight = (Integer)Canvas.class.getMethod(\"getMaximumBitmapHeight\").invoke(canvas);\n            } catch (Exception e) {\n                // Return default\n            }\n        }\n        return new Point(Math.min(maxWidth, maxTileWidth), Math.min(maxHeight, maxTileHeight));\n    }\n\n    /**\n     * Get source width taking rotation into account.\n     */\n    @SuppressWarnings(\"SuspiciousNameCombination\")\n    private int sWidth() {\n        int rotation = getRequiredRotation();\n        if (rotation == 90 || rotation == 270) {\n            return sHeight;\n        } else {\n            return sWidth;\n        }\n    }\n\n    /**\n     * Get source height taking rotation into account.\n     */\n    @SuppressWarnings(\"SuspiciousNameCombination\")\n    private int sHeight() {\n        int rotation = getRequiredRotation();\n        if (rotation == 90 || rotation == 270) {\n            return sWidth;\n        } else {\n            return sHeight;\n        }\n    }\n\n    /**\n     * Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already,\n     * to the rectangle of the image that needs to be loaded.\n     */\n    @SuppressWarnings(\"SuspiciousNameCombination\")\n    @AnyThread\n    private void fileSRect(Rect sRect, Rect target) {\n        if (getRequiredRotation() == 0) {\n            target.set(sRect);\n        } else if (getRequiredRotation() == 90) {\n            target.set(sRect.top, sHeight - sRect.right, sRect.bottom, sHeight - sRect.left);\n        } else if (getRequiredRotation() == 180) {\n            target.set(sWidth - sRect.right, sHeight - sRect.bottom, sWidth - sRect.left, sHeight - sRect.top);\n        } else {\n            target.set(sWidth - sRect.bottom, sRect.left, sWidth - sRect.top, sRect.right);\n        }\n    }\n\n    /**\n     * Determines the rotation to be applied to tiles, based on EXIF orientation or chosen setting.\n     */\n    @AnyThread\n    private int getRequiredRotation() {\n        if (orientation == ORIENTATION_USE_EXIF) {\n            return sOrientation;\n        } else {\n            return orientation;\n        }\n    }\n\n    /**\n     * Pythagoras distance between two points.\n     */\n    private float distance(float x0, float x1, float y0, float y1) {\n        float x = x0 - x1;\n        float y = y0 - y1;\n        return (float) Math.sqrt(x * x + y * y);\n    }\n\n    /**\n     * Releases all resources the view is using and resets the state, nulling any fields that use significant memory.\n     * After you have called this method, the view can be re-used by setting a new image. Settings are remembered\n     * but state (scale and center) is forgotten. You can restore these yourself if required.\n     */\n    public void recycle() {\n        reset(true);\n        bitmapPaint = null;\n        debugPaint = null;\n        tileBgPaint = null;\n    }\n\n    /**\n     * Convert screen to source x coordinate.\n     */\n    private float viewToSourceX(float vx) {\n        if (vTranslate == null) { return Float.NaN; }\n        return (vx - vTranslate.x)/scale;\n    }\n\n    /**\n     * Convert screen to source y coordinate.\n     */\n    private float viewToSourceY(float vy) {\n        if (vTranslate == null) { return Float.NaN; }\n        return (vy - vTranslate.y)/scale;\n    }\n\n    /**\n     * Convert screen coordinate to source coordinate.\n     */\n    public final PointF viewToSourceCoord(PointF vxy) {\n        return viewToSourceCoord(vxy.x, vxy.y, new PointF());\n    }\n\n    /**\n     * Convert screen coordinate to source coordinate.\n     */\n    public final PointF viewToSourceCoord(float vx, float vy) {\n        return viewToSourceCoord(vx, vy, new PointF());\n    }\n\n    /**\n     * Convert screen coordinate to source coordinate.\n     */\n    public final PointF viewToSourceCoord(PointF vxy, PointF sTarget) {\n        return viewToSourceCoord(vxy.x, vxy.y, sTarget);\n    }\n\n    /**\n     * Convert screen coordinate to source coordinate.\n     */\n    public final PointF viewToSourceCoord(float vx, float vy, PointF sTarget) {\n        if (vTranslate == null) {\n            return null;\n        }\n        sTarget.set(viewToSourceX(vx), viewToSourceY(vy));\n        return sTarget;\n    }\n\n    /**\n     * Convert source to screen x coordinate.\n     */\n    private float sourceToViewX(float sx) {\n        if (vTranslate == null) { return Float.NaN; }\n        return (sx * scale) + vTranslate.x;\n    }\n\n    /**\n     * Convert source to screen y coordinate.\n     */\n    private float sourceToViewY(float sy) {\n        if (vTranslate == null) { return Float.NaN; }\n        return (sy * scale) + vTranslate.y;\n    }\n\n    /**\n     * Convert source coordinate to screen coordinate.\n     */\n    public final PointF sourceToViewCoord(PointF sxy) {\n        return sourceToViewCoord(sxy.x, sxy.y, new PointF());\n    }\n\n    /**\n     * Convert source coordinate to screen coordinate.\n     */\n    public final PointF sourceToViewCoord(float sx, float sy) {\n        return sourceToViewCoord(sx, sy, new PointF());\n    }\n\n    /**\n     * Convert source coordinate to screen coordinate.\n     */\n    public final PointF sourceToViewCoord(PointF sxy, PointF vTarget) {\n        return sourceToViewCoord(sxy.x, sxy.y, vTarget);\n    }\n\n    /**\n     * Convert source coordinate to screen coordinate.\n     */\n    public final PointF sourceToViewCoord(float sx, float sy, PointF vTarget) {\n        if (vTranslate == null) {\n            return null;\n        }\n        vTarget.set(sourceToViewX(sx), sourceToViewY(sy));\n        return vTarget;\n    }\n\n    /**\n     * Convert source rect to screen rect, integer values.\n     */\n    private Rect sourceToViewRect(Rect sRect, Rect vTarget) {\n        vTarget.set(\n            (int)sourceToViewX(sRect.left),\n            (int)sourceToViewY(sRect.top),\n            (int)sourceToViewX(sRect.right),\n            (int)sourceToViewY(sRect.bottom)\n        );\n        return vTarget;\n    }\n\n    /**\n     * Get the translation required to place a given source coordinate at the center of the screen, with the center\n     * adjusted for asymmetric padding. Accepts the desired scale as an argument, so this is independent of current\n     * translate and scale. The result is fitted to bounds, putting the image point as near to the screen center as permitted.\n     */\n    private PointF vTranslateForSCenter(float sCenterX, float sCenterY, float scale) {\n        int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2;\n        int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2;\n        if (satTemp == null) {\n            satTemp = new ScaleAndTranslate(0, new PointF(0, 0));\n        }\n        satTemp.scale = scale;\n        satTemp.vTranslate.set(vxCenter - (sCenterX * scale), vyCenter - (sCenterY * scale));\n        fitToBounds(true, satTemp);\n        return satTemp.vTranslate;\n    }\n\n    /**\n     * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in\n     * pan limits, keeping the requested center as near to the middle of the screen as allowed.\n     */\n    private PointF limitedSCenter(float sCenterX, float sCenterY, float scale, PointF sTarget) {\n        PointF vTranslate = vTranslateForSCenter(sCenterX, sCenterY, scale);\n        int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2;\n        int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2;\n        float sx = (vxCenter - vTranslate.x)/scale;\n        float sy = (vyCenter - vTranslate.y)/scale;\n        sTarget.set(sx, sy);\n        return sTarget;\n    }\n\n    /**\n     * Returns the minimum allowed scale.\n     */\n    private float minScale() {\n        int vPadding = getPaddingBottom() + getPaddingTop();\n        int hPadding = getPaddingLeft() + getPaddingRight();\n        if (minimumScaleType == SCALE_TYPE_CENTER_CROP) {\n            return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());\n        } else if (minimumScaleType == SCALE_TYPE_CUSTOM && minScale > 0) {\n            return minScale;\n        } else {\n            return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());\n        }\n    }\n\n    /**\n     * Adjust a requested scale to be within the allowed limits.\n     */\n    private float limitedScale(float targetScale) {\n        targetScale = Math.max(minScale(), targetScale);\n        targetScale = Math.min(maxScale, targetScale);\n        return targetScale;\n    }\n\n    /**\n     * Apply a selected type of easing.\n     * @param type Easing type, from static fields\n     * @param time Elapsed time\n     * @param from Start value\n     * @param change Target value\n     * @param duration Anm duration\n     * @return Current value\n     */\n    private float ease(int type, long time, float from, float change, long duration) {\n        switch (type) {\n            case EASE_IN_OUT_QUAD:\n                return easeInOutQuad(time, from, change, duration);\n            case EASE_OUT_QUAD:\n                return easeOutQuad(time, from, change, duration);\n            default:\n                throw new IllegalStateException(\"Unexpected easing type: \" + type);\n        }\n    }\n\n    /**\n     * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/\n     * @param time Elapsed time\n     * @param from Start value\n     * @param change Target value\n     * @param duration Anm duration\n     * @return Current value\n     */\n    private float easeOutQuad(long time, float from, float change, long duration) {\n        float progress = (float)time/(float)duration;\n        return -change * progress*(progress-2) + from;\n    }\n\n    /**\n     * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/\n     * @param time Elapsed time\n     * @param from Start value\n     * @param change Target value\n     * @param duration Anm duration\n     * @return Current value\n     */\n    private float easeInOutQuad(long time, float from, float change, long duration) {\n        float timeF = time/(duration/2f);\n        if (timeF < 1) {\n            return (change/2f * timeF * timeF) + from;\n        } else {\n            timeF--;\n            return (-change/2f) * (timeF * (timeF - 2) - 1) + from;\n        }\n    }\n\n    /**\n     * Debug logger\n     */\n    @AnyThread\n    private void debug(String message, Object... args) {\n        if (debug) {\n            Log.d(TAG, String.format(message, args));\n        }\n    }\n\n    /**\n     *\n     * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or\n     * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a\n     * public default constructor.\n     * @param regionDecoderClass The {@link ImageRegionDecoder} implementation to use.\n     */\n    public final void setRegionDecoderClass(Class<? extends ImageRegionDecoder> regionDecoderClass) {\n        if (regionDecoderClass == null) {\n            throw new IllegalArgumentException(\"Decoder class cannot be set to null\");\n        }\n        this.regionDecoderFactory = new CompatDecoderFactory<>(regionDecoderClass);\n    }\n\n    /**\n     * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or\n     * asset, and you cannot use a custom decoder when using layout XML to set an asset name.\n     * @param regionDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageRegionDecoder}\n     *                             instances.\n     */\n    public final void setRegionDecoderFactory(DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory) {\n        if (regionDecoderFactory == null) {\n            throw new IllegalArgumentException(\"Decoder factory cannot be set to null\");\n        }\n        this.regionDecoderFactory = regionDecoderFactory;\n    }\n\n    /**\n     * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or\n     * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a\n     * public default constructor.\n     * @param bitmapDecoderClass The {@link ImageDecoder} implementation to use.\n     */\n    public final void setBitmapDecoderClass(Class<? extends ImageDecoder> bitmapDecoderClass) {\n        if (bitmapDecoderClass == null) {\n            throw new IllegalArgumentException(\"Decoder class cannot be set to null\");\n        }\n        this.bitmapDecoderFactory = new CompatDecoderFactory<>(bitmapDecoderClass);\n    }\n\n    /**\n     * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or\n     * asset, and you cannot use a custom decoder when using layout XML to set an asset name.\n     * @param bitmapDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageDecoder} instances.\n     */\n    public final void setBitmapDecoderFactory(DecoderFactory<? extends ImageDecoder> bitmapDecoderFactory) {\n        if (bitmapDecoderFactory == null) {\n            throw new IllegalArgumentException(\"Decoder factory cannot be set to null\");\n        }\n        this.bitmapDecoderFactory = bitmapDecoderFactory;\n    }\n\n    /**\n     * Set the pan limiting style. See static fields. Normally {@link #PAN_LIMIT_INSIDE} is best, for image galleries.\n     */\n    public final void setPanLimit(int panLimit) {\n        if (!VALID_PAN_LIMITS.contains(panLimit)) {\n            throw new IllegalArgumentException(\"Invalid pan limit: \" + panLimit);\n        }\n        this.panLimit = panLimit;\n        if (isReady()) {\n            fitToBounds(true);\n            invalidate();\n        }\n    }\n\n    /**\n     * Set the minimum scale type. See static fields. Normally {@link #SCALE_TYPE_CENTER_INSIDE} is best, for image galleries.\n     */\n    public final void setMinimumScaleType(int scaleType) {\n        if (!VALID_SCALE_TYPES.contains(scaleType)) {\n            throw new IllegalArgumentException(\"Invalid scale type: \" + scaleType);\n        }\n        this.minimumScaleType = scaleType;\n        if (isReady()) {\n            fitToBounds(true);\n            invalidate();\n        }\n    }\n\n    /**\n     * Set the maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according\n     * to screen density - on a retina screen, 1:1 may still be too small. Consider using {@link #setMinimumDpi(int)},\n     * which is density aware.\n     */\n    public final void setMaxScale(float maxScale) {\n        this.maxScale = maxScale;\n    }\n\n    /**\n     * Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according\n     * to screen density. Consider using {@link #setMaximumDpi(int)}, which is density aware.\n     */\n    public final void setMinScale(float minScale) {\n        this.minScale = minScale;\n    }\n\n    /**\n     * This is a screen density aware alternative to {@link #setMaxScale(float)}; it allows you to express the maximum\n     * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being\n     * too small on a high density screen. A sensible starting point is 160 - the default used by this view.\n     * @param dpi Source image pixel density at maximum zoom.\n     */\n    public final void setMinimumDpi(int dpi) {\n        DisplayMetrics metrics = getResources().getDisplayMetrics();\n        float averageDpi = (metrics.xdpi + metrics.ydpi)/2;\n        setMaxScale(averageDpi/dpi);\n    }\n\n    /**\n     * This is a screen density aware alternative to {@link #setMinScale(float)}; it allows you to express the minimum\n     * allowed scale in terms of the maximum pixel density.\n     * @param dpi Source image pixel density at minimum zoom.\n     */\n    public final void setMaximumDpi(int dpi) {\n        DisplayMetrics metrics = getResources().getDisplayMetrics();\n        float averageDpi = (metrics.xdpi + metrics.ydpi)/2;\n        setMinScale(averageDpi / dpi);\n    }\n\n    /**\n     * Returns the maximum allowed scale.\n     */\n    public float getMaxScale() {\n        return maxScale;\n    }\n\n    /**\n     * Returns the minimum allowed scale.\n     */\n    public final float getMinScale() {\n        return minScale();\n    }\n\n    /**\n     * By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be\n     * necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher\n     * resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower\n     * quality image. 160-240dpi will usually be enough. This should be called before setting the image source,\n     * because it affects which tiles get loaded. When using an untiled source image this method has no effect.\n     * @param minimumTileDpi Tile loading threshold.\n     */\n    public void setMinimumTileDpi(int minimumTileDpi) {\n        DisplayMetrics metrics = getResources().getDisplayMetrics();\n        float averageDpi = (metrics.xdpi + metrics.ydpi)/2;\n        this.minimumTileDpi = (int)Math.min(averageDpi, minimumTileDpi);\n        if (isReady()) {\n            reset(false);\n            invalidate();\n        }\n    }\n\n    /**\n     * Returns the source point at the center of the view.\n     */\n    public final PointF getCenter() {\n        int mX = getWidth()/2;\n        int mY = getHeight()/2;\n        return viewToSourceCoord(mX, mY);\n    }\n\n    /**\n     * Returns the current scale value.\n     */\n    public final float getScale() {\n        return scale;\n    }\n\n    /**\n     * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale()\n     * to restore the scale and zoom after a screen rotate.\n     * @param scale New scale to set.\n     * @param sCenter New source image coordinate to center on the screen, subject to boundaries.\n     */\n    public final void setScaleAndCenter(float scale, PointF sCenter) {\n        this.anim = null;\n        this.pendingScale = scale;\n        this.sPendingCenter = sCenter;\n        this.sRequestedCenter = sCenter;\n        invalidate();\n    }\n\n    /**\n     * Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager\n     * and want images to be reset when the user has moved to another page.\n     */\n    public final void resetScaleAndCenter() {\n        this.anim = null;\n        this.pendingScale = limitedScale(0);\n        if (isReady()) {\n            this.sPendingCenter = new PointF(sWidth()/2, sHeight()/2);\n        } else {\n            this.sPendingCenter = new PointF(0, 0);\n        }\n        invalidate();\n    }\n\n    /**\n     * Call to find whether the view is initialised, has dimensions, and will display an image on\n     * the next draw. If a preview has been provided, it may be the preview that will be displayed\n     * and the full size image may still be loading. If no preview was provided, this is called once\n     * the base layer tiles of the full size image are loaded.\n     */\n    public final boolean isReady() {\n        return readySent;\n    }\n\n    /**\n     * Called once when the view is initialised, has dimensions, and will display an image on the\n     * next draw. This is triggered at the same time as {@link OnImageEventListener#onReady()} but\n     * allows a subclass to receive this event without using a listener.\n     */\n    protected void onReady() {\n\n    }\n\n    /**\n     * Call to find whether the main image (base layer tiles where relevant) have been loaded. Before\n     * this event the view is blank unless a preview was provided.\n     */\n    public final boolean isImageLoaded() {\n        return imageLoadedSent;\n    }\n\n    /**\n     * Called once when the full size image or its base layer tiles have been loaded.\n     */\n    protected void onImageLoaded() {\n\n    }\n\n    /**\n     * Get source width, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSHeight()}\n     * for the apparent width.\n     */\n    public final int getSWidth() {\n        return sWidth;\n    }\n\n    /**\n     * Get source height, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSWidth()}\n     * for the apparent height.\n     */\n    public final int getSHeight() {\n        return sHeight;\n    }\n\n    /**\n     * Returns the orientation setting. This can return {@link #ORIENTATION_USE_EXIF}, in which case it doesn't tell you\n     * the applied orientation of the image. For that, use {@link #getAppliedOrientation()}.\n     */\n    public final int getOrientation() {\n        return orientation;\n    }\n\n    /**\n     * Returns the actual orientation of the image relative to the source file. This will be based on the source file's\n     * EXIF orientation if you're using ORIENTATION_USE_EXIF. Values are 0, 90, 180, 270.\n     */\n    public final int getAppliedOrientation() {\n        return getRequiredRotation();\n    }\n\n    /**\n     * Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if\n     * the view is not ready.\n     */\n    public final ImageViewState getState() {\n        if (vTranslate != null && sWidth > 0 && sHeight > 0) {\n            return new ImageViewState(getScale(), getCenter(), getOrientation());\n        }\n        return null;\n    }\n\n    /**\n     * Returns true if zoom gesture detection is enabled.\n     */\n    public final boolean isZoomEnabled() {\n        return zoomEnabled;\n    }\n\n    /**\n     * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale.\n     */\n    public final void setZoomEnabled(boolean zoomEnabled) {\n        this.zoomEnabled = zoomEnabled;\n    }\n\n    /**\n     * Returns true if double tap &amp; swipe to zoom is enabled.\n     */\n    public final boolean isQuickScaleEnabled() {\n        return quickScaleEnabled;\n    }\n\n    /**\n     * Enable or disable double tap &amp; swipe to zoom.\n     */\n    public final void setQuickScaleEnabled(boolean quickScaleEnabled) {\n        this.quickScaleEnabled = quickScaleEnabled;\n    }\n\n    /**\n     * Returns true if pan gesture detection is enabled.\n     */\n    public final boolean isPanEnabled() {\n        return panEnabled;\n    }\n\n    /**\n     * Enable or disable pan gesture detection. Disabling pan causes the image to be centered.\n     */\n    public final void setPanEnabled(boolean panEnabled) {\n        this.panEnabled = panEnabled;\n        if (!panEnabled && vTranslate != null) {\n            vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2));\n            vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2));\n            if (isReady()) {\n                refreshRequiredTiles(true);\n                invalidate();\n            }\n        }\n    }\n\n    /**\n     * Set a solid color to render behind tiles, useful for displaying transparent PNGs.\n     * @param tileBgColor Background color for tiles.\n     */\n    public final void setTileBackgroundColor(int tileBgColor) {\n        if (Color.alpha(tileBgColor) == 0) {\n            tileBgPaint = null;\n        } else {\n            tileBgPaint = new Paint();\n            tileBgPaint.setStyle(Style.FILL);\n            tileBgPaint.setColor(tileBgColor);\n        }\n        invalidate();\n    }\n\n    /**\n     * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted\n     * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values\n     * greater than the max zoom.\n     * @param doubleTapZoomScale New value for double tap gesture zoom scale.\n     */\n    public final void setDoubleTapZoomScale(float doubleTapZoomScale) {\n        this.doubleTapZoomScale = doubleTapZoomScale;\n    }\n\n    /**\n     * A density aware alternative to {@link #setDoubleTapZoomScale(float)}; this allows you to express the scale the\n     * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will\n     * be ignored. A sensible starting point is 160 - the default used by this view.\n     * @param dpi New value for double tap gesture zoom scale.\n     */\n    public final void setDoubleTapZoomDpi(int dpi) {\n        DisplayMetrics metrics = getResources().getDisplayMetrics();\n        float averageDpi = (metrics.xdpi + metrics.ydpi)/2;\n        setDoubleTapZoomScale(averageDpi/dpi);\n    }\n\n    /**\n     * Set the type of zoom animation to be used for double taps. See static fields.\n     * @param doubleTapZoomStyle New value for zoom style.\n     */\n    public final void setDoubleTapZoomStyle(int doubleTapZoomStyle) {\n        if (!VALID_ZOOM_STYLES.contains(doubleTapZoomStyle)) {\n            throw new IllegalArgumentException(\"Invalid zoom style: \" + doubleTapZoomStyle);\n        }\n        this.doubleTapZoomStyle = doubleTapZoomStyle;\n    }\n\n    /**\n     * Set the duration of the double tap zoom animation.\n     * @param durationMs Duration in milliseconds.\n     */\n    public final void setDoubleTapZoomDuration(int durationMs) {\n        this.doubleTapZoomDuration = Math.max(0, durationMs);\n    }\n\n    /**\n     * Toggle parallel loading. When enabled, tiles are loaded using the thread pool executor available\n     * in SDK 11+. In older versions this has no effect. Parallel loading may use more memory and there\n     * is a possibility that it will make the tile loading unreliable, but it reduces the chances of\n     * an app's background processes blocking loading.\n     * @param parallelLoadingEnabled Whether to run AsyncTasks using a thread pool executor.\n     */\n    public void setParallelLoadingEnabled(boolean parallelLoadingEnabled) {\n        this.parallelLoadingEnabled = parallelLoadingEnabled;\n    }\n\n    /**\n     * Enables visual debugging, showing tile boundaries and sizes.\n     */\n    public final void setDebug(boolean debug) {\n        this.debug = debug;\n    }\n\n    /**\n     * Check if an image has been set. The image may not have been loaded and displayed yet.\n     * @return If an image is currently set.\n     */\n    public boolean hasImage() {\n        return uri != null || bitmap != null;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void setOnLongClickListener(OnLongClickListener onLongClickListener) {\n        this.onLongClickListener = onLongClickListener;\n    }\n\n    /**\n     * Add a listener allowing notification of load and error events.\n     */\n    public void setOnImageEventListener(OnImageEventListener onImageEventListener) {\n        this.onImageEventListener = onImageEventListener;\n    }\n\n    /**\n     * Add a listener for pan and zoom events.\n     */\n    public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) {\n        this.onStateChangedListener = onStateChangedListener;\n    }\n\n    private void sendStateChanged(float oldScale, PointF oldVTranslate, int origin) {\n        if (onStateChangedListener != null) {\n            if (scale != oldScale) {\n                onStateChangedListener.onScaleChanged(scale, origin);\n            }\n            if (!vTranslate.equals(oldVTranslate)) {\n                onStateChangedListener.onCenterChanged(getCenter(), origin);\n            }\n        }\n    }\n\n    /**\n     * Creates a panning animation builder, that when started will animate the image to place the given coordinates of\n     * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the\n     * image is instead animated to move the center point as near to the center of the screen as is allowed - it's\n     * guaranteed to be on screen.\n     * @param sCenter Target center point\n     * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim.\n     */\n    public AnimationBuilder animateCenter(PointF sCenter) {\n        if (!isReady()) {\n            return null;\n        }\n        return new AnimationBuilder(sCenter);\n    }\n\n    /**\n     * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image\n     * beyond the panning limits, the image is automatically panned during the animation.\n     * @param scale Target scale.\n     * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim.\n     */\n    public AnimationBuilder animateScale(float scale) {\n        if (!isReady()) {\n            return null;\n        }\n        return new AnimationBuilder(scale);\n    }\n\n    /**\n     * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image\n     * beyond the panning limits, the image is automatically panned during the animation.\n     * @param scale Target scale.\n     * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim.\n     */\n    public AnimationBuilder animateScaleAndCenter(float scale, PointF sCenter) {\n        if (!isReady()) {\n            return null;\n        }\n        return new AnimationBuilder(scale, sCenter);\n    }\n\n    /**\n     * Builder class used to set additional options for a scale animation. Create an instance using {@link #animateScale(float)},\n     * then set your options and call {@link #start()}.\n     */\n    public final class AnimationBuilder {\n\n        private final float targetScale;\n        private final PointF targetSCenter;\n        private final PointF vFocus;\n        private long duration = 500;\n        private int easing = EASE_IN_OUT_QUAD;\n        private int origin = ORIGIN_ANIM;\n        private boolean interruptible = true;\n        private boolean panLimited = true;\n        private OnAnimationEventListener listener;\n\n        private AnimationBuilder(PointF sCenter) {\n            this.targetScale = scale;\n            this.targetSCenter = sCenter;\n            this.vFocus = null;\n        }\n\n        private AnimationBuilder(float scale) {\n            this.targetScale = scale;\n            this.targetSCenter = getCenter();\n            this.vFocus = null;\n        }\n\n        private AnimationBuilder(float scale, PointF sCenter) {\n            this.targetScale = scale;\n            this.targetSCenter = sCenter;\n            this.vFocus = null;\n        }\n\n        private AnimationBuilder(float scale, PointF sCenter, PointF vFocus) {\n            this.targetScale = scale;\n            this.targetSCenter = sCenter;\n            this.vFocus = vFocus;\n        }\n\n        /**\n         * Desired duration of the anim in milliseconds. Default is 500.\n         * @param duration duration in milliseconds.\n         * @return this builder for method chaining.\n         */\n        public AnimationBuilder withDuration(long duration) {\n            this.duration = duration;\n            return this;\n        }\n\n        /**\n         * Whether the animation can be interrupted with a touch. Default is true.\n         * @param interruptible interruptible flag.\n         * @return this builder for method chaining.\n         */\n        public AnimationBuilder withInterruptible(boolean interruptible) {\n            this.interruptible = interruptible;\n            return this;\n        }\n\n        /**\n         * Set the easing style. See static fields. {@link #EASE_IN_OUT_QUAD} is recommended, and the default.\n         * @param easing easing style.\n         * @return this builder for method chaining.\n         */\n        public AnimationBuilder withEasing(int easing) {\n            if (!VALID_EASING_STYLES.contains(easing)) {\n                throw new IllegalArgumentException(\"Unknown easing type: \" + easing);\n            }\n            this.easing = easing;\n            return this;\n        }\n\n        /**\n         * Add an animation event listener.\n         * @param listener The listener.\n         * @return this builder for method chaining.\n         */\n        public AnimationBuilder withOnAnimationEventListener(OnAnimationEventListener listener) {\n            this.listener = listener;\n            return this;\n        }\n\n        /**\n         * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest\n         * point to the center allowed by pan limits. When false, animation is in the direction of the requested end\n         * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but\n         * nothing else.\n         */\n        private AnimationBuilder withPanLimited(boolean panLimited) {\n            this.panLimited = panLimited;\n            return this;\n        }\n\n        /**\n         * Only for internal use. Indicates what caused the animation.\n         */\n        private AnimationBuilder withOrigin(int origin) {\n            this.origin = origin;\n            return this;\n        }\n\n        /**\n         * Starts the animation.\n         */\n        public void start() {\n            if (anim != null && anim.listener != null) {\n                try {\n                    anim.listener.onInterruptedByNewAnim();\n                } catch (Exception e) {\n                    Log.w(TAG, \"Error thrown by animation listener\", e);\n                }\n            }\n\n            int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2;\n            int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2;\n            float targetScale = limitedScale(this.targetScale);\n            PointF targetSCenter = panLimited ? limitedSCenter(this.targetSCenter.x, this.targetSCenter.y, targetScale, new PointF()) : this.targetSCenter;\n            anim = new Anim();\n            anim.scaleStart = scale;\n            anim.scaleEnd = targetScale;\n            anim.time = System.currentTimeMillis();\n            anim.sCenterEndRequested = targetSCenter;\n            anim.sCenterStart = getCenter();\n            anim.sCenterEnd = targetSCenter;\n            anim.vFocusStart = sourceToViewCoord(targetSCenter);\n            anim.vFocusEnd = new PointF(\n                vxCenter,\n                vyCenter\n            );\n            anim.duration = duration;\n            anim.interruptible = interruptible;\n            anim.easing = easing;\n            anim.origin = origin;\n            anim.time = System.currentTimeMillis();\n            anim.listener = listener;\n\n            if (vFocus != null) {\n                // Calculate where translation will be at the end of the anim\n                float vTranslateXEnd = vFocus.x - (targetScale * anim.sCenterStart.x);\n                float vTranslateYEnd = vFocus.y - (targetScale * anim.sCenterStart.y);\n                ScaleAndTranslate satEnd = new ScaleAndTranslate(targetScale, new PointF(vTranslateXEnd, vTranslateYEnd));\n                // Fit the end translation into bounds\n                fitToBounds(true, satEnd);\n                // Adjust the position of the focus point at end so image will be in bounds\n                anim.vFocusEnd = new PointF(\n                    vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd),\n                    vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd)\n                );\n            }\n\n            invalidate();\n        }\n\n    }\n\n    /**\n     * An event listener for animations, allows events to be triggered when an animation completes,\n     * is aborted by another animation starting, or is aborted by a touch event. Note that none of\n     * these events are triggered if the activity is paused, the image is swapped, or in other cases\n     * where the view's internal state gets wiped or draw events stop.\n     */\n    public interface OnAnimationEventListener {\n\n        /**\n         * The animation has completed, having reached its endpoint.\n         */\n        void onComplete();\n\n        /**\n         * The animation has been aborted before reaching its endpoint because the user touched the screen.\n         */\n        void onInterruptedByUser();\n\n        /**\n         * The animation has been aborted before reaching its endpoint because a new animation has been started.\n         */\n        void onInterruptedByNewAnim();\n\n    }\n\n    /**\n     * Default implementation of {@link OnAnimationEventListener} for extension. This does nothing in any method.\n     */\n    public static class DefaultOnAnimationEventListener implements OnAnimationEventListener {\n\n        @Override public void onComplete() { }\n        @Override public void onInterruptedByUser() { }\n        @Override public void onInterruptedByNewAnim() { }\n\n    }\n\n    /**\n     * An event listener, allowing subclasses and activities to be notified of significant events.\n     */\n    public interface OnImageEventListener {\n\n        /**\n         * Called when the dimensions of the image and view are known, and either a preview image,\n         * the full size image, or base layer tiles are loaded. This indicates the scale and translate\n         * are known and the next draw will display an image. This event can be used to hide a loading\n         * graphic, or inform a subclass that it is safe to draw overlays.\n         */\n        void onReady();\n\n        /**\n         * Called when the full size image is ready. When using tiling, this means the lowest resolution\n         * base layer of tiles are loaded, and when tiling is disabled, the image bitmap is loaded.\n         * This event could be used as a trigger to enable gestures if you wanted interaction disabled\n         * while only a preview is displayed, otherwise for most cases {@link #onReady()} is the best\n         * event to listen to.\n         */\n        void onImageLoaded();\n\n        /**\n         * Called when a preview image could not be loaded. This method cannot be relied upon; certain\n         * encoding types of supported image formats can result in corrupt or blank images being loaded\n         * and displayed with no detectable error. The view will continue to load the full size image.\n         * @param e The exception thrown. This error is logged by the view.\n         */\n        void onPreviewLoadError(Exception e);\n\n        /**\n         * Indicates an error initiliasing the decoder when using a tiling, or when loading the full\n         * size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding\n         * types of supported image formats can result in corrupt or blank images being loaded and\n         * displayed with no detectable error.\n         * @param e The exception thrown. This error is also logged by the view.\n         */\n        void onImageLoadError(Exception e);\n\n        /**\n         * Called when an image tile could not be loaded. This method cannot be relied upon; certain\n         * encoding types of supported image formats can result in corrupt or blank images being loaded\n         * and displayed with no detectable error. Most cases where an unsupported file is used will\n         * result in an error caught by {@link #onImageLoadError(Exception)}.\n         * @param e The exception thrown. This error is logged by the view.\n         */\n        void onTileLoadError(Exception e);\n\n        /**\n        * Called when a bitmap set using ImageSource.cachedBitmap is no longer being used by the View.\n        * This is useful if you wish to manage the bitmap after the preview is shown\n        */\n        void onPreviewReleased();\n    }\n\n    /**\n     * Default implementation of {@link OnImageEventListener} for extension. This does nothing in any method.\n     */\n    public static class DefaultOnImageEventListener implements OnImageEventListener {\n\n        @Override public void onReady() { }\n        @Override public void onImageLoaded() { }\n        @Override public void onPreviewLoadError(Exception e) { }\n        @Override public void onImageLoadError(Exception e) { }\n        @Override public void onTileLoadError(Exception e) { }\n        @Override public void onPreviewReleased() { }\n\n    }\n\n    /**\n     * An event listener, allowing activities to be notified of pan and zoom events. Initialisation\n     * and calls made by your code do not trigger events; touch events and animations do. Methods in\n     * this listener will be called on the UI thread and may be called very frequently - your\n     * implementation should return quickly.\n     */\n    public interface OnStateChangedListener {\n\n        /**\n         * The scale has changed. Use with {@link #getMaxScale()} and {@link #getMinScale()} to determine\n         * whether the image is fully zoomed in or out.\n         * @param newScale The new scale.\n         * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}.\n         */\n        void onScaleChanged(float newScale, int origin);\n\n        /**\n         * The source center has been changed. This can be a result of panning or zooming.\n         * @param newCenter The new source center point.\n         * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}.\n         */\n        void onCenterChanged(PointF newCenter, int origin);\n\n    }\n\n    /**\n     * Default implementation of {@link OnStateChangedListener}. This does nothing in any method.\n     */\n    public static class DefaultOnStateChangedListener implements OnStateChangedListener {\n\n        @Override public void onCenterChanged(PointF newCenter, int origin) { }\n        @Override public void onScaleChanged(float newScale, int origin) { }\n\n    }\n\n}\n"
  },
  {
    "path": "matisse/src/main/res/anim/bottom_down_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n        android:duration=\"@android:integer/config_mediumAnimTime\"\n        android:fromYDelta=\"0\"\n        android:toYDelta=\"100%p\" />\n    <alpha\n        android:duration=\"@android:integer/config_mediumAnimTime\"\n        android:fromAlpha=\"1\"\n        android:toAlpha=\"0.95\" />\n</set>"
  },
  {
    "path": "matisse/src/main/res/anim/bottom_up_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:interpolator/accelerate_decelerate\">\n    <translate\n        android:duration=\"@android:integer/config_mediumAnimTime\"\n        android:fromYDelta=\"100%p\"\n        android:toYDelta=\"0\" />\n    <alpha\n        android:duration=\"@android:integer/config_mediumAnimTime\"\n        android:fromAlpha=\"0.95\"\n        android:toAlpha=\"1\" />\n</set>"
  },
  {
    "path": "matisse/src/main/res/anim/ucrop_anim_fade_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<alpha xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"0\"\n    android:fromAlpha=\"0.2\"\n    android:toAlpha=\"1.0\">\n\n</alpha>"
  },
  {
    "path": "matisse/src/main/res/anim/ucrop_close.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:anim/decelerate_interpolator\">\n    <scale\n        android:duration=\"200\"\n        android:fromXScale=\"1.0\"\n        android:fromYScale=\"1.0\"\n        android:pivotX=\"50.0%\"\n        android:pivotY=\"50.0%\"\n        android:toXScale=\"0.5\"\n        android:toYScale=\"0.5\" />\n    <alpha\n        android:duration=\"200\"\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\" />\n</set>"
  },
  {
    "path": "matisse/src/main/res/anim/ucrop_loader_circle_path.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <objectAnimator\n        android:duration=\"@integer/ucrop_progress_loading_anim_time\"\n        android:propertyName=\"trimPathEnd\"\n        android:repeatCount=\"-1\"\n        android:valueFrom=\"0\"\n        android:valueTo=\"1\"\n        android:valueType=\"floatType\"/>\n\n    <objectAnimator\n        android:duration=\"@integer/ucrop_progress_loading_anim_time\"\n        android:propertyName=\"strokeAlpha\"\n        android:repeatCount=\"-1\"\n        android:valueFrom=\"1\"\n        android:valueTo=\"0\"\n        android:valueType=\"floatType\"/>\n\n</set>"
  },
  {
    "path": "matisse/src/main/res/anim/ucrop_loader_circle_scale.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <objectAnimator\n        android:duration=\"@integer/ucrop_progress_loading_anim_time\"\n        android:propertyName=\"scaleX\"\n        android:repeatCount=\"-1\"\n        android:valueFrom=\"0.666\"\n        android:valueTo=\"1.0\"\n        android:valueType=\"floatType\"/>\n\n    <objectAnimator\n        android:duration=\"@integer/ucrop_progress_loading_anim_time\"\n        android:propertyName=\"scaleY\"\n        android:repeatCount=\"-1\"\n        android:valueFrom=\"0.666\"\n        android:valueTo=\"1.0\"\n        android:valueType=\"floatType\"/>\n\n    <objectAnimator\n        android:duration=\"@integer/ucrop_progress_loading_anim_time\"\n        android:propertyName=\"rotation\"\n        android:repeatCount=\"-1\"\n        android:valueFrom=\"0\"\n        android:valueTo=\"360\"\n        android:valueType=\"floatType\"/>\n\n</set>"
  },
  {
    "path": "matisse/src/main/res/color/selector_base_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/color_base_press\" />\n    <item android:color=\"@color/color_base_press\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/color_base\" android:state_enabled=\"true\" />\n    <item android:color=\"@color/color_base\" android:state_focused=\"true\" />\n</selector>"
  },
  {
    "path": "matisse/src/main/res/color/selector_black_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/color_title_press_text\" />\n    <item android:color=\"@color/color_title_press_text\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/color_title_text\" android:state_enabled=\"true\" />\n    <item android:color=\"@color/color_title_text\" android:state_focused=\"true\" />\n</selector>"
  },
  {
    "path": "matisse/src/main/res/color/selector_white_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/white_press\" />\n    <item android:color=\"@color/white_press\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/white\" android:state_enabled=\"true\" />\n    <item android:color=\"@color/white\" android:state_focused=\"true\" />\n</selector>"
  },
  {
    "path": "matisse/src/main/res/color/ucrop_scale_text_view_selector.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/ucrop_color_widget_active\" android:state_selected=\"true\"/>\n    <item android:color=\"@color/ucrop_color_widget\"/>\n</selector>\n"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_gif_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:topLeftRadius=\"2dp\" />\n    <solid android:color=\"@color/ucrop_color_ba3\" />\n    <padding\n        android:left=\"6dp\"\n        android:right=\"6dp\"\n     />\n</shape>"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_oval_true.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\"\n    android:useLevel=\"false\">\n\n    <solid android:color=\"#00AFFF\" />\n    <stroke\n        android:width=\"1dp\"\n        android:color=\"#00AFFF\" />\n    <size\n        android:width=\"8dp\"\n        android:height=\"8dp\" />\n</shape>"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_shadow_upside.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:shape=\"rectangle\">\n    <gradient\n        android:angle=\"270\"\n        android:endColor=\"#14232323\"\n        android:startColor=\"@android:color/transparent\"/>\n</shape>"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_vector_ic_crop.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportHeight=\"20.0\"\n        android:viewportWidth=\"20.0\">\n    <path\n        android:fillColor=\"@color/ucrop_color_default_logo\"\n        android:pathData=\"M20,1.25L18.75,0L14.66,4.02L6.04,4.02L6.04,0L4,0L4,4.02L0,4.02L0,6.03L4,6.03L4,15.98L14,15.98L14,20L16,20L16,15.98L20,15.98L20,13.99L16,13.99L16,5.33L20,1.25ZM6.03,6.02L12.55,6.02L6.03,12.67L6.03,6.02ZM7.18,13.99L13.99,7.2L13.99,13.99L7.18,13.99ZM7.18,13.99\"/>\n</vector>\n"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_vector_loader.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"@dimen/ucrop_progress_size\"\n        android:height=\"@dimen/ucrop_progress_size\"\n        android:viewportHeight=\"192\"\n        android:viewportWidth=\"192\">\n\n    <group\n        android:name=\"circleGroup\"\n        android:pivotX=\"96.0\"\n        android:pivotY=\"96.0\"\n        android:rotation=\"0\">\n\n        <path\n            android:name=\"circlePath\"\n            android:pathData=\"M96,5.56405C145.946,5.56405 186.436,46.0536 186.436,96C186.436,145.946 145.946,186.436 96,186.436C46.0536,186.436 5.56405,145.946 5.56405,96C5.56405,46.0536 46.0536,5.56405 96,5.56405Z\"\n            android:strokeAlpha=\"1\"\n            android:strokeColor=\"@color/ucrop_color_toolbar_widget\"\n            android:strokeWidth=\"13\"\n            android:trimPathEnd=\"0.7\"/>\n\n    </group>\n\n</vector>"
  },
  {
    "path": "matisse/src/main/res/drawable/ucrop_vector_loader_animated.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                 android:drawable=\"@drawable/ucrop_vector_loader\">\n    <target\n        android:name=\"circleGroup\"\n        android:animation=\"@anim/ucrop_loader_circle_scale\"/>\n    <target\n        android:name=\"circlePath\"\n        android:animation=\"@anim/ucrop_loader_circle_path\"/>\n\n</animated-vector>"
  },
  {
    "path": "matisse/src/main/res/drawable-xhdpi/transparent.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <solid android:color=\"#00fdfdfd\" />\n</shape>\n"
  },
  {
    "path": "matisse/src/main/res/layout/activity_matisse.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    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:animateLayoutChanges=\"true\">\n\n    <include\n        android:id=\"@+id/toolbar\"\n        layout=\"@layout/include_view_navigation\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <include\n        android:id=\"@+id/bottom_view\"\n        layout=\"@layout/include_view_bottom\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n    <FrameLayout\n        android:id=\"@+id/container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toTopOf=\"@+id/bottom_view\"\n        app:layout_constraintTop_toBottomOf=\"@+id/toolbar\" />\n\n    <FrameLayout\n        android:id=\"@+id/empty_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/toolbar\">\n\n        <TextView\n            android:id=\"@+id/empty_view_content\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:drawableTop=\"?attr/Media_Empty_resource\"\n            android:drawablePadding=\"8dp\"\n            android:text=\"?attr/Media_Empty_text\"\n            android:textColor=\"?attr/Media_Empty_textColor\"\n            android:textSize=\"?attr/Media_Empty_textSize\" />\n\n    </FrameLayout>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/activity_media_preview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"?attr/Preview_bg\">\n\n    <com.matisse.widget.PreviewViewPager\n        android:id=\"@+id/pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingBottom=\"50dp\" />\n\n    <include\n        layout=\"@layout/include_view_bottom\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\" />\n\n    <FrameLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end\"\n        android:layout_margin=\"8dp\"\n        android:fitsSystemWindows=\"true\">\n\n        <com.matisse.widget.CheckView\n            android:id=\"@+id/check_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"10dp\" />\n    </FrameLayout>\n\n</FrameLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/dialog_bottom_sheet.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    android:theme=\"@style/Theme.Design.NoActionBar\">\n\n    <FrameLayout\n        android:id=\"@+id/design_bottom_sheet\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:behavior_hideable=\"true\"\n        app:layout_behavior=\"@string/bottom_sheet_behavior\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/dialog_bottom_sheet_folder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.recyclerview.widget.RecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/recyclerview\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@color/white\"\n    android:overScrollMode=\"never\"\n    android:scrollbars=\"none\" />\n"
  },
  {
    "path": "matisse/src/main/res/layout/fragment_media_selection.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.recyclerview.widget.RecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/recyclerview\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clipChildren=\"false\"\n    android:clipToPadding=\"false\"\n    android:overScrollMode=\"never\"\n    android:paddingTop=\"@dimen/media_grid_spacing\"\n    android:paddingBottom=\"@dimen/media_grid_spacing\"\n    android:scrollbars=\"none\" />\n"
  },
  {
    "path": "matisse/src/main/res/layout/fragment_picture_preview_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout 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\n    <com.matisse.photoview.PhotoView\n        android:id=\"@+id/preview_image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <com.matisse.widget.longimage.SubsamplingScaleImageView\n        android:id=\"@+id/longImg\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <ImageView\n        android:id=\"@+id/video_play_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:src=\"@drawable/ic_play_circle_outline_white_48dp\"\n        tools:ignore=\"ContentDescription\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/fragment_preview_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout 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\n    <it.sephiroth.android.library.imagezoom.ImageViewTouch\n        android:id=\"@+id/image_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <ImageView\n        android:id=\"@+id/video_play_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:src=\"@drawable/ic_play_circle_outline_white_48dp\"\n        tools:ignore=\"ContentDescription\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/include_view_bottom.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:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/bottom_toolbar\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?attr/BottomToolbar_height\"\n    android:background=\"?attr/BottomToolbar_bg\"\n    android:elevation=\"2dp\"\n    tools:layout_height=\"45dp\">\n\n    <TextView\n        android:id=\"@+id/button_preview\"\n        style=\"@style/singleLineText\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:foreground=\"?selectableItemBackground\"\n        android:gravity=\"start\"\n        android:padding=\"16dp\"\n        android:text=\"?attr/Media_Preview_text\"\n        android:textColor=\"?attr/Media_Preview_textColor\"\n        android:textSize=\"?attr/Media_Preview_textSize\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@+id/original_layout\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <LinearLayout\n        android:id=\"@+id/original_layout\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:orientation=\"horizontal\"\n        android:padding=\"16dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:showIn=\"@layout/activity_matisse\">\n\n        <com.matisse.widget.CheckRadioView\n            android:id=\"@+id/original\"\n            android:layout_width=\"16dp\"\n            android:layout_height=\"16dp\"\n            android:layout_gravity=\"center_vertical\"\n            android:src=\"@drawable/ic_preview_radio_off\" />\n\n        <TextView\n            style=\"@style/singleLineText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:enabled=\"true\"\n            android:padding=\"4dp\"\n            android:text=\"?attr/Media_Original_text\"\n            android:textColor=\"?attr/Media_Original_textColor\"\n            android:textSize=\"?attr/Media_Original_textSize\" />\n\n        <TextView\n            android:id=\"@+id/tv_size\"\n            style=\"@style/singleLineText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textColor=\"@color/preview_bottom_size\"\n            android:textSize=\"16sp\"\n            android:visibility=\"gone\"\n            tools:text=\"ff\"\n            tools:visibility=\"visible\" />\n\n    </LinearLayout>\n\n    <TextView\n        android:id=\"@+id/button_apply\"\n        style=\"@style/singleLineText\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:foreground=\"?selectableItemBackground\"\n        android:gravity=\"end\"\n        android:padding=\"16dp\"\n        android:text=\"?attr/Media_Album_text\"\n        android:textColor=\"?attr/Media_Album_textColor\"\n        android:textSize=\"?attr/Media_Album_textSize\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@+id/original_layout\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/include_view_navigation.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    android:id=\"@+id/bottom_toolbar\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?attr/Navigation_height\"\n    android:background=\"?attr/Navigation_bg\"\n    android:elevation=\"2dp\">\n\n    <TextView\n        android:id=\"@+id/button_back\"\n        style=\"@style/singleLineText\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"start\"\n        android:drawableStart=\"?attr/Navigation_backRes\"\n        android:drawablePadding=\"5dp\"\n        android:foreground=\"?selectableItemBackground\"\n        android:gravity=\"center_vertical\"\n        android:padding=\"16dp\"\n        android:text=\"?attr/Media_Back_text\"\n        android:textColor=\"?attr/Media_Back_textColor\"\n        android:textSize=\"?attr/Media_Back_textSize\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <TextView\n        android:id=\"@+id/button_complete\"\n        style=\"@style/singleLineText\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end\"\n        android:foreground=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"?attr/Media_Sure_text\"\n        android:textColor=\"?attr/Media_Sure_textColor\"\n        android:textSize=\"?attr/Media_Sure_textSize\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/item_album_folder.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:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"10dp\"\n    android:foreground=\"?selectableItemBackground\"\n    android:gravity=\"center\">\n\n    <ImageView\n        android:id=\"@+id/iv_bucket_cover\"\n        android:layout_width=\"72dp\"\n        android:layout_height=\"72dp\"\n        android:background=\"@drawable/gallery_bg_bucket\"\n        android:padding=\"1dp\"\n        android:scaleType=\"centerCrop\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:ignore=\"ContentDescription\" />\n\n    <TextView\n        android:id=\"@+id/tv_bucket_name\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_centerVertical=\"true\"\n        android:layout_marginStart=\"15dp\"\n        android:ellipsize=\"middle\"\n        android:lineSpacingExtra=\"4dp\"\n        android:singleLine=\"true\"\n        android:textColor=\"?attr/Media_Album_Item_textColor\"\n        android:textSize=\"?attr/Media_Album_Item_textSize\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@+id/rb_selected\"\n        app:layout_constraintStart_toEndOf=\"@+id/iv_bucket_cover\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.matisse.widget.CheckRadioView\n        android:id=\"@+id/rb_selected\"\n        android:layout_width=\"20dp\"\n        android:layout_height=\"20dp\"\n        android:visibility=\"visible\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/item_media_grid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.matisse.widget.MediaGrid xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:foreground=\"?selectableItemBackground\" />"
  },
  {
    "path": "matisse/src/main/res/layout/item_photo_capture.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.matisse.widget.SquareFrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:foreground=\"?selectableItemBackground\"\n    android:layout_height=\"match_parent\">\n\n    <TextView\n        android:id=\"@+id/hint\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"8dp\"\n        android:drawableTop=\"@drawable/ic_photo_camera_white_24dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/photo_grid_capture\"\n        android:textColor=\"?attr/Media_Camera_textColor\"\n        android:textSize=\"?attr/Media_Camera_textSize\" />\n\n</com.matisse.widget.SquareFrameLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_activity_photobox.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ucrop_photobox\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        android:id=\"@+id/toolbar\"\n        layout=\"@layout/include_view_navigation\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <FrameLayout\n        android:id=\"@+id/ucrop_frame\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/ucrop_color_default_crop_frame\">\n\n        <com.matisse.ucrop.view.UCropView\n            android:id=\"@+id/ucrop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:alpha=\"0\" />\n\n    </FrameLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_aspect_ratio.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout style=\"@style/ucrop_WrapperIconState\">\n\n    <com.matisse.ucrop.view.widget.AspectRatioTextView\n        style=\"@style/ucrop_TextViewCropAspectRatio\"/>\n\n</FrameLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_layout_rotate_wheel.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout 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:visibility=\"gone\"\n                tools:showIn=\"@layout/ucrop_activity_photobox\"\n                tools:visibility=\"visible\">\n\n    <TextView\n        android:id=\"@+id/text_view_rotate\"\n        style=\"@style/ucrop_TextViewWidgetText\"\n        android:text=\"100°\"/>\n\n    <com.matisse.ucrop.view.widget.HorizontalProgressWheelView\n        android:id=\"@+id/rotate_scroll_wheel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_below=\"@+id/text_view_rotate\"\n        android:layout_toEndOf=\"@+id/wrapper_reset_rotate\"\n        android:layout_toLeftOf=\"@+id/wrapper_rotate_by_angle\"\n        android:layout_toRightOf=\"@+id/wrapper_reset_rotate\"\n        android:layout_toStartOf=\"@+id/wrapper_rotate_by_angle\"/>\n\n    <FrameLayout\n        android:id=\"@+id/wrapper_reset_rotate\"\n        style=\"@style/ucrop_WrapperRotateButton\"\n        android:layout_centerVertical=\"true\">\n\n        <ImageView\n            style=\"@style/ucrop_ImageViewWidgetIcon\"\n            android:src=\"@drawable/ucrop_ic_reset\"/>\n\n    </FrameLayout>\n\n    <FrameLayout\n        android:id=\"@+id/wrapper_rotate_by_angle\"\n        style=\"@style/ucrop_WrapperRotateButton\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_centerVertical=\"true\">\n\n        <ImageView\n            style=\"@style/ucrop_ImageViewWidgetIcon\"\n            android:src=\"@drawable/ucrop_ic_angle\"/>\n\n    </FrameLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_layout_scale_wheel.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout 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                tools:showIn=\"@layout/ucrop_activity_photobox\"\n                tools:visibility=\"gone\">\n\n    <TextView\n        android:id=\"@+id/text_view_scale\"\n        style=\"@style/ucrop_TextViewWidgetText\"\n        android:text=\"100%\"/>\n\n    <com.matisse.ucrop.view.widget.HorizontalProgressWheelView\n        android:id=\"@+id/scale_scroll_wheel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_below=\"@+id/text_view_scale\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_picture_activity_multi_cutting.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ucrop_mulit_photobox\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/ucrop_color_toolbar\"\n        android:minHeight=\"?attr/actionBarSize\">\n\n        <TextView\n            android:id=\"@+id/toolbar_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:text=\"@string/ucrop_label_edit_photo\"\n            android:textColor=\"@color/ucrop_color_toolbar_widget\"\n            android:textSize=\"18sp\" />\n\n    </androidx.appcompat.widget.Toolbar>\n\n    <FrameLayout\n        android:id=\"@+id/ucrop_frame\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_below=\"@id/toolbar\">\n\n        <com.matisse.ucrop.view.UCropView\n            android:id=\"@+id/ucrop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:alpha=\"0\" />\n\n    </FrameLayout>\n\n</RelativeLayout>\n"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_picture_gf_adapter_edit_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2014 pengjianbo(pengjianbosoft@gmail.com), Inc.\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\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"60dp\"\n    android:layout_height=\"wrap_content\">\n\n    <ImageView\n        android:id=\"@+id/iv_dot\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_marginTop=\"3dp\"\n        android:src=\"@drawable/ucrop_oval_true\" />\n\n    <ImageView\n        android:id=\"@+id/iv_photo\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"60dp\"\n        android:layout_below=\"@id/iv_dot\"\n        android:layout_margin=\"3dp\"\n        android:scaleType=\"centerCrop\" />\n\n    <TextView\n        android:id=\"@+id/tv_gif\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignRight=\"@id/iv_photo\"\n        android:layout_alignBottom=\"@id/iv_photo\"\n        android:background=\"@drawable/ucrop_gif_bg\"\n        android:text=\"@string/ucrop_gif_tag\"\n        android:textColor=\"@color/ucrop_color_toolbar_widget\"\n        android:textSize=\"9sp\"\n        android:visibility=\"gone\" />\n</RelativeLayout>"
  },
  {
    "path": "matisse/src/main/res/layout/ucrop_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"match_parent\">\n\n    <!-- Padding for both views can be different -->\n\n    <com.matisse.ucrop.view.GestureCropImageView\n        android:id=\"@+id/image_view_crop\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"@dimen/ucrop_padding_crop_frame\"/>\n\n    <com.matisse.ucrop.view.OverlayView\n        android:id=\"@+id/view_overlay\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"@dimen/ucrop_padding_crop_frame\"/>\n\n</merge>"
  },
  {
    "path": "matisse/src/main/res/layout/view_media_grid_content.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <ImageView\n        android:id=\"@+id/media_thumbnail\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <com.matisse.widget.CheckView\n        android:id=\"@+id/check_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"top|end\"\n        android:layout_margin=\"6dp\" />\n\n    <ImageView\n        android:id=\"@+id/gif\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end|bottom\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginBottom=\"8dp\"\n        android:src=\"@drawable/ic_gif\"\n        android:visibility=\"gone\" />\n\n    <TextView\n        android:id=\"@+id/video_duration\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end|bottom\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginBottom=\"8dp\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"12sp\"\n        android:visibility=\"gone\" />\n\n</merge>"
  },
  {
    "path": "matisse/src/main/res/menu/ucrop_menu_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_crop\"\n        android:icon=\"@drawable/ucrop_ic_done\"\n        android:title=\"@string/ucrop_menu_crop\"\n        app:showAsAction=\"ifRoom\"/>\n\n    <item\n        android:id=\"@+id/menu_loader\"\n        android:enabled=\"false\"\n        android:icon=\"@drawable/ucrop_vector_loader_animated\"\n        android:title=\"\"\n        app:showAsAction=\"ifRoom\"/>\n\n</menu>"
  },
  {
    "path": "matisse/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <attr name=\"Navigation_height\" format=\"dimension\" />\n    <attr name=\"Navigation_bg\" format=\"reference|color\" />\n    <attr name=\"Navigation_backRes\" format=\"reference\" />\n\n    <attr name=\"Preview_bg\" format=\"reference|color\" />\n\n    <attr name=\"BottomToolbar_bg\" format=\"reference|color\" />\n    <attr name=\"BottomToolbar_height\" format=\"dimension\" />\n\n    <attr name=\"Media_Back_text\" format=\"reference\" />\n    <attr name=\"Media_Sure_text\" format=\"reference\" />\n    <attr name=\"Media_Preview_text\" format=\"reference\" />\n    <attr name=\"Media_Original_text\" format=\"reference\" />\n    <attr name=\"Media_Album_text\" format=\"reference\" />\n    <attr name=\"Media_Empty_text\" format=\"reference\" />\n    <attr name=\"Media_Camera_text\" format=\"reference\" />\n    <attr name=\"Preview_Back_text\" format=\"reference\" />\n    <attr name=\"Preview_Confirm_text\" format=\"reference\" />\n\n    <attr name=\"Media_Back_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Sure_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Preview_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Album_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Original_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Camera_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Album_Item_textColor\" format=\"reference|color\" />\n\n    <attr name=\"Media_Back_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Sure_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Preview_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Album_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Original_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Camera_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Album_Item_textSize\" format=\"dimension\" />\n\n    <attr name=\"Item_checkCircle_bgColor\" format=\"reference|color\" />\n    <attr name=\"Item_checkCircle_borderColor\" format=\"reference|color\" />\n    <attr name=\"Item_checkCircle_numColor\" format=\"reference|color\" />\n    <attr name=\"Item_checkRadio\" format=\"reference|color\" />\n\n    <attr name=\"Media_Empty_textColor\" format=\"reference|color\" />\n    <attr name=\"Media_Empty_textSize\" format=\"dimension\" />\n    <attr name=\"Media_Empty_resource\" format=\"reference\" />\n\n    <attr name=\"Item_placeholder\" format=\"reference|color\" />\n    <attr name=\"Page_background\" format=\"reference|color\" />\n\n    <declare-styleable name=\"ucrop_UCropView\">\n\n        <!--Crop image view-->\n        <attr name=\"ucrop_aspect_ratio_x\" format=\"float\"/>\n        <attr name=\"ucrop_aspect_ratio_y\" format=\"float\"/>\n\n        <!--Overlay view-->\n        <attr name=\"ucrop_show_oval_crop_frame\" format=\"boolean\"/>\n        <attr name=\"ucrop_circle_dimmed_layer\" format=\"boolean\"/>\n        <attr name=\"ucrop_dimmed_color\" format=\"color\"/>\n\n        <attr name=\"ucrop_grid_stroke_size\" format=\"dimension\"/>\n        <attr name=\"ucrop_grid_color\" format=\"color\"/>\n        <attr name=\"ucrop_grid_row_count\" format=\"integer\"/>\n        <attr name=\"ucrop_grid_column_count\" format=\"integer\"/>\n        <attr name=\"ucrop_show_grid\" format=\"boolean\"/>\n\n        <attr name=\"ucrop_frame_stroke_size\" format=\"dimension\"/>\n        <attr name=\"ucrop_frame_color\" format=\"color\"/>\n        <attr name=\"ucrop_show_frame\" format=\"boolean\"/>\n\n    </declare-styleable>\n\n    <declare-styleable name=\"ucrop_AspectRatioTextView\">\n\n        <attr name=\"ucrop_artv_ratio_title\" format=\"string\"/>\n\n        <attr name=\"ucrop_artv_ratio_x\" format=\"float\"/>\n        <attr name=\"ucrop_artv_ratio_y\" format=\"float\"/>\n\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#FFFFFF</color>\n    <color name=\"white_press\">#77FFFFFF</color>\n    <color name=\"gray\">#361F1F1F</color>\n\n    <color name=\"black\">#DE000000</color>\n    <color name=\"shadow_hint\">#00000000</color>\n    <color name=\"shadow\">#0D000000</color>\n    <color name=\"black_press\">#4D000000</color>\n\n    <color name=\"color_title_text\">@color/black</color>\n    <color name=\"color_title_press_text\">@color/black_press</color>\n    <color name=\"color_hint_text\">#424242</color>\n\n    <color name=\"preview_bottom_size\">#61FFFFFF</color>\n\n    <!--uCrop Activity-->\n    <color name=\"ucrop_color_toolbar\">#FF6E40</color>\n    <color name=\"ucrop_color_statusbar\">#CC5833</color>\n    <color name=\"ucrop_color_toolbar_widget\">#fff</color>\n    <color name=\"ucrop_color_widget\">#000</color>\n    <color name=\"ucrop_color_widget_active\">#FF6E40</color>\n    <color name=\"ucrop_color_widget_background\">#fff</color>\n    <color name=\"ucrop_color_widget_text\">#000</color>\n    <color name=\"ucrop_color_progress_wheel_line\">#808080</color>\n    <color name=\"ucrop_color_crop_background\">#000</color>\n\n    <!--Crop View-->\n    <color name=\"ucrop_color_default_crop_grid\">#80ffffff</color>\n    <color name=\"ucrop_color_default_crop_frame\">#ffffff</color>\n    <color name=\"ucrop_color_default_dimmed\">#8c000000</color>\n    <color name=\"ucrop_color_default_logo\">#4f212121</color>\n    <color name=\"ucrop_color_grey\">#999999</color>\n\n    <!--more-->\n    <color name=\"ucrop_color_ba3\">#b0567ba3</color>\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/colors_default.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"primary\">@color/white</color>\n    <color name=\"primary_dark\">@color/gray</color>\n\n    <color name=\"color_base\">#1523A7</color>\n    <color name=\"color_base_press\">#771523A7</color>\n\n    <color name=\"navigation_bg\">@color/white</color>\n    <color name=\"preview_bg\">@color/black</color>\n    <color name=\"bottomTool_bg\">@color/white</color>\n\n    <color name=\"text_preview\">@color/selector_black_text</color>\n    <color name=\"text_original\">@color/color_hint_text</color>\n    <color name=\"text_album\">@color/selector_base_text</color>\n    <color name=\"text_item_album\">@color/selector_black_text</color>\n    <color name=\"text_camera\">@color/color_hint_text</color>\n    <color name=\"text_empty\">@color/color_hint_text</color>\n    <!--选择控件边框颜色-->\n    <color name=\"item_checkCircle_borderColor\">@color/selector_white_text</color>\n    <!--选择控件文字状态文字颜色-->\n    <color name=\"check_original_radio_disable\">#808080</color>\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"media_grid_size\">48dp</dimen>\n    <dimen name=\"media_grid_spacing\">4dp</dimen>\n\n    <dimen name=\"navigation_height\">48dp</dimen>\n    <dimen name=\"bottom_tool_height\">48dp</dimen>\n\n    <dimen name=\"text_back\">15sp</dimen>\n    <dimen name=\"text_sure\">15sp</dimen>\n    <dimen name=\"text_preview\">15sp</dimen>\n    <dimen name=\"text_album\">15sp</dimen>\n    <dimen name=\"text_original\">15sp</dimen>\n    <dimen name=\"text_camera\">14sp</dimen>\n    <dimen name=\"text_item_album\">16sp</dimen>\n    <dimen name=\"text_empty\">17sp</dimen>\n\n    <!--uCrop Activity-->\n    <dimen name=\"ucrop_padding_crop_frame\">16dp</dimen>\n    <dimen name=\"ucrop_size_dot_scale_text_view\">8dp</dimen>\n    <dimen name=\"ucrop_height_horizontal_wheel_progress_line\">20dp</dimen>\n    <dimen name=\"ucrop_width_horizontal_wheel_progress_line\">2dp</dimen>\n    <dimen name=\"ucrop_margin_horizontal_wheel_progress_line\">10dp</dimen>\n    <dimen name=\"ucrop_height_wrapper_controls\">64dp</dimen>\n    <dimen name=\"ucrop_height_wrapper_states\">72dp</dimen>\n    <dimen name=\"ucrop_height_divider_shadow\">3dp</dimen>\n    <dimen name=\"ucrop_text_size_widget_text\">13sp</dimen>\n    <dimen name=\"ucrop_margit_top_widget_text\">10sp</dimen>\n    <dimen name=\"ucrop_size_wrapper_rotate_button\">50dp</dimen>\n    <dimen name=\"ucrop_height_crop_aspect_ratio_text\">40dp</dimen>\n    <dimen name=\"ucrop_progress_size\">30dp</dimen>\n\n    <!--Crop View-->\n    <dimen name=\"ucrop_default_crop_grid_stoke_width\">1dp</dimen>\n    <dimen name=\"ucrop_default_crop_frame_stoke_width\">1dp</dimen>\n    <dimen name=\"ucrop_default_crop_rect_corner_touch_threshold\">30dp</dimen>\n    <dimen name=\"ucrop_default_crop_rect_min_size\">100dp</dimen>\n    <dimen name=\"ucrop_default_crop_rect_corner_touch_area_line_length\">10dp</dimen>\n\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <item name=\"id_recycler\" type=\"id\" tools:ignore=\"ResourceName\" />\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/long_attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nCopyright 2014 David Morrissey\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<resources>\n\n    <declare-styleable name=\"SubsamplingScaleImageView\">\n        <attr name=\"src\" format=\"reference\"/>\n        <attr name=\"assetName\" format=\"string\"/>\n        <attr name=\"panEnabled\" format=\"boolean\"/>\n        <attr name=\"zoomEnabled\" format=\"boolean\"/>\n        <attr name=\"quickScaleEnabled\" format=\"boolean\"/>\n        <attr name=\"tileBackgroundColor\" format=\"color\"/>\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"album_name_all\">All Media</string>\n    <string name=\"button_ok\">OK</string>\n    <string name=\"button_original\">Original</string>\n    <string name=\"button_sure_default\">Sure</string>\n    <string name=\"button_sure\">Sure</string>\n    <string name=\"button_preview\">Preview</string>\n    <string name=\"button_apply\">Apply(%1$d)</string>\n    <string name=\"button_back\">Back</string>\n    <string name=\"button_null\">\"\"</string>\n    <string name=\"button_complete\">Complete</string>\n    <string name=\"folder_count\">%1$s(%2$d)</string>\n    <string name=\"picture_size\">%1$s M</string>\n\n    <string name=\"photo_grid_capture\">Camera</string>\n    <string name=\"empty_text\">No media yet</string>\n    <string name=\"error_over_count\">You can only select up to %1$d media files</string>\n    <string name=\"error_over_count_of_image\">You can only select up to %1$d media of image files</string>\n    <string name=\"error_over_count_of_video\">You can only select up to %1$d media of video files</string>\n    <string name=\"error_file_type\">Unsupported file type</string>\n    <string name=\"error_type_conflict\">Can\\'t select images and videos at the same time</string>\n    <string name=\"error_no_video_activity\">No App found supporting video preview</string>\n    <string name=\"error_over_original_size\">Can\\'t select the images larger than %1$d MB</string>\n    <string name=\"error_over_original_count\">%1$d images over %2$d MB. Original will be unchecked</string>\n    <string name=\"error_crop\">Crop Failed</string>\n    <string name=\"please_select_media_resource\">Please select media resource</string>\n    <string name=\"empty_album\">album is empty</string>\n\n    <string name=\"ucrop_label_original\">Original</string>\n    <string name=\"ucrop_label_edit_photo\">Edit Photo</string>\n\n    <string name=\"ucrop_menu_crop\">Crop</string>\n\n    <string name=\"ucrop_error_input_data_is_absent\">Both input and output Uri must be specified</string>\n    <string name=\"ucrop_mutate_exception_hint\">Therefore, override color resource (ucrop_color_toolbar_widget) in your app to make it work on pre-L devices</string>\n\n    <string name=\"ucrop_gif_tag\">gif</string>\n</resources>\n"
  },
  {
    "path": "matisse/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Animation.Bottom\" parent=\"Theme.AppCompat.Light.Dialog\">\n        <item name=\"android:windowEnterAnimation\">@anim/bottom_up_in</item>\n        <item name=\"android:windowExitAnimation\">@anim/bottom_down_out</item>\n    </style>\n\n    <style name=\"singleLineText\">\n        <item name=\"android:maxLines\">1</item>\n        <item name=\"android:ellipsize\">end</item>\n    </style>\n\n    <style name=\"Matisse.Default\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!--状态栏相关-->\n        <item name=\"colorPrimary\">@color/primary</item>\n        <!--状态栏颜色-->\n        <item name=\"colorPrimaryDark\">@color/primary_dark</item>\n\n        <!--顶部导航条高度-->\n        <item name=\"Navigation_height\">@dimen/navigation_height</item>\n        <!--顶部导航条背景色-->\n        <item name=\"Navigation_bg\">@color/navigation_bg</item>\n        <!--顶部导航条返回按钮资源图-->\n        <item name=\"Navigation_backRes\">@drawable/icon_arrow_right_white</item>\n        <!--图片预览界面背景色-->\n        <item name=\"Preview_bg\">@color/preview_bg</item>\n\n        <!--底部工具栏背景色-->\n        <item name=\"BottomToolbar_bg\">@color/bottomTool_bg</item>\n        <!--底部工具栏高度-->\n        <item name=\"BottomToolbar_height\">@dimen/bottom_tool_height</item>\n\n        <!--返回按钮文字 当无文字是可设置为空字符串 如下-->\n        <item name=\"Media_Back_text\">@string/button_null</item>\n        <!--返回按钮颜色-->\n        <item name=\"Media_Back_textColor\">@color/color_base</item>\n        <!--返回按钮文字大小-->\n        <item name=\"Media_Back_textSize\">@dimen/text_back</item>\n\n        <item name=\"Media_Sure_text\">@string/button_sure</item>\n        <!--确定按钮颜色-->\n        <item name=\"Media_Sure_textColor\">@color/color_base</item>\n        <!--确认按钮文字大小-->\n        <item name=\"Media_Sure_textSize\">@dimen/text_sure</item>\n\n        <item name=\"Media_Preview_text\">@string/button_preview</item>\n        <!--预览按钮颜色-->\n        <item name=\"Media_Preview_textColor\">@color/text_preview</item>\n        <!--预览按钮文字大小-->\n        <item name=\"Media_Preview_textSize\">@dimen/text_preview</item>\n\n        <item name=\"Media_Original_text\">@string/button_original</item>\n        <!--原图选择控件文字颜色-->\n        <item name=\"Media_Original_textColor\">@color/text_original</item>\n        <!--原图按钮文字大小-->\n        <item name=\"Media_Original_textSize\">@dimen/text_original</item>\n\n        <item name=\"Media_Album_text\">@string/album_name_all</item>\n        <!--查看全部相册文件夹按钮颜色-->\n        <item name=\"Media_Album_textColor\">@color/text_album</item>\n        <!--查看全部相册按钮文字大小-->\n        <item name=\"Media_Album_textSize\">@dimen/text_album</item>\n        <!--相册文件夹内部列表item颜色-->\n        <item name=\"Media_Album_Item_textColor\">@color/text_item_album</item>\n        <!--查看全部相册内item文字大小-->\n        <item name=\"Media_Album_Item_textSize\">@dimen/text_item_album</item>\n\n        <!--列表中可拍照状态下相机item文字颜色-->\n        <item name=\"Media_Camera_textColor\">@color/text_camera</item>\n        <!--列表中可拍照状态下相机item文字大小-->\n        <item name=\"Media_Camera_textSize\">@dimen/text_camera</item>\n\n        <item name=\"Preview_Back_text\">@string/button_back</item>\n        <item name=\"Preview_Confirm_text\">@string/button_sure_default</item>\n\n        <!--选中控件背景色-->\n        <item name=\"Item_checkCircle_bgColor\">@color/selector_base_text</item>\n        <!--选中控件圆环边框颜色-->\n        <item name=\"Item_checkCircle_borderColor\">@color/item_checkCircle_borderColor</item>\n        <!--原图radio控件颜色-->\n        <item name=\"Item_checkRadio\">@color/selector_base_text</item>\n\n        <!--空状态文字颜色-->\n        <item name=\"Media_Empty_textColor\">@color/text_empty</item>\n        <!--空状态文字大小-->\n        <item name=\"Media_Empty_textSize\">@dimen/text_empty</item>\n        <item name=\"Media_Empty_text\">@string/empty_text</item>\n        <!--空状态资源图-->\n        <item name=\"Media_Empty_resource\">@drawable/ic_empty_zhihu</item>\n\n        <!--图片加载占位图-->\n        <!--<item name=\"Item_placeholder\">@drawable/zhihu_item_placeholder</item>-->\n    </style>\n\n\n    <style name=\"ucrop_ImageViewWidgetIcon\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:scaleType\">fitCenter</item>\n        <item name=\"android:duplicateParentState\">true</item>\n    </style>\n\n    <style name=\"ucrop_WrapperIconState\">\n        <item name=\"android:layout_width\">0dp</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:background\">?attr/selectableItemBackground</item>\n        <item name=\"android:clickable\">true</item>\n    </style>\n\n    <style name=\"ucrop_WrapperRotateButton\">\n        <item name=\"android:layout_width\">@dimen/ucrop_size_wrapper_rotate_button</item>\n        <item name=\"android:layout_height\">@dimen/ucrop_size_wrapper_rotate_button</item>\n        <item name=\"android:background\">?attr/selectableItemBackgroundBorderless</item>\n        <item name=\"android:clickable\">true</item>\n    </style>\n\n    <style name=\"ucrop_TextViewCropAspectRatio\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">@dimen/ucrop_height_crop_aspect_ratio_text</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:duplicateParentState\">true</item>\n        <item name=\"android:textColor\">@color/ucrop_scale_text_view_selector</item>\n    </style>\n\n    <style name=\"ucrop_TextViewWidgetText\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_centerHorizontal\">true</item>\n        <item name=\"android:layout_marginTop\">@dimen/ucrop_margit_top_widget_text</item>\n        <item name=\"android:textColor\">@color/ucrop_color_widget_text</item>\n        <item name=\"android:textSize\">@dimen/ucrop_text_size_widget_text</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values/values.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <integer name=\"ucrop_progress_loading_anim_time\">1500</integer>\n</resources>"
  },
  {
    "path": "matisse/src/main/res/values-zh/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"ExtraTranslation\" tools:locale=\"en\">\n    <string name=\"album_name_all\">全部</string>\n\n    <string name=\"button_preview\">预览</string>\n    <string name=\"button_apply\">使用(%1$d)</string>\n    <string name=\"button_back\">返回</string>\n    <string name=\"button_ok\">我知道了</string>\n    <string name=\"button_original\">原图</string>\n    <string name=\"button_sure_default\">确定</string>\n    <string name=\"button_sure\">确定</string>\n    <string name=\"button_complete\">完成</string>\n    <string name=\"button_null\">\"\"</string>\n    <string name=\"folder_count\">%1$s(%2$d)</string>\n    <string name=\"picture_size\">%1$s M</string>\n\n    <string name=\"photo_grid_capture\">拍一张</string>\n    <string name=\"empty_text\">还没有图片或视频</string>\n    <string name=\"error_over_count\">最多只能选择 %1$d 个文件</string>\n    <string name=\"error_over_count_of_image\">最多只能选择 %1$d 个图片文件</string>\n    <string name=\"error_over_count_of_video\">最多只能选择 %1$d 个视频文件</string>\n    <string name=\"error_file_type\">不支持的文件类型</string>\n    <string name=\"error_type_conflict\">不能同时选择图片和视频</string>\n    <string name=\"error_no_video_activity\">没有支持视频预览的应用</string>\n    <string name=\"error_over_original_size\">\"该照片大于 %1$d M，无法上传将取消勾选原图\"</string>\n    <string name=\"error_over_original_count\">\"有 %1$d 张照片大于 %2$d M\\n无法上传，将取消勾选原图\"</string>\n    <string name=\"error_crop\">裁剪失败</string>\n    <string name=\"please_select_media_resource\">请选择媒体资源</string>\n    <string name=\"empty_album\">相册为空</string>\n\n    <string name=\"ucrop_label_original\">原始比例</string>\n    <string name=\"ucrop_label_edit_photo\">裁剪</string>\n\n    <string name=\"ucrop_menu_crop\">裁剪</string>\n\n    <string name=\"ucrop_gif_tag\">动图</string>\n</resources>"
  },
  {
    "path": "matisse/src/test/java/com/matisse/ExampleUnitTest.java",
    "content": "package com.matisse;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':matisse'\n"
  }
]