├── .gitignore ├── LICENSE ├── README.md ├── Screenshot_20220617_163923.png ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mill │ │ └── cropcut │ │ ├── MainActivity.kt │ │ ├── VcActivity.kt │ │ ├── VideoCropPreActivity.kt │ │ ├── adapter │ │ └── VDurationCutAdapter.java │ │ ├── bean │ │ └── LocalVideoBean.java │ │ ├── utils │ │ ├── CutUtils.java │ │ ├── FileUtils.java │ │ ├── RectUtils.java │ │ ├── VideoCropHelper.kt │ │ └── VideoFFCrop.kt │ │ └── view │ │ ├── LocalVideoView.java │ │ ├── OverlayView.java │ │ ├── RangeSlider.java │ │ ├── ThumbView.java │ │ ├── VDurationCutView.java │ │ └── VHwCropView.java │ ├── jniLibs │ ├── arm64-v8a │ │ └── ffmpeg.so │ └── armeabi-v7a │ │ └── ffmpeg.so │ └── res │ ├── drawable │ ├── ic_progress_left.png │ └── ic_progress_right.png │ ├── layout │ ├── activity_main.xml │ ├── item_edit_view.xml │ ├── main.xml │ ├── video_crop.xml │ └── video_view_crop.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | /local.properties 2 | /.idea/ 3 | /.gradle/ 4 | /build/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoCrop 2 | A video crop demo base by ffmpeg 3 | 4 | *** 5 | 6 | 7 | 8 | *** 9 | ![avatar](https://github.com/Pangu-Immortal/Pangu-Immortal/blob/main/getqrcode.png) 10 | ### ffmpeg比较大,可以考虑放到后台动态加载; 11 | 12 | ### 适配环境(Android 4.1 -- Android 12) 13 | 14 | 完整全工程代码。可以拉取代码直接运行。 15 | 16 | *** 17 | 18 | 19 | -------------------------------------------------------------------------------- /Screenshot_20220617_163923.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pangu-Immortal/VideoCropping/c6588f0ee71557eef73545f0cf168ae7f4e07ec1/Screenshot_20220617_163923.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdk 31 6 | 7 | defaultConfig { 8 | applicationId "com.qihao.videocrop" 9 | 10 | minSdk 23 11 | targetSdk 30 12 | 13 | versionCode 22061621 14 | versionName "1.1.0" 15 | 16 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: "libs", include: ["*.jar", '*.aar']) 28 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 29 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 30 | implementation 'androidx.appcompat:appcompat:1.4.2' 31 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 32 | implementation "androidx.core:core-ktx:1.8.0" 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2' 35 | implementation 'com.google.android.material:material:1.6.1' 36 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0-rc01' 37 | } 38 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\software\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.database.Cursor 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Bundle 11 | import android.os.Environment 12 | import android.provider.MediaStore 13 | import android.provider.Settings 14 | import android.util.Log 15 | import android.widget.Toast 16 | import androidx.activity.result.contract.ActivityResultContracts 17 | import androidx.appcompat.app.AppCompatActivity 18 | import androidx.appcompat.widget.AppCompatButton 19 | import androidx.core.app.ActivityCompat 20 | import androidx.core.content.ContextCompat 21 | import com.mill.cropcut.utils.FileUtils 22 | import kotlinx.coroutines.* 23 | import java.io.* 24 | import java.nio.channels.FileChannel 25 | 26 | /** 27 | * Doc说明 (此类核心功能): 28 | * @date on 2022/6/17 11:52 29 | * +--------------------------------------------+ 30 | * | @author qihao | 31 | * | @GitHub https://github.com/Pangu-Immortal | 32 | * +--------------------------------------------+ 33 | */ 34 | private const val REQUEST_CODE_PERMISSIONS = 10 35 | private const val REQUEST_SELECT_FILE = 11 36 | 37 | private const val TAG = "MainActivity" 38 | 39 | class MainActivity : AppCompatActivity() { 40 | 41 | private var srcVideo = Environment.getExternalStorageDirectory().path + "/cc.mp4" 42 | 43 | 44 | //获取权限弹窗操作完成回调 45 | override fun onRequestPermissionsResult( 46 | requestCode: Int, permissions: Array, 47 | grantResults: IntArray 48 | ) { 49 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 50 | if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 51 | selectFile()//去选择文件 52 | } 53 | } 54 | 55 | 56 | /** 57 | * 请求存储权限 58 | */ 59 | private fun requestWritePermission() { 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 61 | // 先判断有没有权限 62 | if (Environment.isExternalStorageManager()) { 63 | selectFile()//去选择文件 64 | } else { 65 | try { 66 | Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { 67 | data = Uri.parse("package:$packageName") 68 | }.let { startActivityForResult(it, REQUEST_CODE_PERMISSIONS) } 69 | } catch (e: Exception) { 70 | e.printStackTrace() 71 | } 72 | } 73 | } else { 74 | if (ActivityCompat.checkSelfPermission( 75 | this, 76 | Manifest.permission.READ_EXTERNAL_STORAGE 77 | ) == PackageManager.PERMISSION_GRANTED 78 | && ContextCompat.checkSelfPermission( 79 | this, 80 | Manifest.permission.WRITE_EXTERNAL_STORAGE 81 | ) == PackageManager.PERMISSION_GRANTED 82 | ) { 83 | selectFile()//去选择文件 84 | } else { 85 | requestPermissions( 86 | arrayOf( 87 | Manifest.permission.READ_EXTERNAL_STORAGE, 88 | Manifest.permission.WRITE_EXTERNAL_STORAGE 89 | ), 90 | REQUEST_CODE_PERMISSIONS 91 | ) 92 | } 93 | } 94 | } 95 | 96 | 97 | override fun onCreate(savedInstanceState: Bundle?) { 98 | super.onCreate(savedInstanceState) 99 | setContentView(R.layout.activity_main) 100 | FileUtils.CreateFile(srcVideo) 101 | findViewById(R.id.pic_file).apply { 102 | setOnClickListener { 103 | Log.d(TAG, "onCreate: 点击") 104 | requestWritePermission() 105 | } 106 | } 107 | } 108 | 109 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 110 | super.onActivityResult(requestCode, resultCode, data) 111 | when (requestCode) { 112 | REQUEST_SELECT_FILE -> { 113 | if (resultCode == Activity.RESULT_OK && data != null && data.data != null) { 114 | toCrop(data.data!!)//选完文件以后跳转到裁剪界面 115 | } 116 | } 117 | } 118 | } 119 | 120 | private var someActivityResultLauncher = registerForActivityResult( 121 | ActivityResultContracts.StartActivityForResult() 122 | ) { result -> 123 | if (result.resultCode == RESULT_OK 124 | && result.data != null && result.data!!.data != null 125 | ) { 126 | toCrop(result.data!!.data!!)//选完文件以后跳转到裁剪界面 127 | } 128 | } 129 | 130 | private fun selectFile() { 131 | val i = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 133 | someActivityResultLauncher.launch(i)//Android 10以上的写法,targetAPI在31以上才这么写 134 | } else { 135 | startActivityForResult(i, REQUEST_SELECT_FILE)//Android 10以下的写法 136 | } 137 | } 138 | 139 | /** 140 | * 跳转裁剪页面 141 | */ 142 | private fun toCrop(data: Uri) { 143 | val filePathColumn = arrayOf(MediaStore.Video.Media.DATA) 144 | try { 145 | val cursor: Cursor? = contentResolver.query( 146 | data, 147 | filePathColumn, null, null, null 148 | ) 149 | if (cursor != null) { 150 | cursor.moveToFirst() 151 | val columnIndex: Int = cursor.getColumnIndex(filePathColumn[0]) 152 | val videoPath: String = cursor.getString(columnIndex) 153 | cursor.close() 154 | 155 | if (copyFile(videoPath, srcVideo)){ 156 | startCrop(srcVideo) 157 | } 158 | } 159 | } catch (e: Exception) { 160 | Log.e(TAG, "读取文件出错", e) 161 | Toast.makeText(this, "readVideoFileError", Toast.LENGTH_LONG).show() 162 | } 163 | } 164 | 165 | private fun startCrop(videoPath: String) { 166 | Intent(this, VideoCropPreActivity::class.java).apply { 167 | putExtra("src", videoPath)//在裁剪界面接收这个参数即可 168 | }.let { startActivity(it) } 169 | } 170 | 171 | private val fileName = "a23.mp4" 172 | 173 | /** 174 | * 根据文件路径拷贝文件 175 | * @param src 源文件 176 | * @param destPath目标文件路径 177 | * @return boolean 成功true、失败false 178 | */ 179 | private fun copyFile(src: File?, destPath: String?): Boolean { 180 | var result = false 181 | if (src == null || destPath == null) { 182 | return result 183 | } 184 | val dest = File(destPath + fileName) 185 | if (dest != null && dest.exists()) { 186 | dest.delete() // delete file 187 | } 188 | try { 189 | dest.createNewFile() 190 | } catch (e: IOException) { 191 | e.printStackTrace() 192 | } 193 | var srcChannel: FileChannel? = null 194 | var dstChannel: FileChannel? = null 195 | try { 196 | srcChannel = FileInputStream(src).channel 197 | dstChannel = FileOutputStream(dest).channel 198 | srcChannel.transferTo(0, srcChannel.size(), dstChannel) 199 | result = true 200 | } catch (e: FileNotFoundException) { 201 | e.printStackTrace() 202 | return result 203 | } catch (e: IOException) { 204 | e.printStackTrace() 205 | return result 206 | } 207 | try { 208 | srcChannel.close() 209 | dstChannel.close() 210 | } catch (e: IOException) { 211 | e.printStackTrace() 212 | } 213 | return result 214 | } 215 | 216 | 217 | /** 218 | * 复制单个文件 219 | * 220 | * @param oldPathName String 原文件路径+文件名 如:data/user/0/com.test/files/abc.txt 221 | * @param newPathName String 复制后路径+文件名 如:data/user/0/com.test/cache/abc.txt 222 | * @return `true` if and only if the file was copied; 223 | * `false` otherwise 224 | */ 225 | fun copyFile(`oldPath$Name`: String?, `newPath$Name`: String?): Boolean { 226 | return try { 227 | val oldFile = File(`oldPath$Name`) 228 | if (!oldFile.exists()) { 229 | Log.e("--Method--", "copyFile: oldFile not exist.") 230 | return false 231 | } else if (!oldFile.isFile) { 232 | Log.e("--Method--", "copyFile: oldFile not file.") 233 | return false 234 | } else if (!oldFile.canRead()) { 235 | Log.e("--Method--", "copyFile: oldFile cannot read.") 236 | return false 237 | } 238 | 239 | /* 如果不需要打log,可以使用下面的语句 240 | if (!oldFile.exists() || !oldFile.isFile() || !oldFile.canRead()) { 241 | return false; 242 | } 243 | */ 244 | val fileInputStream = FileInputStream(`oldPath$Name`) 245 | val fileOutputStream = FileOutputStream(`newPath$Name`) 246 | val buffer = ByteArray(1024) 247 | var byteRead: Int 248 | while (-1 != fileInputStream.read(buffer).also { byteRead = it }) { 249 | fileOutputStream.write(buffer, 0, byteRead) 250 | } 251 | fileInputStream.close() 252 | fileOutputStream.flush() 253 | fileOutputStream.close() 254 | true 255 | } catch (e: java.lang.Exception) { 256 | e.printStackTrace() 257 | false 258 | } 259 | } 260 | 261 | /** 262 | * 复制文件夹及其中的文件 263 | * 264 | * @param oldPath String 原文件夹路径 如:data/user/0/com.test/files 265 | * @param newPath String 复制后的路径 如:data/user/0/com.test/cache 266 | * @return `true` if and only if the directory and files were copied; 267 | * `false` otherwise 268 | */ 269 | fun copyFolder(oldPath: String, newPath: String): Boolean { 270 | return try { 271 | val newFile = File(newPath) 272 | if (!newFile.exists()) { 273 | if (!newFile.mkdirs()) { 274 | Log.e("--Method--", "copyFolder: cannot create directory.") 275 | return false 276 | } 277 | } 278 | val oldFile = File(oldPath) 279 | val files = oldFile.list() 280 | var temp: File 281 | for (file in files) { 282 | temp = if (oldPath.endsWith(File.separator)) { 283 | File(oldPath + file) 284 | } else { 285 | File(oldPath + File.separator.toString() + file) 286 | } 287 | if (temp.isDirectory) { //如果是子文件夹 288 | copyFolder("$oldPath/$file", "$newPath/$file") 289 | } else if (!temp.exists()) { 290 | Log.e("--Method--", "copyFolder: oldFile not exist.") 291 | return false 292 | } else if (!temp.isFile) { 293 | Log.e("--Method--", "copyFolder: oldFile not file.") 294 | return false 295 | } else if (!temp.canRead()) { 296 | Log.e("--Method--", "copyFolder: oldFile cannot read.") 297 | return false 298 | } else { 299 | val fileInputStream = FileInputStream(temp) 300 | val fileOutputStream = FileOutputStream(newPath + "/" + temp.name) 301 | val buffer = ByteArray(1024) 302 | var byteRead: Int 303 | while (fileInputStream.read(buffer).also { byteRead = it } != -1) { 304 | fileOutputStream.write(buffer, 0, byteRead) 305 | } 306 | fileInputStream.close() 307 | fileOutputStream.flush() 308 | fileOutputStream.close() 309 | } 310 | 311 | /* 如果不需要打log,可以使用下面的语句 312 | if (temp.isDirectory()) { //如果是子文件夹 313 | copyFolder(oldPath + "/" + file, newPath + "/" + file); 314 | } else if (temp.exists() && temp.isFile() && temp.canRead()) { 315 | FileInputStream fileInputStream = new FileInputStream(temp); 316 | FileOutputStream fileOutputStream = new FileOutputStream(newPath + "/" + temp.getName()); 317 | byte[] buffer = new byte[1024]; 318 | int byteRead; 319 | while ((byteRead = fileInputStream.read(buffer)) != -1) { 320 | fileOutputStream.write(buffer, 0, byteRead); 321 | } 322 | fileInputStream.close(); 323 | fileOutputStream.flush(); 324 | fileOutputStream.close(); 325 | } 326 | */ 327 | } 328 | true 329 | } catch (e: java.lang.Exception) { 330 | e.printStackTrace() 331 | false 332 | } 333 | } 334 | 335 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/VcActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.mill.cropcut.utils.VideoFFCrop 6 | import android.widget.EditText 7 | import android.media.MediaMetadataRetriever 8 | import android.util.Log 9 | import android.view.View 10 | import android.widget.Button 11 | import com.mill.cropcut.utils.VideoFFCrop.FFListener 12 | import android.widget.TextView 13 | import java.lang.Exception 14 | 15 | class VcActivity : AppCompatActivity(), View.OnClickListener { 16 | /** 17 | * Called when the activity is first created. 18 | */ 19 | public override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.main) 22 | VideoFFCrop.instance?.init(this) 23 | (findViewById(R.id.runbtn) as Button).setOnClickListener(this) 24 | } 25 | 26 | override fun onClick(v: View) { 27 | val srcVideo = (findViewById(R.id.editText1) as EditText).text.toString() 28 | val destPath = (findViewById(R.id.editText3) as EditText).text.toString() 29 | var duration = 0 30 | var srcW = 0 31 | var srcH = 0 32 | val mmr = MediaMetadataRetriever() 33 | try { 34 | mmr.setDataSource(srcVideo) 35 | duration = 36 | (java.lang.Long.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) / 1000).toInt() 37 | srcW = 38 | Integer.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)) 39 | srcH = 40 | Integer.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)) 41 | Log.d(VideoFFCrop.TAG, "MediaMetadataRetriever $srcVideo===$duration===$srcW====$srcH") 42 | } catch (e: Exception) { 43 | e.printStackTrace() 44 | } finally { 45 | mmr.release() 46 | } 47 | VideoFFCrop.instance?.cropVideo(this@VcActivity, srcVideo, destPath, 0, duration, object : FFListener { 48 | override fun onProgress(progress: Int?) { 49 | Log.d("VcActivity", "progress: $progress"); 50 | (findViewById(R.id.textView6) as TextView).text = "progress: $progress" 51 | } 52 | 53 | override fun onFinish() { 54 | Log.d("VcActivity", "finished") 55 | (findViewById(R.id.textView6) as TextView).text = "finished" 56 | } 57 | 58 | override fun onFail(msg: String?) { 59 | Log.d("VcActivity", "failed") 60 | (findViewById(R.id.textView6) as TextView).text = "failed" 61 | } 62 | }) 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/VideoCropPreActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut 2 | 3 | import android.os.Bundle 4 | import android.os.Environment 5 | import android.util.Log 6 | import android.view.View 7 | import android.widget.TextView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.mill.cropcut.bean.LocalVideoBean 10 | import com.mill.cropcut.utils.VideoCropHelper 11 | import com.mill.cropcut.utils.VideoFFCrop 12 | import com.mill.cropcut.utils.VideoFFCrop.FFListener 13 | import com.mill.cropcut.view.VDurationCutView 14 | import com.mill.cropcut.view.VDurationCutView.IOnRangeChangeListener 15 | import com.mill.cropcut.view.VHwCropView 16 | 17 | 18 | private const val TAG = "VideoCropPreActivity" 19 | 20 | class VideoCropPreActivity : AppCompatActivity(), View.OnClickListener, IOnRangeChangeListener { 21 | 22 | 23 | 24 | private var srcVideo = Environment.getExternalStorageDirectory().absolutePath+ "testvideo.mp4" 25 | 26 | private var mVCropView: VHwCropView? = null 27 | private var mCutView: VDurationCutView? = null 28 | private var mCropBtn: TextView? = null 29 | private var mLocalVideoInfo: LocalVideoBean? = null 30 | 31 | 32 | 33 | public override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | VideoFFCrop.instance?.init(this) 36 | setContentView(R.layout.video_crop) 37 | 38 | srcVideo = intent.extras?.getString("src").toString() 39 | 40 | 41 | mVCropView = findViewById(R.id.crop_view) as VHwCropView 42 | mCutView = findViewById(R.id.cut_view) as VDurationCutView 43 | mCropBtn = findViewById(R.id.tv_ok) as TextView 44 | mCropBtn!!.setOnClickListener(this) 45 | mCutView!!.setRangeChangeListener(this) 46 | mLocalVideoInfo = VideoCropHelper.getLocalVideoInfo(srcVideo) 47 | this.mLocalVideoInfo?.duration?.let { 48 | mVCropView!!.videoView.setLocalPath( 49 | srcVideo, 50 | it.toInt() 51 | ) 52 | } 53 | mCutView!!.setMediaFileInfo(mLocalVideoInfo) 54 | 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | override fun onResume() { 71 | super.onResume() 72 | mVCropView?.videoView?.start() 73 | } 74 | 75 | override fun onPause() { 76 | super.onPause() 77 | mVCropView?.videoView?.pause() 78 | } 79 | 80 | override fun onDestroy() { 81 | super.onDestroy() 82 | mVCropView?.videoView?.stopPlayback() 83 | } 84 | 85 | override fun onClick(v: View) { 86 | Log.d( 87 | TAG, 88 | "mVideoView " + mLocalVideoInfo?.width + "===" + mLocalVideoInfo?.height 89 | ) 90 | VideoCropHelper.cropWpVideo( 91 | this@VideoCropPreActivity, 92 | mLocalVideoInfo!!, 93 | mVCropView!!, 94 | object : FFListener { 95 | override fun onProgress(progress: Int?) { 96 | Log.d(TAG, "progress: $progress") 97 | mCropBtn!!.text = "progress: $progress" 98 | } 99 | 100 | 101 | override fun onFinish() { 102 | Log.d(TAG, "finished") 103 | mCropBtn!!.text = "finished" 104 | } 105 | 106 | override fun onFail(msg: String?) { 107 | Log.d(TAG, "failed") 108 | mCropBtn!!.text = "failed" 109 | } 110 | }) 111 | } 112 | 113 | override fun onKeyDown() {} 114 | override fun onKeyUp(startTime: Int, endTime: Int) { 115 | mVCropView!!.setStarEndPo(startTime, endTime) 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/adapter/VDurationCutAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import com.mill.cropcut.R; 12 | import com.mill.cropcut.view.VDurationCutView; 13 | 14 | import java.util.ArrayList; 15 | 16 | /** 17 | * Created by lulei-ms on 2017/8/23. 18 | */ 19 | 20 | public class VDurationCutAdapter extends RecyclerView.Adapter { 21 | private final Context mContext; 22 | private ArrayList data = new ArrayList(); 23 | 24 | public VDurationCutAdapter(Context context) { 25 | mContext = context; 26 | } 27 | 28 | @Override 29 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 30 | final int itemCount = VDurationCutView.THUMB_COUNT; 31 | int padding = mContext.getResources().getDimensionPixelOffset(R.dimen.activity_horizontal_margin); 32 | int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; 33 | final int itemWidth = (screenWidth - 2 * padding) / itemCount; 34 | int height = mContext.getResources().getDimensionPixelOffset(R.dimen.ugc_item_thumb_height); 35 | // int height = (int) (itemWidth / VideoCropHelper.WHA); 36 | ImageView view = new ImageView(parent.getContext()); 37 | view.setLayoutParams(new ViewGroup.LayoutParams(itemWidth, height)); 38 | view.setScaleType(ImageView.ScaleType.CENTER_CROP); 39 | return new ViewHolder(view); 40 | } 41 | 42 | @Override 43 | public void onBindViewHolder(ViewHolder holder, int position) { 44 | holder.thumb.setImageBitmap(data.get(position)); 45 | } 46 | 47 | @Override 48 | public int getItemCount() { 49 | return data.size(); 50 | } 51 | 52 | public void add(int position, Bitmap b) { 53 | data.add(b); 54 | notifyItemInserted(position); 55 | } 56 | 57 | public class ViewHolder extends RecyclerView.ViewHolder { 58 | private final ImageView thumb; 59 | 60 | public ViewHolder(View itemView) { 61 | super(itemView); 62 | thumb = (ImageView) itemView; 63 | } 64 | } 65 | 66 | public void addAll(ArrayList bitmap) { 67 | recycleAllBitmap(); 68 | 69 | data.addAll(bitmap); 70 | notifyDataSetChanged(); 71 | } 72 | 73 | public void recycleAllBitmap() { 74 | for (Bitmap b : data) { 75 | if (!b.isRecycled()) 76 | b.recycle(); 77 | } 78 | data.clear(); 79 | notifyDataSetChanged(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/bean/LocalVideoBean.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.bean; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * Created by lulei-ms on 2017/8/23. 7 | */ 8 | 9 | public class LocalVideoBean { 10 | public Bitmap coverImage; 11 | public long duration; //单位:ms 12 | public String src_path; 13 | public long fileSize; 14 | public int fps; 15 | public int bitrate; 16 | public int width; 17 | public int height; 18 | 19 | @Override 20 | public String toString() { 21 | return "LocalVideoBean{" + 22 | " duration=" + duration + 23 | ", src_path='" + src_path + '\'' + 24 | ", fileSize=" + fileSize + 25 | ", fps=" + fps + 26 | ", bitrate=" + bitrate + 27 | ", width=" + width + 28 | ", height=" + height + 29 | '}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/utils/CutUtils.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.ContentUris; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Environment; 10 | import android.provider.DocumentsContract; 11 | import android.provider.MediaStore; 12 | 13 | public class CutUtils { 14 | /** 15 | * 时间格式化 16 | */ 17 | public static String formattedTime(long second) { 18 | String hs, ms, ss, formatTime; 19 | 20 | long h, m, s; 21 | h = second / 3600; 22 | m = (second % 3600) / 60; 23 | s = (second % 3600) % 60; 24 | if (h < 10) { 25 | hs = "0" + h; 26 | } else { 27 | hs = "" + h; 28 | } 29 | 30 | if (m < 10) { 31 | ms = "0" + m; 32 | } else { 33 | ms = "" + m; 34 | } 35 | 36 | if (s < 10) { 37 | ss = "0" + s; 38 | } else { 39 | ss = "" + s; 40 | } 41 | formatTime = hs + ":" + ms + ":" + ss; 42 | return formatTime; 43 | } 44 | 45 | public static String duration(long durationMs) { 46 | long duration = durationMs / 1000; 47 | long h = duration / 3600; 48 | long m = (duration - h * 3600) / 60; 49 | long s = duration - (h * 3600 + m * 60); 50 | 51 | String durationValue; 52 | 53 | if (h == 0) { 54 | durationValue = asTwoDigit(m) + ":" + asTwoDigit(s); 55 | } else { 56 | durationValue = asTwoDigit(h) + ":" + asTwoDigit(m) + ":" + asTwoDigit(s); 57 | } 58 | return durationValue; 59 | } 60 | 61 | public static String asTwoDigit(long digit) { 62 | String value = ""; 63 | 64 | if (digit < 10) { 65 | value = "0"; 66 | } 67 | 68 | value += String.valueOf(digit); 69 | return value; 70 | } 71 | 72 | /** 73 | * Get a file path from a Uri. This will get the the path for Storage Access 74 | * Framework Documents, as well as the _data field for the MediaStore and 75 | * other file-based ContentProviders.
76 | *
77 | * Callers should check whether the path is local before assuming it 78 | * represents a local file. 79 | * 80 | * @param context The context. 81 | * @param uri The Uri to query. 82 | */ 83 | @TargetApi(19) 84 | public static String getPath(final Context context, final Uri uri) { 85 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 86 | 87 | // DocumentProvider 88 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 89 | // ExternalStorageProvider 90 | if (isExternalStorageDocument(uri)) { 91 | final String docId = DocumentsContract.getDocumentId(uri); 92 | final String[] split = docId.split(":"); 93 | final String type = split[0]; 94 | 95 | if ("primary".equalsIgnoreCase(type)) { 96 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 97 | } 98 | 99 | // TODO handle non-primary volumes 100 | } 101 | // DownloadsProvider 102 | else if (isDownloadsDocument(uri)) { 103 | 104 | final String id = DocumentsContract.getDocumentId(uri); 105 | final Uri contentUri = ContentUris.withAppendedId( 106 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 107 | 108 | return getDataColumn(context, contentUri, null, null); 109 | } 110 | // MediaProvider 111 | else if (isMediaDocument(uri)) { 112 | final String docId = DocumentsContract.getDocumentId(uri); 113 | final String[] split = docId.split(":"); 114 | final String type = split[0]; 115 | 116 | Uri contentUri = null; 117 | if ("image".equals(type)) { 118 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 119 | } else if ("video".equals(type)) { 120 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 121 | } else if ("audio".equals(type)) { 122 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 123 | } 124 | 125 | final String selection = "_id=?"; 126 | final String[] selectionArgs = new String[]{ 127 | split[1] 128 | }; 129 | 130 | return getDataColumn(context, contentUri, selection, selectionArgs); 131 | } 132 | } 133 | // MediaStore (and general) 134 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 135 | 136 | // Return the remote address 137 | if (isGooglePhotosUri(uri)) 138 | return uri.getLastPathSegment(); 139 | 140 | return getDataColumn(context, uri, null, null); 141 | } 142 | // File 143 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 144 | return uri.getPath(); 145 | } 146 | 147 | return null; 148 | } 149 | 150 | /** 151 | * @param uri The Uri to check. 152 | * @return Whether the Uri authority is ExternalStorageProvider. 153 | */ 154 | public static boolean isExternalStorageDocument(Uri uri) { 155 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 156 | } 157 | 158 | /** 159 | * @param uri The Uri to check. 160 | * @return Whether the Uri authority is DownloadsProvider. 161 | */ 162 | public static boolean isDownloadsDocument(Uri uri) { 163 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 164 | } 165 | 166 | /** 167 | * @param uri The Uri to check. 168 | * @return Whether the Uri authority is MediaProvider. 169 | */ 170 | public static boolean isMediaDocument(Uri uri) { 171 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 172 | } 173 | 174 | /** 175 | * @param uri The Uri to check. 176 | * @return Whether the Uri authority is Google Photos. 177 | */ 178 | public static boolean isGooglePhotosUri(Uri uri) { 179 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 180 | } 181 | 182 | /** 183 | * Get the value of the data column for this Uri. This is useful for 184 | * MediaStore Uris, and other file-based ContentProviders. 185 | * 186 | * @param context The context. 187 | * @param uri The Uri to query. 188 | * @param selection (Optional) Filter used in the query. 189 | * @param selectionArgs (Optional) Selection arguments used in the query. 190 | * @return The value of the _data column, which is typically a file path. 191 | */ 192 | public static String getDataColumn(Context context, Uri uri, String selection, 193 | String[] selectionArgs) { 194 | 195 | Cursor cursor = null; 196 | final String column = "_data"; 197 | final String[] projection = { 198 | column 199 | }; 200 | 201 | try { 202 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 203 | null); 204 | if (cursor != null && cursor.moveToFirst()) { 205 | 206 | final int column_index = cursor.getColumnIndexOrThrow(column); 207 | return cursor.getString(column_index); 208 | } 209 | } finally { 210 | if (cursor != null) 211 | cursor.close(); 212 | } 213 | return null; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.utils; 2 | 3 | import android.util.Log; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | /** 8 | * 创建文件 和 文件夹 9 | * Created by yu on 2016/8/21. 10 | */ 11 | public class FileUtils { 12 | 13 | private static final String TAG = "FileUtils"; 14 | 15 | public static final int FLAG_SUCCESS = 1;//创建成功 16 | public static final int FLAG_EXISTS = 2;//已存在 17 | public static final int FLAG_FAILED = 3;//创建失败 18 | 19 | 20 | /** 21 | * 创建 单个 文件 22 | * 23 | * @param filePath 待创建的文件路径 24 | * @return 结果码 25 | */ 26 | public static int CreateFile(String filePath) { 27 | File file = new File(filePath); 28 | if (file.exists()) { 29 | Log.e(TAG, "The file [ " + filePath + " ] has already exists"); 30 | return FLAG_EXISTS; 31 | } 32 | if (filePath.endsWith(File.separator)) {// 以 路径分隔符 结束,说明是文件夹 33 | Log.e(TAG, "The file [ " + filePath + " ] can not be a directory"); 34 | return FLAG_FAILED; 35 | } 36 | 37 | //判断父目录是否存在 38 | if (!file.getParentFile().exists()) { 39 | //父目录不存在 创建父目录 40 | Log.d(TAG, "creating parent directory..."); 41 | if (!file.getParentFile().mkdirs()) { 42 | Log.e(TAG, "created parent directory failed."); 43 | return FLAG_FAILED; 44 | } 45 | } 46 | 47 | //创建目标文件 48 | try { 49 | if (file.createNewFile()) {//创建文件成功 50 | Log.i(TAG, "create file [ " + filePath + " ] success"); 51 | return FLAG_SUCCESS; 52 | } 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | Log.e(TAG, "create file [ " + filePath + " ] failed"); 56 | return FLAG_FAILED; 57 | } 58 | 59 | return FLAG_FAILED; 60 | } 61 | 62 | /** 63 | * 创建 文件夹 64 | * 65 | * @param dirPath 文件夹路径 66 | * @return 结果码 67 | */ 68 | public static int createDir(String dirPath) { 69 | 70 | File dir = new File(dirPath); 71 | //文件夹是否已经存在 72 | if (dir.exists()) { 73 | Log.w(TAG, "The directory [ " + dirPath + " ] has already exists"); 74 | return FLAG_EXISTS; 75 | } 76 | if (!dirPath.endsWith(File.separator)) {//不是以 路径分隔符 "/" 结束,则添加路径分隔符 "/" 77 | dirPath = dirPath + File.separator; 78 | } 79 | //创建文件夹 80 | if (dir.mkdirs()) { 81 | Log.d(TAG, "create directory [ " + dirPath + " ] success"); 82 | return FLAG_SUCCESS; 83 | } 84 | 85 | Log.e(TAG, "create directory [ " + dirPath + " ] failed"); 86 | return FLAG_FAILED; 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/utils/RectUtils.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.utils; 2 | 3 | import android.graphics.RectF; 4 | 5 | public class RectUtils { 6 | 7 | /** 8 | * Gets a float array of the 2D coordinates representing a rectangles 9 | * corners. 10 | * The order of the corners in the float array is: 11 | * 0------->1 12 | * ^ | 13 | * | | 14 | * | v 15 | * 3<-------2 16 | * 17 | * @param r the rectangle to get the corners of 18 | * @return the float array of corners (8 floats) 19 | */ 20 | public static float[] getCornersFromRect(RectF r) { 21 | return new float[]{ 22 | r.left, r.top, 23 | r.right, r.top, 24 | r.right, r.bottom, 25 | r.left, r.bottom 26 | }; 27 | } 28 | 29 | /** 30 | * Gets a float array of two lengths representing a rectangles width and height 31 | * The order of the corners in the input float array is: 32 | * 0------->1 33 | * ^ | 34 | * | | 35 | * | v 36 | * 3<-------2 37 | * 38 | * @param corners the float array of corners (8 floats) 39 | * @return the float array of width and height (2 floats) 40 | */ 41 | public static float[] getRectSidesFromCorners(float[] corners) { 42 | return new float[]{(float) Math.sqrt(Math.pow(corners[0] - corners[2], 2) + Math.pow(corners[1] - corners[3], 2)), 43 | (float) Math.sqrt(Math.pow(corners[2] - corners[4], 2) + Math.pow(corners[3] - corners[5], 2))}; 44 | } 45 | 46 | public static float[] getCenterFromRect(RectF r) { 47 | return new float[]{r.centerX(), r.centerY()}; 48 | } 49 | 50 | /** 51 | * Takes an array of 2D coordinates representing corners and returns the 52 | * smallest rectangle containing those coordinates. 53 | * 54 | * @param array array of 2D coordinates 55 | * @return smallest rectangle containing coordinates 56 | */ 57 | public static RectF trapToRect(float[] array) { 58 | RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 59 | Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); 60 | for (int i = 1; i < array.length; i += 2) { 61 | float x = Math.round(array[i - 1] * 10) / 10.f; 62 | float y = Math.round(array[i] * 10) / 10.f; 63 | r.left = (x < r.left) ? x : r.left; 64 | r.top = (y < r.top) ? y : r.top; 65 | r.right = (x > r.right) ? x : r.right; 66 | r.bottom = (y > r.bottom) ? y : r.bottom; 67 | } 68 | r.sort(); 69 | return r; 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/utils/VideoCropHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.utils 2 | 3 | import android.content.Context 4 | import com.mill.cropcut.bean.LocalVideoBean 5 | import com.mill.cropcut.view.VHwCropView 6 | import com.mill.cropcut.utils.VideoFFCrop.FFListener 7 | import android.media.MediaMetadataRetriever 8 | import android.os.AsyncTask 9 | import android.graphics.Bitmap 10 | import android.os.Environment 11 | import android.util.Log 12 | import java.lang.Exception 13 | 14 | /** 15 | * Created by lulei-ms on 2017/8/23. 16 | */ 17 | private const val TAG = "VideoCropHelper" 18 | 19 | object VideoCropHelper { 20 | const val WHA = 2 / 3f //尺寸裁切成宽高比 2:3 21 | 22 | /** 23 | * 裁切横屏 视频 24 | * 25 | * @param context 26 | * @param videoBean 27 | * @param mVCropView 28 | * @param listener 29 | */ 30 | fun cropWpVideo( 31 | context: Context, 32 | videoBean: LocalVideoBean, 33 | mVCropView: VHwCropView, 34 | listener: FFListener 35 | ) { 36 | if (videoBean == null) { 37 | Log.e(TAG, "cropWpVideo: videoBean=null"); 38 | return 39 | } 40 | val srcVideo = videoBean.src_path.trim() 41 | var startPo = 0 42 | var duration = 0 43 | val srcW = videoBean.width 44 | val srcH = videoBean.height 45 | var width = 0 46 | var height = 0 47 | var x = 0 48 | var y = 0 49 | // if (srcW <= srcH) { 50 | // Log.e(TAG, "cropWpVideo: srcW <= srcH") 51 | // return 52 | // } 53 | width = (srcH * WHA).toInt() 54 | height = srcH 55 | if (mVCropView != null) { 56 | val rectF = mVCropView.overlayView.cropViewRect 57 | x = (srcW * rectF.left / mVCropView.width).toInt() 58 | startPo = mVCropView.videoView.startPo / 1000 59 | duration = mVCropView.videoView.endPo / 1000 - startPo 60 | } else { 61 | x = (srcW - width) / 2 62 | startPo = 0 63 | duration = (videoBean.duration / 1000).toInt() 64 | } 65 | duration = if (duration <= 0) 1 else duration //最小为1 66 | y = 0 67 | Log.d(VideoFFCrop.TAG, "Media $videoBean====$x") 68 | var start = srcVideo.lastIndexOf(".") 69 | if (start == -1) { 70 | start = srcVideo.length 71 | } 72 | // val destPath = srcVideo.substring(0, start) + "_wp.mp4" 73 | val destPath = Environment.getExternalStorageDirectory().path +"/1234/cc.mp4" 74 | FileUtils.createDir(Environment.getExternalStorageDirectory().path +"/1234") 75 | Log.d(TAG, "cropWpVideo: $destPath") 76 | VideoFFCrop.instance?.cropVideo( 77 | context, 78 | srcVideo, 79 | destPath, 80 | startPo, 81 | duration, 82 | listener 83 | ) 84 | } 85 | 86 | /** 87 | * 获取本地视频信息 88 | */ 89 | fun getLocalVideoInfo(path: String?): LocalVideoBean { 90 | val info = LocalVideoBean() 91 | info.src_path = path 92 | val mmr = MediaMetadataRetriever() 93 | try { 94 | mmr.setDataSource(path) 95 | info.duration = 96 | java.lang.Long.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) 97 | info.width = 98 | Integer.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)) 99 | info.height = 100 | Integer.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)) 101 | } catch (e: Exception) { 102 | e.printStackTrace() 103 | } finally { 104 | mmr.release() 105 | } 106 | return info 107 | } 108 | 109 | /** 110 | * 获取视频帧列表 111 | * 112 | * @param path 113 | * @param count 期望个数 114 | * @param width 期望压缩后宽度 115 | * @param height 期望压缩后高度 116 | * @param listener 117 | */ 118 | @JvmStatic 119 | fun getLocalVideoBitmap( 120 | path: String?, 121 | count: Int, 122 | width: Int, 123 | height: Int, 124 | listener: OnBitmapListener? 125 | ) { 126 | val task: AsyncTask = object : AsyncTask() { 127 | 128 | override fun doInBackground(vararg params: Any?): Any? { 129 | val mmr = MediaMetadataRetriever() 130 | try { 131 | mmr.setDataSource(path) 132 | val duration = 133 | java.lang.Long.valueOf(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000 134 | val inv = duration / count 135 | var i: Long = 0 136 | while (i < duration) { 137 | 138 | //注意getFrameAtTime方法的timeUs 是微妙, 1us * 1000 * 1000 = 1s 139 | val bitmap = 140 | mmr.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) 141 | // Log.d(VideoFFCrop.TAG, "getFrameAtTime "+ i + "===" + bitmap.getWidth() + "===" + bitmap.getHeight()); 142 | val destBitmap = Bitmap.createScaledBitmap(bitmap!!, width, height, true) 143 | Log.d( 144 | VideoFFCrop.TAG, 145 | "getFrameAtTime " + i + "===" + destBitmap.width + "===" + destBitmap.height 146 | ) 147 | bitmap.recycle() 148 | publishProgress(destBitmap) 149 | i += inv 150 | } 151 | } catch (e: Throwable) { 152 | e.printStackTrace() 153 | } finally { 154 | mmr.release() 155 | } 156 | return null 157 | } 158 | 159 | override fun onProgressUpdate(vararg values: Any?) { 160 | listener?.onBitmapGet(values[0] as Bitmap) 161 | } 162 | 163 | override fun onPostExecute(result: Any?) {} 164 | } 165 | task.execute() 166 | } 167 | 168 | interface OnBitmapListener { 169 | fun onBitmapGet(bitmap: Bitmap?) 170 | } 171 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/utils/VideoFFCrop.kt: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.AsyncTask 6 | import android.widget.Toast 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.util.* 12 | 13 | /** 14 | * ffmpeg.so 命令行裁剪 15 | * Created by lulei-ms on 2017/8/22. 16 | */ 17 | @SuppressLint("StaticFieldLeak") 18 | class VideoFFCrop private constructor() { 19 | /** 20 | * 初始化 21 | */ 22 | fun init(context: Context) { 23 | val task: AsyncTask = 24 | object : AsyncTask() { 25 | override fun doInBackground(vararg params: Any?): Any? { 26 | val executablePath = context.applicationInfo.nativeLibraryDir + "/ffmpeg.so" 27 | Log(TAG, "initializing...") 28 | var ffcutSrc: InputStream? = null 29 | var ffcutDest: FileOutputStream? = null 30 | try { 31 | val exFile = File(executablePath) 32 | ffcutSrc = context.assets.open("ffmpeg.so") 33 | if (exFile != null && exFile.exists() && ffcutSrc.available() 34 | .toLong() == exFile.length() 35 | ) { 36 | Log(TAG, "initialized already...") 37 | return null 38 | } 39 | ffcutDest = FileOutputStream(executablePath) 40 | Log(TAG, "copying executable...") 41 | val buf = ByteArray(96 * 1024) 42 | var length = 0 43 | while (ffcutSrc.read(buf).also { length = it } != -1) { 44 | ffcutDest.write(buf, 0, length) 45 | } 46 | ffcutDest.flush() 47 | ffcutDest.close() 48 | ffcutSrc.close() 49 | Log(TAG, "executable is copyed, applying permissions...") 50 | val chmod = 51 | Runtime.getRuntime().exec("/system/bin/chmod 755 $executablePath") 52 | chmod.waitFor() 53 | Log(TAG, "ffcut is initialized") 54 | } catch (e: Exception) { 55 | Log( 56 | TAG, 57 | "ffcut initialization is failed, " + e.javaClass.name + ": " + e.message 58 | ) 59 | } finally { 60 | if (ffcutSrc != null) { 61 | try { 62 | ffcutSrc.close() 63 | } catch (e: IOException) { 64 | e.printStackTrace() 65 | } 66 | } 67 | if (ffcutDest != null) { 68 | try { 69 | ffcutDest.close() 70 | } catch (e: IOException) { 71 | e.printStackTrace() 72 | } 73 | } 74 | } 75 | return null 76 | } 77 | 78 | override fun onProgressUpdate(vararg values: Any?) {} 79 | override fun onPostExecute(result: Any?) {} 80 | } 81 | task.execute() 82 | } 83 | 84 | /** 85 | * 视频 时长&尺寸裁剪 86 | */ 87 | fun cropVideo( 88 | context: Context, srcVideoPath: String, destPath: String, start: Int, 89 | duration: Int, listener: FFListener? 90 | ) { 91 | val task: AsyncTask = object : AsyncTask() { 92 | override fun doInBackground(vararg params: String?): Int? { 93 | val cmd = params[0] 94 | Log(TAG, "running command $cmd") 95 | return try { 96 | val ffcut = Runtime.getRuntime().exec(cmd) 97 | val error = ffcut.errorStream 98 | val errorScanner = Scanner(error) 99 | var count = 0 100 | while (errorScanner.hasNextLine()) { 101 | val line = errorScanner.nextLine() 102 | Log(TAG, "ffmpeg: $line") 103 | publishProgress(++count) 104 | } 105 | ffcut.waitFor() 106 | } catch (e: Exception) { 107 | Log(TAG, "exception " + e.javaClass.name + ": " + e.message) 108 | 200 109 | } 110 | } 111 | 112 | override fun onProgressUpdate(vararg values: Int?) { 113 | // Log(TAG, "progress: " + values[0] + "%") 114 | val fz = values[0] 115 | val fm = duration * 1000 / 100 116 | var progress = (fz?.times(100) ?: 0) / fm 117 | progress = if (progress > 99) 99 else progress 118 | listener?.onProgress(progress) 119 | } 120 | 121 | override fun onPostExecute(result: Int) { 122 | Log(TAG, "ffmpeg is finished with code $result") 123 | if (result == 0) { 124 | if (listener != null) { 125 | listener.onProgress(100) 126 | listener.onFinish() 127 | Log(TAG, "finished ,video path = $destPath") 128 | Toast.makeText(context,"video path = $destPath",Toast.LENGTH_SHORT).show() 129 | } 130 | } else { 131 | listener?.onFail("crop doInBackground exception") 132 | } 133 | } 134 | 135 | override fun onCancelled() { 136 | listener?.onFail("crop canceled") 137 | } 138 | } 139 | 140 | //-ss 0 -t 5 时间裁切 141 | //-strict -2 -vf crop=500:500:0:100 尺寸裁切 142 | val cmd = (context.applicationInfo.nativeLibraryDir + "/ffmpeg.so" + " -y -i " 143 | + "" + srcVideoPath + "" //加引号避免名字有空格无法识别 144 | + " -ss " + start + " -t " + duration // + " -strict -2 -vf crop=" + width + ":" + height + ":" + x + ":" + y + " -preset fast " 145 | + " -c copy " 146 | + "" + destPath + "") //加引号避免名字有空格无法识别 147 | android.util.Log.d(TAG, "cropVideo: $cmd") 148 | task.execute(cmd) 149 | } 150 | 151 | /** 152 | * Log信息 153 | * 154 | * @param tag 155 | * @param msg 156 | */ 157 | fun Log(tag: String?, msg: String?) { 158 | if (isDebug) { 159 | android.util.Log.d(tag, msg!!) 160 | } 161 | } 162 | 163 | /** 164 | * 回调监听 165 | */ 166 | interface FFListener { 167 | /** 168 | * 169 | */ 170 | fun onProgress(progress: Int?) 171 | 172 | /** 173 | * 174 | */ 175 | fun onFinish() 176 | 177 | /** 178 | * 179 | */ 180 | fun onFail(msg: String?) 181 | } 182 | 183 | companion object { 184 | const val TAG = "VideoFFCrop" 185 | private var mVideoFFCrop: VideoFFCrop? = null 186 | private const val isDebug = true 187 | val instance: VideoFFCrop? 188 | get() { 189 | if (mVideoFFCrop == null) { 190 | synchronized(VideoFFCrop::class.java) { 191 | if (mVideoFFCrop == null) { 192 | mVideoFFCrop = VideoFFCrop() 193 | } 194 | } 195 | } 196 | return mVideoFFCrop 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/LocalVideoView.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.media.MediaPlayer; 5 | import android.os.Handler; 6 | import android.text.TextUtils; 7 | import android.util.AttributeSet; 8 | import android.widget.VideoView; 9 | 10 | /** 11 | * 视频本地播放 12 | * Created by lulei-ms on 2017/8/23. 13 | */ 14 | public class LocalVideoView extends VideoView implements MediaPlayer.OnCompletionListener { 15 | private String mFilePath; 16 | private int startPo; //单位:ms 17 | private int endPo; //单位:ms 18 | 19 | private boolean isStart = false; 20 | 21 | private Handler mHandler = new Handler(); 22 | private Runnable countTimeRun = new Runnable() { 23 | public void run() { 24 | // 获得当前播放时间 25 | int currentPosition = getCurrentPosition(); 26 | // Log.d(VideoFFCrop.TAG, "countTimeRun " + currentPosition); 27 | onTimePlaying(currentPosition); 28 | mHandler.postDelayed(countTimeRun, 1000); 29 | } 30 | }; 31 | 32 | public LocalVideoView(Context context) { 33 | super(context); 34 | } 35 | 36 | public LocalVideoView(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | } 39 | 40 | public void setLocalPath(String path, int endPo) { 41 | if (!TextUtils.isEmpty(path)) { 42 | mFilePath = path; 43 | setVideoPath(path); 44 | setOnCompletionListener(this); 45 | start(); 46 | 47 | this.startPo = 0; 48 | this.endPo = endPo; 49 | } 50 | } 51 | 52 | public int getStartPo() { 53 | return startPo; 54 | } 55 | 56 | public int getEndPo() { 57 | return endPo; 58 | } 59 | 60 | public void setStarEndPo(int start, int end) { 61 | startPo = start; 62 | endPo = end; 63 | start0(); 64 | } 65 | 66 | public void start() { 67 | if (!isStart) { 68 | isStart = true; 69 | mHandler.post(countTimeRun); 70 | seekTo(startPo); 71 | } 72 | super.start(); 73 | } 74 | 75 | @Override 76 | public void pause() { 77 | if (isStart) { 78 | isStart = false; 79 | mHandler.removeCallbacks(countTimeRun); 80 | } 81 | super.pause(); 82 | } 83 | 84 | @Override 85 | public void stopPlayback() { 86 | if (isStart) { 87 | isStart = false; 88 | mHandler.removeCallbacks(countTimeRun); 89 | } 90 | super.stopPlayback(); 91 | } 92 | 93 | @Override 94 | protected void onDetachedFromWindow() { 95 | stopPlayback(); 96 | super.onDetachedFromWindow(); 97 | } 98 | 99 | @Override 100 | public void onCompletion(MediaPlayer mp) { 101 | //从头开始,循环播放 102 | start0(); 103 | } 104 | 105 | public void start0() { 106 | if (isStart) { 107 | seekTo(startPo); 108 | start(); 109 | } 110 | } 111 | 112 | private void onTimePlaying(int time) { 113 | if (endPo > 0 && time >= endPo) { 114 | //从头开始,循环播放 115 | start0(); 116 | } 117 | } 118 | 119 | @Override 120 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 121 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 122 | 123 | if (onVideoSizeChangeedListener != null) { 124 | onVideoSizeChangeedListener.onVideoSizeChanged(getMeasuredWidth(), getMeasuredHeight()); 125 | } 126 | } 127 | 128 | private OnVideoSizeChangeedListener onVideoSizeChangeedListener; 129 | 130 | public void setOnVideoSizeChangeedListener(OnVideoSizeChangeedListener onVideoSizeChangeedListener) { 131 | this.onVideoSizeChangeedListener = onVideoSizeChangeedListener; 132 | } 133 | 134 | 135 | public interface OnVideoSizeChangeedListener { 136 | public void onVideoSizeChanged(int width, int height); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/OverlayView.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.RectF; 9 | import android.graphics.Region; 10 | import android.os.Build; 11 | import android.util.AttributeSet; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | 15 | import com.mill.cropcut.R; 16 | import com.mill.cropcut.utils.RectUtils; 17 | 18 | /** 19 | * Created by Oleksii Shliama (https://github.com/shliama). 20 | *

21 | * This view is used for drawing the overlay on top of the image. It may have frame, crop guidelines and dimmed area. 22 | * This must have LAYER_TYPE_SOFTWARE to draw itself properly. 23 | */ 24 | public class OverlayView extends View { 25 | 26 | public static final int FREESTYLE_CROP_MODE_DISABLE = 0; 27 | public static final int FREESTYLE_CROP_MODE_ENABLE = 1; 28 | public static final int FREESTYLE_CROP_MODE_ENABLE_WITH_PASS_THROUGH = 2; 29 | 30 | public static final boolean DEFAULT_SHOW_CROP_FRAME = true; 31 | public static final boolean DEFAULT_SHOW_CROP_GRID = true; 32 | public static final boolean DEFAULT_CIRCLE_DIMMED_LAYER = false; 33 | public static final int DEFAULT_FREESTYLE_CROP_MODE = FREESTYLE_CROP_MODE_DISABLE; 34 | public static final int DEFAULT_CROP_GRID_ROW_COUNT = 2; 35 | public static final int DEFAULT_CROP_GRID_COLUMN_COUNT = 2; 36 | 37 | private RectF mCropViewRect = new RectF(); 38 | private final RectF mTempRect = new RectF(); 39 | 40 | protected int mThisWidth, mThisHeight; 41 | protected float[] mCropGridCorners; 42 | protected float[] mCropGridCenter; 43 | 44 | private int mCropGridRowCount, mCropGridColumnCount; 45 | private float mTargetAspectRatio; 46 | private float[] mGridPoints = null; 47 | private boolean mShowCropFrame, mShowCropGrid; 48 | private boolean mCircleDimmedLayer; 49 | private int mDimmedColor; 50 | private Path mCircularPath = new Path(); 51 | private Paint mDimmedStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 52 | private Paint mCropGridPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 53 | private Paint mCropFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 54 | private Paint mCropFrameCornersPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 55 | private int mFreestyleCropMode = DEFAULT_FREESTYLE_CROP_MODE; 56 | private float mPreviousTouchX = -1, mPreviousTouchY = -1; 57 | private int mCurrentTouchCornerIndex = -1; 58 | private int mTouchPointThreshold; 59 | private int mCropRectMinSize; 60 | private int mCropRectCornerTouchAreaLineLength; 61 | 62 | private OverlayViewChangeListener mCallback; 63 | 64 | private boolean mShouldSetupCropBounds; 65 | 66 | { 67 | mTouchPointThreshold = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_threshold); 68 | mCropRectMinSize = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_min_size); 69 | mCropRectCornerTouchAreaLineLength = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_area_line_length); 70 | } 71 | 72 | public OverlayView(Context context) { 73 | this(context, null); 74 | } 75 | 76 | public OverlayView(Context context, AttributeSet attrs) { 77 | this(context, attrs, 0); 78 | } 79 | 80 | public OverlayView(Context context, AttributeSet attrs, int defStyle) { 81 | super(context, attrs, defStyle); 82 | init(); 83 | } 84 | 85 | public OverlayViewChangeListener getOverlayViewChangeListener() { 86 | return mCallback; 87 | } 88 | 89 | public void setOverlayViewChangeListener(OverlayViewChangeListener callback) { 90 | mCallback = callback; 91 | } 92 | 93 | 94 | public RectF getCropViewRect() { 95 | return mCropViewRect; 96 | } 97 | 98 | @Deprecated 99 | /*** 100 | * Please use the new method {@link #getFreestyleCropMode() getFreestyleCropMode} method as we have more than 1 freestyle crop mode. 101 | */ 102 | public boolean isFreestyleCropEnabled() { 103 | return mFreestyleCropMode == FREESTYLE_CROP_MODE_ENABLE; 104 | } 105 | 106 | @Deprecated 107 | /*** 108 | * Please use the new method {@link #setFreestyleCropMode setFreestyleCropMode} method as we have more than 1 freestyle crop mode. 109 | */ 110 | public void setFreestyleCropEnabled(boolean freestyleCropEnabled) { 111 | mFreestyleCropMode = freestyleCropEnabled ? FREESTYLE_CROP_MODE_ENABLE : FREESTYLE_CROP_MODE_DISABLE; 112 | } 113 | 114 | public int getFreestyleCropMode() { 115 | return mFreestyleCropMode; 116 | } 117 | 118 | public void setFreestyleCropMode(int mFreestyleCropMode) { 119 | this.mFreestyleCropMode = mFreestyleCropMode; 120 | postInvalidate(); 121 | } 122 | 123 | /** 124 | * Setter for {@link #mCircleDimmedLayer} variable. 125 | * 126 | * @param circleDimmedLayer - set it to true if you want dimmed layer to be an circle 127 | */ 128 | public void setCircleDimmedLayer(boolean circleDimmedLayer) { 129 | mCircleDimmedLayer = circleDimmedLayer; 130 | } 131 | 132 | /** 133 | * Setter for crop grid rows count. 134 | * Resets {@link #mGridPoints} variable because it is not valid anymore. 135 | */ 136 | public void setCropGridRowCount(int cropGridRowCount) { 137 | mCropGridRowCount = cropGridRowCount; 138 | mGridPoints = null; 139 | } 140 | 141 | /** 142 | * Setter for crop grid columns count. 143 | * Resets {@link #mGridPoints} variable because it is not valid anymore. 144 | */ 145 | public void setCropGridColumnCount(int cropGridColumnCount) { 146 | mCropGridColumnCount = cropGridColumnCount; 147 | mGridPoints = null; 148 | } 149 | 150 | /** 151 | * Setter for {@link #mShowCropFrame} variable. 152 | * 153 | * @param showCropFrame - set to true if you want to see a crop frame rectangle on top of an image 154 | */ 155 | public void setShowCropFrame(boolean showCropFrame) { 156 | mShowCropFrame = showCropFrame; 157 | } 158 | 159 | /** 160 | * Setter for {@link #mShowCropGrid} variable. 161 | * 162 | * @param showCropGrid - set to true if you want to see a crop grid on top of an image 163 | */ 164 | public void setShowCropGrid(boolean showCropGrid) { 165 | mShowCropGrid = showCropGrid; 166 | } 167 | 168 | /** 169 | * Setter for {@link #mDimmedColor} variable. 170 | * 171 | * @param dimmedColor - desired color of dimmed area around the crop bounds 172 | */ 173 | public void setDimmedColor(int dimmedColor) { 174 | mDimmedColor = dimmedColor; 175 | } 176 | 177 | /** 178 | * Setter for crop frame stroke width 179 | */ 180 | public void setCropFrameStrokeWidth(int width) { 181 | mCropFramePaint.setStrokeWidth(width); 182 | } 183 | 184 | /** 185 | * Setter for crop grid stroke width 186 | */ 187 | public void setCropGridStrokeWidth(int width) { 188 | mCropGridPaint.setStrokeWidth(width); 189 | } 190 | 191 | /** 192 | * Setter for crop frame color 193 | */ 194 | public void setCropFrameColor(int color) { 195 | mCropFramePaint.setColor(color); 196 | } 197 | 198 | /** 199 | * Setter for crop grid color 200 | */ 201 | public void setCropGridColor(int color) { 202 | mCropGridPaint.setColor(color); 203 | } 204 | 205 | /** 206 | * This method sets aspect ratio for crop bounds. 207 | * 208 | * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9) 209 | */ 210 | public void setTargetAspectRatio(final float targetAspectRatio) { 211 | mTargetAspectRatio = targetAspectRatio; 212 | if (mThisWidth > 0) { 213 | setupCropBounds(); 214 | postInvalidate(); 215 | } else { 216 | mShouldSetupCropBounds = true; 217 | } 218 | } 219 | 220 | /** 221 | * This method setups crop bounds rectangles for given aspect ratio and view size. 222 | * {@link #mCropViewRect} is used to draw crop bounds - uses padding. 223 | */ 224 | public void setupCropBounds() { 225 | int height = (int) (mThisWidth / mTargetAspectRatio); 226 | if (height > mThisHeight) { 227 | int width = (int) (mThisHeight * mTargetAspectRatio); 228 | int halfDiff = (mThisWidth - width) / 2; 229 | mCropViewRect.set(getPaddingLeft() + halfDiff, getPaddingTop(), 230 | getPaddingLeft() + width + halfDiff, getPaddingTop() + mThisHeight); 231 | } else { 232 | int halfDiff = (mThisHeight - height) / 2; 233 | mCropViewRect.set(getPaddingLeft(), getPaddingTop() + halfDiff, 234 | getPaddingLeft() + mThisWidth, getPaddingTop() + height + halfDiff); 235 | } 236 | 237 | if (mCallback != null) { 238 | mCallback.onCropRectUpdated(mCropViewRect); 239 | } 240 | 241 | updateGridPoints(); 242 | } 243 | 244 | public void setCropViewRect(int left, int top, int right, int bottom) { 245 | mCropViewRect.set(left, top, right, bottom); 246 | invalidate(); 247 | } 248 | 249 | private void updateGridPoints() { 250 | mCropGridCorners = RectUtils.getCornersFromRect(mCropViewRect); 251 | mCropGridCenter = RectUtils.getCenterFromRect(mCropViewRect); 252 | 253 | mGridPoints = null; 254 | mCircularPath.reset(); 255 | mCircularPath.addCircle(mCropViewRect.centerX(), mCropViewRect.centerY(), 256 | Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, Path.Direction.CW); 257 | } 258 | 259 | protected void init() { 260 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && 261 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 262 | setLayerType(LAYER_TYPE_SOFTWARE, null); 263 | } 264 | } 265 | 266 | @Override 267 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 268 | super.onLayout(changed, left, top, right, bottom); 269 | if (changed) { 270 | left = getPaddingLeft(); 271 | top = getPaddingTop(); 272 | right = getWidth() - getPaddingRight(); 273 | bottom = getHeight() - getPaddingBottom(); 274 | mThisWidth = right - left; 275 | mThisHeight = bottom - top; 276 | 277 | if (mShouldSetupCropBounds) { 278 | mShouldSetupCropBounds = false; 279 | setTargetAspectRatio(mTargetAspectRatio); 280 | } 281 | } 282 | } 283 | 284 | /** 285 | * Along with image there are dimmed layer, crop bounds and crop guidelines that must be drawn. 286 | */ 287 | @Override 288 | protected void onDraw(Canvas canvas) { 289 | super.onDraw(canvas); 290 | drawDimmedLayer(canvas); 291 | drawCropGrid(canvas); 292 | } 293 | 294 | @Override 295 | public boolean onTouchEvent(MotionEvent event) { 296 | if (mCropViewRect.isEmpty() || mFreestyleCropMode == FREESTYLE_CROP_MODE_DISABLE) { 297 | return false; 298 | } 299 | 300 | float x = event.getX(); 301 | float y = event.getY(); 302 | 303 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 304 | mCurrentTouchCornerIndex = getCurrentTouchIndex(x, y); 305 | boolean shouldHandle = mCurrentTouchCornerIndex != -1; 306 | if (!shouldHandle) { 307 | mPreviousTouchX = -1; 308 | mPreviousTouchY = -1; 309 | } else if (mPreviousTouchX < 0) { 310 | mPreviousTouchX = x; 311 | mPreviousTouchY = y; 312 | } 313 | return shouldHandle; 314 | } 315 | 316 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) { 317 | if (event.getPointerCount() == 1 && mCurrentTouchCornerIndex != -1) { 318 | 319 | x = Math.min(Math.max(x, getPaddingLeft()), getWidth() - getPaddingRight()); 320 | y = Math.min(Math.max(y, getPaddingTop()), getHeight() - getPaddingBottom()); 321 | 322 | updateCropViewRect(x, y); 323 | 324 | mPreviousTouchX = x; 325 | mPreviousTouchY = y; 326 | 327 | return true; 328 | } 329 | } 330 | 331 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { 332 | mPreviousTouchX = -1; 333 | mPreviousTouchY = -1; 334 | mCurrentTouchCornerIndex = -1; 335 | 336 | if (mCallback != null) { 337 | mCallback.onCropRectUpdated(mCropViewRect); 338 | } 339 | } 340 | 341 | return false; 342 | } 343 | 344 | /** 345 | * * The order of the corners is: 346 | * 0------->1 347 | * ^ | 348 | * | 4 | 349 | * | v 350 | * 3<-------2 351 | */ 352 | private void updateCropViewRect(float touchX, float touchY) { 353 | mTempRect.set(mCropViewRect); 354 | 355 | switch (mCurrentTouchCornerIndex) { 356 | // resize rectangle 357 | case 0: 358 | mTempRect.set(touchX, touchY, mCropViewRect.right, mCropViewRect.bottom); 359 | break; 360 | case 1: 361 | mTempRect.set(mCropViewRect.left, touchY, touchX, mCropViewRect.bottom); 362 | break; 363 | case 2: 364 | mTempRect.set(mCropViewRect.left, mCropViewRect.top, touchX, touchY); 365 | break; 366 | case 3: 367 | mTempRect.set(touchX, mCropViewRect.top, mCropViewRect.right, touchY); 368 | break; 369 | // move rectangle 370 | case 4: 371 | mTempRect.offset(touchX - mPreviousTouchX, touchY - mPreviousTouchY); 372 | if (mTempRect.left > getLeft() && mTempRect.top > getTop() 373 | && mTempRect.right < getRight() && mTempRect.bottom < getBottom()) { 374 | mCropViewRect.set(mTempRect); 375 | updateGridPoints(); 376 | postInvalidate(); 377 | } 378 | return; 379 | } 380 | 381 | boolean changeHeight = mTempRect.height() >= mCropRectMinSize; 382 | boolean changeWidth = mTempRect.width() >= mCropRectMinSize; 383 | mCropViewRect.set( 384 | changeWidth ? mTempRect.left : mCropViewRect.left, 385 | changeHeight ? mTempRect.top : mCropViewRect.top, 386 | changeWidth ? mTempRect.right : mCropViewRect.right, 387 | changeHeight ? mTempRect.bottom : mCropViewRect.bottom); 388 | 389 | if (changeHeight || changeWidth) { 390 | updateGridPoints(); 391 | postInvalidate(); 392 | } 393 | } 394 | 395 | /** 396 | * * The order of the corners in the float array is: 397 | * 0------->1 398 | * ^ | 399 | * | 4 | 400 | * | v 401 | * 3<-------2 402 | * 403 | * @return - index of corner that is being dragged 404 | */ 405 | private int getCurrentTouchIndex(float touchX, float touchY) { 406 | int closestPointIndex = -1; 407 | double closestPointDistance = mTouchPointThreshold; 408 | for (int i = 0; i < 8; i += 2) { 409 | double distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2) 410 | + Math.pow(touchY - mCropGridCorners[i + 1], 2)); 411 | if (distanceToCorner < closestPointDistance) { 412 | closestPointDistance = distanceToCorner; 413 | closestPointIndex = i / 2; 414 | } 415 | } 416 | 417 | if (mFreestyleCropMode == FREESTYLE_CROP_MODE_ENABLE && closestPointIndex < 0 && mCropViewRect.contains(touchX, touchY)) { 418 | return 4; 419 | } 420 | 421 | // for (int i = 0; i <= 8; i += 2) { 422 | // 423 | // double distanceToCorner; 424 | // if (i < 8) { // corners 425 | // distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2) 426 | // + Math.pow(touchY - mCropGridCorners[i + 1], 2)); 427 | // } else { // center 428 | // distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCenter[0], 2) 429 | // + Math.pow(touchY - mCropGridCenter[1], 2)); 430 | // } 431 | // if (distanceToCorner < closestPointDistance) { 432 | // closestPointDistance = distanceToCorner; 433 | // closestPointIndex = i / 2; 434 | // } 435 | // } 436 | return closestPointIndex; 437 | } 438 | 439 | /** 440 | * This method draws dimmed area around the crop bounds. 441 | * 442 | * @param canvas - valid canvas object 443 | */ 444 | protected void drawDimmedLayer(Canvas canvas) { 445 | canvas.save(); 446 | if (mCircleDimmedLayer) { 447 | canvas.clipPath(mCircularPath, Region.Op.DIFFERENCE); 448 | } else { 449 | canvas.clipRect(mCropViewRect, Region.Op.DIFFERENCE); 450 | } 451 | canvas.drawColor(mDimmedColor); 452 | canvas.restore(); 453 | 454 | if (mCircleDimmedLayer) { // Draw 1px stroke to fix antialias 455 | canvas.drawCircle(mCropViewRect.centerX(), mCropViewRect.centerY(), 456 | Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, mDimmedStrokePaint); 457 | } 458 | } 459 | 460 | /** 461 | * This method draws crop bounds (empty rectangle) 462 | * and crop guidelines (vertical and horizontal lines inside the crop bounds) if needed. 463 | * 464 | * @param canvas - valid canvas object 465 | */ 466 | protected void drawCropGrid(Canvas canvas) { 467 | if (mShowCropGrid) { 468 | if (mGridPoints == null && !mCropViewRect.isEmpty()) { 469 | 470 | mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4]; 471 | 472 | int index = 0; 473 | for (int i = 0; i < mCropGridRowCount; i++) { 474 | mGridPoints[index++] = mCropViewRect.left; 475 | mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top; 476 | mGridPoints[index++] = mCropViewRect.right; 477 | mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top; 478 | } 479 | 480 | for (int i = 0; i < mCropGridColumnCount; i++) { 481 | mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left; 482 | mGridPoints[index++] = mCropViewRect.top; 483 | mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left; 484 | mGridPoints[index++] = mCropViewRect.bottom; 485 | } 486 | } 487 | 488 | if (mGridPoints != null) { 489 | canvas.drawLines(mGridPoints, mCropGridPaint); 490 | } 491 | } 492 | 493 | if (mShowCropFrame) { 494 | canvas.drawRect(mCropViewRect, mCropFramePaint); 495 | } 496 | 497 | if (mFreestyleCropMode != FREESTYLE_CROP_MODE_DISABLE) { 498 | canvas.save(); 499 | 500 | mTempRect.set(mCropViewRect); 501 | mTempRect.inset(mCropRectCornerTouchAreaLineLength, -mCropRectCornerTouchAreaLineLength); 502 | canvas.clipRect(mTempRect, Region.Op.DIFFERENCE); 503 | 504 | mTempRect.set(mCropViewRect); 505 | mTempRect.inset(-mCropRectCornerTouchAreaLineLength, mCropRectCornerTouchAreaLineLength); 506 | canvas.clipRect(mTempRect, Region.Op.DIFFERENCE); 507 | 508 | canvas.drawRect(mCropViewRect, mCropFrameCornersPaint); 509 | 510 | canvas.restore(); 511 | } 512 | } 513 | 514 | /** 515 | * This method extracts all needed values from the styled attributes. 516 | * Those are used to configure the view. 517 | */ 518 | @SuppressWarnings("deprecation") 519 | protected void processStyledAttributes(TypedArray a) { 520 | mCircleDimmedLayer = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_circle_dimmed_layer, DEFAULT_CIRCLE_DIMMED_LAYER); 521 | mDimmedColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_dimmed_color, 522 | getResources().getColor(R.color.ucrop_color_default_dimmed)); 523 | mDimmedStrokePaint.setColor(mDimmedColor); 524 | mDimmedStrokePaint.setStyle(Paint.Style.STROKE); 525 | mDimmedStrokePaint.setStrokeWidth(1); 526 | 527 | initCropFrameStyle(a); 528 | mShowCropFrame = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_frame, DEFAULT_SHOW_CROP_FRAME); 529 | 530 | initCropGridStyle(a); 531 | mShowCropGrid = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_grid, DEFAULT_SHOW_CROP_GRID); 532 | } 533 | 534 | /** 535 | * This method setups Paint object for the crop bounds. 536 | */ 537 | @SuppressWarnings("deprecation") 538 | private void initCropFrameStyle(TypedArray a) { 539 | int cropFrameStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_frame_stroke_size, 540 | getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width)); 541 | int cropFrameColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_frame_color, 542 | getResources().getColor(R.color.ucrop_color_default_crop_frame)); 543 | mCropFramePaint.setStrokeWidth(cropFrameStrokeSize); 544 | mCropFramePaint.setColor(cropFrameColor); 545 | mCropFramePaint.setStyle(Paint.Style.STROKE); 546 | 547 | mCropFrameCornersPaint.setStrokeWidth(cropFrameStrokeSize * 3); 548 | mCropFrameCornersPaint.setColor(cropFrameColor); 549 | mCropFrameCornersPaint.setStyle(Paint.Style.STROKE); 550 | } 551 | 552 | /** 553 | * This method setups Paint object for the crop guidelines. 554 | */ 555 | @SuppressWarnings("deprecation") 556 | private void initCropGridStyle(TypedArray a) { 557 | int cropGridStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_grid_stroke_size, 558 | getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width)); 559 | int cropGridColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_grid_color, 560 | getResources().getColor(R.color.ucrop_color_default_crop_grid)); 561 | mCropGridPaint.setStrokeWidth(cropGridStrokeSize); 562 | mCropGridPaint.setColor(cropGridColor); 563 | 564 | mCropGridRowCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_row_count, DEFAULT_CROP_GRID_ROW_COUNT); 565 | mCropGridColumnCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_column_count, DEFAULT_CROP_GRID_COLUMN_COUNT); 566 | } 567 | 568 | 569 | 570 | 571 | /** 572 | * Created by Oleksii Shliama. 573 | */ 574 | public interface OverlayViewChangeListener { 575 | 576 | void onCropRectUpdated(RectF cropRect); 577 | 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/RangeSlider.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.graphics.drawable.Drawable; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.ViewConfiguration; 12 | import android.view.ViewGroup; 13 | 14 | import com.mill.cropcut.R; 15 | 16 | /** 17 | * 时长选择拖动框 18 | * Created by lulei-ms on 2017/8/23. 19 | */ 20 | public class RangeSlider extends ViewGroup { 21 | 22 | private static final int DEFAULT_LINE_SIZE = 1; 23 | private static final int DEFAULT_THUMB_WIDTH = 7; 24 | private static final int DEFAULT_TICK_START = 0; 25 | private static final int DEFAULT_TICK_END = 5; 26 | private static final int DEFAULT_TICK_INTERVAL = 1; 27 | private static final int DEFAULT_MASK_BACKGROUND = 0xA0000000; 28 | private static final int DEFAULT_LINE_COLOR = 0xFF000000; 29 | public static final int TYPE_LEFT = 1; 30 | public static final int TYPE_RIGHT = 2; 31 | 32 | private final Paint mLinePaint, mBgPaint; 33 | private final ThumbView mLeftThumb, mRightThumb; 34 | 35 | private int mTouchSlop; 36 | private int mOriginalX, mLastX; 37 | 38 | private int mThumbWidth; 39 | 40 | private int mTickStart = DEFAULT_TICK_START; 41 | private int mTickEnd = DEFAULT_TICK_END; 42 | private int mTickInterval = DEFAULT_TICK_INTERVAL; 43 | private int mTickCount = (mTickEnd - mTickStart) / mTickInterval; 44 | 45 | private float mLineSize; 46 | 47 | private boolean mIsDragging; 48 | 49 | private OnRangeChangeListener mRangeChangeListener; 50 | 51 | public RangeSlider(Context context) { 52 | this(context, null); 53 | } 54 | 55 | public RangeSlider(Context context, AttributeSet attrs) { 56 | this(context, attrs, 0); 57 | } 58 | 59 | public RangeSlider(Context context, AttributeSet attrs, int defStyleAttr) { 60 | super(context, attrs, defStyleAttr); 61 | 62 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RangeSlider, 0, 0); 63 | mThumbWidth = array.getDimensionPixelOffset(R.styleable.RangeSlider_thumbWidth, DEFAULT_THUMB_WIDTH); 64 | mLineSize = array.getDimensionPixelOffset(R.styleable.RangeSlider_lineHeight, DEFAULT_LINE_SIZE); 65 | mBgPaint = new Paint(); 66 | mBgPaint.setColor(array.getColor(R.styleable.RangeSlider_maskColor, DEFAULT_MASK_BACKGROUND)); 67 | 68 | mLinePaint = new Paint(); 69 | mLinePaint.setColor(array.getColor(R.styleable.RangeSlider_lineColor, DEFAULT_LINE_COLOR)); 70 | 71 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 72 | 73 | Drawable lDrawable = array.getDrawable(R.styleable.RangeSlider_leftThumbDrawable); 74 | Drawable rDrawable = array.getDrawable(R.styleable.RangeSlider_rightThumbDrawable); 75 | mLeftThumb = new ThumbView(context, mThumbWidth, lDrawable == null ? new ColorDrawable(DEFAULT_LINE_COLOR) : lDrawable); 76 | mRightThumb = new ThumbView(context, mThumbWidth, rDrawable == null ? new ColorDrawable(DEFAULT_LINE_COLOR) : rDrawable); 77 | setTickCount(array.getInteger(R.styleable.RangeSlider_tickCount, DEFAULT_TICK_END)); 78 | setRangeIndex(array.getInteger(R.styleable.RangeSlider_leftThumbIndex, DEFAULT_TICK_START), 79 | array.getInteger(R.styleable.RangeSlider_rightThumbIndex, mTickCount)); 80 | array.recycle(); 81 | 82 | addView(mLeftThumb); 83 | addView(mRightThumb); 84 | 85 | setWillNotDraw(false); 86 | } 87 | 88 | public void setThumbWidth(int thumbWidth) { 89 | mThumbWidth = thumbWidth; 90 | mLeftThumb.setThumbWidth(thumbWidth); 91 | mRightThumb.setThumbWidth(thumbWidth); 92 | } 93 | 94 | public void setLeftThumbDrawable(Drawable drawable) { 95 | mLeftThumb.setThumbDrawable(drawable); 96 | } 97 | 98 | public void setRightThumbDrawable(Drawable drawable) { 99 | mRightThumb.setThumbDrawable(drawable); 100 | } 101 | 102 | public void setLineColor(int color) { 103 | mLinePaint.setColor(color); 104 | } 105 | 106 | public void setLineSize(float lineSize) { 107 | mLineSize = lineSize; 108 | } 109 | 110 | public void setMaskColor(int color) { 111 | mBgPaint.setColor(color); 112 | } 113 | 114 | @Override 115 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 116 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY); 117 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 118 | mLeftThumb.measure(widthMeasureSpec, heightMeasureSpec); 119 | mRightThumb.measure(widthMeasureSpec, heightMeasureSpec); 120 | } 121 | 122 | @Override 123 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 124 | final int lThumbWidth = mLeftThumb.getMeasuredWidth(); 125 | final int lThumbHeight = mLeftThumb.getMeasuredHeight(); 126 | mLeftThumb.layout(0, 0, lThumbWidth, lThumbHeight); 127 | mRightThumb.layout(0, 0, lThumbWidth, lThumbHeight); 128 | } 129 | 130 | @Override 131 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 132 | moveThumbByIndex(mLeftThumb, mLeftThumb.getRangeIndex()); 133 | moveThumbByIndex(mRightThumb, mRightThumb.getRangeIndex()); 134 | } 135 | 136 | @Override 137 | protected void onDraw(Canvas canvas) { 138 | final int width = getMeasuredWidth(); 139 | final int height = getMeasuredHeight(); 140 | 141 | final int lThumbWidth = mLeftThumb.getMeasuredWidth(); 142 | final float lThumbOffset = mLeftThumb.getX(); 143 | final float rThumbOffset = mRightThumb.getX(); 144 | 145 | final float lineTop = mLineSize; 146 | final float lineBottom = height - mLineSize; 147 | 148 | 149 | // top line 150 | canvas.drawRect(lThumbWidth + lThumbOffset, 0, rThumbOffset, lineTop, mLinePaint); 151 | 152 | // bottom line 153 | canvas.drawRect(lThumbWidth + lThumbOffset, lineBottom, rThumbOffset, height, mLinePaint); 154 | 155 | if (lThumbOffset > mThumbWidth) { 156 | canvas.drawRect(0, 0, lThumbOffset + mThumbWidth, height, mBgPaint); 157 | } 158 | if (rThumbOffset < width - mThumbWidth) { 159 | canvas.drawRect(rThumbOffset, 0, width, height, mBgPaint); 160 | } 161 | 162 | } 163 | 164 | @Override 165 | public boolean onTouchEvent(MotionEvent event) { 166 | if (!isEnabled()) { 167 | return false; 168 | } 169 | 170 | boolean handle = false; 171 | 172 | switch (event.getAction()) { 173 | 174 | case MotionEvent.ACTION_DOWN: 175 | int x = (int) event.getX(); 176 | int y = (int) event.getY(); 177 | 178 | mLastX = mOriginalX = x; 179 | mIsDragging = false; 180 | 181 | if (!mLeftThumb.isPressed() && mLeftThumb.inInTarget(x, y)) { 182 | mLeftThumb.setPressed(true); 183 | handle = true; 184 | if (mRangeChangeListener != null) { 185 | mRangeChangeListener.onKeyDown(TYPE_LEFT); 186 | } 187 | } else if (!mRightThumb.isPressed() && mRightThumb.inInTarget(x, y)) { 188 | mRightThumb.setPressed(true); 189 | handle = true; 190 | if (mRangeChangeListener != null) { 191 | mRangeChangeListener.onKeyDown(TYPE_RIGHT); 192 | } 193 | } 194 | break; 195 | 196 | case MotionEvent.ACTION_CANCEL: 197 | case MotionEvent.ACTION_UP: 198 | mIsDragging = false; 199 | mOriginalX = mLastX = 0; 200 | getParent().requestDisallowInterceptTouchEvent(false); 201 | if (mLeftThumb.isPressed()) { 202 | releaseLeftThumb(); 203 | invalidate(); 204 | handle = true; 205 | if (mRangeChangeListener != null) { 206 | mRangeChangeListener.onKeyUp(TYPE_LEFT, mLeftThumb.getRangeIndex(), mRightThumb.getRangeIndex()); 207 | } 208 | } else if (mRightThumb.isPressed()) { 209 | releaseRightThumb(); 210 | invalidate(); 211 | handle = true; 212 | if (mRangeChangeListener != null) { 213 | mRangeChangeListener.onKeyUp(TYPE_RIGHT, mLeftThumb.getRangeIndex(), mRightThumb.getRangeIndex()); 214 | } 215 | } 216 | break; 217 | 218 | case MotionEvent.ACTION_MOVE: 219 | x = (int) event.getX(); 220 | 221 | if (!mIsDragging && Math.abs(x - mOriginalX) > mTouchSlop) { 222 | mIsDragging = true; 223 | } 224 | if (mIsDragging) { 225 | int moveX = x - mLastX; 226 | if (mLeftThumb.isPressed()) { 227 | getParent().requestDisallowInterceptTouchEvent(true); 228 | moveLeftThumbByPixel(moveX); 229 | handle = true; 230 | invalidate(); 231 | } else if (mRightThumb.isPressed()) { 232 | getParent().requestDisallowInterceptTouchEvent(true); 233 | moveRightThumbByPixel(moveX); 234 | handle = true; 235 | invalidate(); 236 | } 237 | } 238 | 239 | mLastX = x; 240 | break; 241 | } 242 | 243 | return handle; 244 | } 245 | 246 | private boolean isValidTickCount(int tickCount) { 247 | return (tickCount > 1); 248 | } 249 | 250 | private boolean indexOutOfRange(int leftThumbIndex, int rightThumbIndex) { 251 | return (leftThumbIndex < 0 || leftThumbIndex > mTickCount 252 | || rightThumbIndex < 0 253 | || rightThumbIndex > mTickCount); 254 | } 255 | 256 | private float getRangeLength() { 257 | int width = getMeasuredWidth(); 258 | if (width < mThumbWidth) { 259 | return 0; 260 | } 261 | return width - mThumbWidth; 262 | } 263 | 264 | private float getIntervalLength() { 265 | return getRangeLength() / mTickCount; 266 | } 267 | 268 | public int getNearestIndex(float x) { 269 | return Math.round(x / getIntervalLength()); 270 | } 271 | 272 | public int getLeftIndex() { 273 | return mLeftThumb.getRangeIndex(); 274 | } 275 | 276 | public int getRightIndex() { 277 | return mRightThumb.getRangeIndex(); 278 | } 279 | 280 | private void notifyRangeChange(int type) { 281 | if (mRangeChangeListener != null) { 282 | // mRangeChangeListener.onRangeChange(this, type, mLeftThumb.getRangeIndex(), mRightThumb.getRangeIndex()); 283 | } 284 | } 285 | 286 | public void setRangeChangeListener(OnRangeChangeListener rangeChangeListener) { 287 | mRangeChangeListener = rangeChangeListener; 288 | } 289 | 290 | /** 291 | * Sets the tick count in the RangeSlider. 292 | * 293 | * @param count Integer specifying the number of ticks. 294 | */ 295 | public void setTickCount(int count) { 296 | int tickCount = (count - mTickStart) / mTickInterval; 297 | if (isValidTickCount(tickCount)) { 298 | mTickEnd = count; 299 | mTickCount = tickCount; 300 | mRightThumb.setTickIndex(mTickCount); 301 | } else { 302 | throw new IllegalArgumentException("tickCount less than 2; invalid tickCount."); 303 | } 304 | } 305 | 306 | /** 307 | * The location of the thumbs according by the supplied index. 308 | * Numbered from 0 to mTickCount - 1 from the left. 309 | * 310 | * @param leftIndex Integer specifying the index of the left thumb 311 | * @param rightIndex Integer specifying the index of the right thumb 312 | */ 313 | public void setRangeIndex(int leftIndex, int rightIndex) { 314 | if (indexOutOfRange(leftIndex, rightIndex)) { 315 | throw new IllegalArgumentException( 316 | "Thumb index left " + leftIndex + ", or right " + rightIndex 317 | + " is out of bounds. Check that it is greater than the minimum (" 318 | + mTickStart + ") and less than the maximum value (" 319 | + mTickEnd + ")"); 320 | } else { 321 | if (mLeftThumb.getRangeIndex() != leftIndex) { 322 | mLeftThumb.setTickIndex(leftIndex); 323 | } 324 | if (mRightThumb.getRangeIndex() != rightIndex) { 325 | mRightThumb.setTickIndex(rightIndex); 326 | } 327 | } 328 | } 329 | 330 | private boolean moveThumbByIndex(ThumbView view, int index) { 331 | view.setX(index * getIntervalLength()); 332 | if (view.getRangeIndex() != index) { 333 | view.setTickIndex(index); 334 | return true; 335 | } 336 | return false; 337 | } 338 | 339 | private void moveLeftThumbByPixel(int pixel) { 340 | float x = mLeftThumb.getX() + pixel; 341 | float interval = getIntervalLength(); 342 | float start = mTickStart / mTickInterval * interval; 343 | float end = mTickEnd / mTickInterval * interval; 344 | 345 | if (x > start && x < end && x < mRightThumb.getX() - mThumbWidth) { 346 | mLeftThumb.setX(x); 347 | int index = getNearestIndex(x); 348 | if (mLeftThumb.getRangeIndex() != index) { 349 | mLeftThumb.setTickIndex(index); 350 | notifyRangeChange(TYPE_LEFT); 351 | } 352 | } 353 | } 354 | 355 | private void moveRightThumbByPixel(int pixel) { 356 | float x = mRightThumb.getX() + pixel; 357 | float interval = getIntervalLength(); 358 | float start = mTickStart / mTickInterval * interval; 359 | float end = mTickEnd / mTickInterval * interval; 360 | 361 | if (x > start && x < end && x > mLeftThumb.getX() + mThumbWidth) { 362 | mRightThumb.setX(x); 363 | int index = getNearestIndex(x); 364 | if (mRightThumb.getRangeIndex() != index) { 365 | mRightThumb.setTickIndex(index); 366 | notifyRangeChange(TYPE_RIGHT); 367 | } 368 | } 369 | } 370 | 371 | private void releaseLeftThumb() { 372 | int index = getNearestIndex(mLeftThumb.getX()); 373 | int endIndex = mRightThumb.getRangeIndex(); 374 | if (index >= endIndex) { 375 | index = endIndex - 1; 376 | } 377 | if (moveThumbByIndex(mLeftThumb, index)) { 378 | notifyRangeChange(TYPE_LEFT); 379 | } 380 | mLeftThumb.setPressed(false); 381 | } 382 | 383 | private void releaseRightThumb() { 384 | int index = getNearestIndex(mRightThumb.getX()); 385 | int endIndex = mLeftThumb.getRangeIndex(); 386 | if (index <= endIndex) { 387 | index = endIndex + 1; 388 | } 389 | if (moveThumbByIndex(mRightThumb, index)) { 390 | notifyRangeChange(TYPE_RIGHT); 391 | } 392 | mRightThumb.setPressed(false); 393 | } 394 | 395 | public interface OnRangeChangeListener { 396 | void onKeyDown(int type); 397 | void onKeyUp(int type, int leftPinIndex, int rightPinIndex); 398 | } 399 | 400 | } 401 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/ThumbView.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.graphics.drawable.Drawable; 6 | import android.util.TypedValue; 7 | import android.view.View; 8 | 9 | /** 10 | * @link RangeSlider 11 | * Created by lulei-ms on 2017/8/23. 12 | */ 13 | public class ThumbView extends View { 14 | 15 | private static final int EXTEND_TOUCH_SLOP = 15; 16 | 17 | private final int mExtendTouchSlop; 18 | 19 | private Drawable mThumbDrawable; 20 | 21 | private boolean mPressed; 22 | 23 | private int mThumbWidth; 24 | private int mTickIndex; 25 | 26 | public ThumbView(Context context, int thumbWidth, Drawable drawable) { 27 | super(context); 28 | mThumbWidth = thumbWidth; 29 | mThumbDrawable = drawable; 30 | mExtendTouchSlop = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 31 | EXTEND_TOUCH_SLOP, context.getResources().getDisplayMetrics()); 32 | setBackgroundDrawable(mThumbDrawable); 33 | } 34 | 35 | @Override 36 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 37 | super.onMeasure(MeasureSpec.makeMeasureSpec(mThumbWidth, MeasureSpec.EXACTLY), 38 | MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY)); 39 | 40 | mThumbDrawable.setBounds(0, 0, mThumbWidth, getMeasuredHeight()); 41 | } 42 | 43 | public void setThumbWidth(int thumbWidth) { 44 | mThumbWidth = thumbWidth; 45 | } 46 | 47 | public void setThumbDrawable(Drawable thumbDrawable) { 48 | mThumbDrawable = thumbDrawable; 49 | } 50 | 51 | public boolean inInTarget(int x, int y) { 52 | Rect rect = new Rect(); 53 | getHitRect(rect); 54 | rect.left -= mExtendTouchSlop; 55 | rect.right += mExtendTouchSlop; 56 | rect.top -= mExtendTouchSlop; 57 | rect.bottom += mExtendTouchSlop; 58 | return rect.contains(x, y); 59 | } 60 | 61 | public int getRangeIndex() { 62 | return mTickIndex; 63 | } 64 | 65 | public void setTickIndex(int tickIndex) { 66 | mTickIndex = tickIndex; 67 | } 68 | 69 | @Override 70 | public boolean isPressed() { 71 | return mPressed; 72 | } 73 | 74 | @Override 75 | public void setPressed(boolean pressed) { 76 | mPressed = pressed; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/VDurationCutView.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.util.AttributeSet; 6 | import android.util.Log; 7 | import android.view.LayoutInflater; 8 | import android.widget.RelativeLayout; 9 | import android.widget.TextView; 10 | 11 | import androidx.recyclerview.widget.LinearLayoutManager; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.mill.cropcut.R; 15 | import com.mill.cropcut.adapter.VDurationCutAdapter; 16 | import com.mill.cropcut.bean.LocalVideoBean; 17 | import com.mill.cropcut.utils.CutUtils; 18 | import com.mill.cropcut.utils.VideoCropHelper; 19 | import com.mill.cropcut.utils.VideoFFCrop; 20 | 21 | /** 22 | * 时长选择 23 | * Created by lulei-ms on 2017/8/23. 24 | */ 25 | public class VDurationCutView extends RelativeLayout implements RangeSlider.OnRangeChangeListener { 26 | public static final int THUMB_COUNT = 10; 27 | 28 | public interface IOnRangeChangeListener { 29 | void onKeyDown(); 30 | 31 | void onKeyUp(int startTime, int endTime); 32 | } 33 | 34 | private Context mContext; 35 | 36 | private TextView mTvTip; 37 | private RecyclerView mRecyclerView; 38 | private RangeSlider mRangeSlider; 39 | 40 | private long mVideoDuration; 41 | private long mVideoStartPos; 42 | private long mVideoEndPos; 43 | 44 | private VDurationCutAdapter mAdapter; 45 | 46 | private IOnRangeChangeListener mRangeChangeListener; 47 | 48 | public VDurationCutView(Context context) { 49 | super(context); 50 | 51 | init(context); 52 | } 53 | 54 | public VDurationCutView(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | 57 | init(context); 58 | } 59 | 60 | public VDurationCutView(Context context, AttributeSet attrs, int defStyle) { 61 | super(context, attrs, defStyle); 62 | 63 | init(context); 64 | } 65 | 66 | public void setRangeChangeListener(IOnRangeChangeListener listener) { 67 | mRangeChangeListener = listener; 68 | } 69 | 70 | private void init(Context context) { 71 | mContext = context; 72 | 73 | LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 74 | inflater.inflate(R.layout.item_edit_view, this, true); 75 | 76 | mTvTip = (TextView) findViewById(R.id.tv_tip); 77 | 78 | mRangeSlider = (RangeSlider) findViewById(R.id.range_slider); 79 | mRangeSlider.setRangeChangeListener(this); 80 | 81 | mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); 82 | LinearLayoutManager manager = new LinearLayoutManager(mContext); 83 | manager.setOrientation(LinearLayoutManager.HORIZONTAL); 84 | mRecyclerView.setLayoutManager(manager); 85 | 86 | mAdapter = new VDurationCutAdapter(mContext); 87 | mRecyclerView.setAdapter(mAdapter); 88 | } 89 | 90 | public int getSegmentFrom() { 91 | return (int) mVideoStartPos; 92 | } 93 | 94 | public int getSegmentTo() { 95 | return (int) mVideoEndPos; 96 | } 97 | 98 | public void setMediaFileInfo(LocalVideoBean videoInfo) { 99 | if (videoInfo == null) { 100 | return; 101 | } 102 | mVideoDuration = videoInfo.duration; 103 | 104 | mVideoStartPos = 0; 105 | mVideoEndPos = mVideoDuration; 106 | 107 | VideoCropHelper.getLocalVideoBitmap(videoInfo.src_path, VDurationCutView.THUMB_COUNT, 120, 120, new VideoCropHelper.OnBitmapListener() { 108 | @Override 109 | public void onBitmapGet(Bitmap bitmap) { 110 | addBitmap(mAdapter.getItemCount(), bitmap); 111 | } 112 | }); 113 | } 114 | 115 | 116 | 117 | public void addBitmap(int index, Bitmap bitmap) { 118 | mAdapter.add(index, bitmap); 119 | } 120 | 121 | @Override 122 | public void onKeyDown(int type) { 123 | if (mRangeChangeListener != null) { 124 | mRangeChangeListener.onKeyDown(); 125 | } 126 | } 127 | 128 | @Override 129 | protected void onDetachedFromWindow() { 130 | super.onDetachedFromWindow(); 131 | if (mAdapter != null) { 132 | Log.d(VideoFFCrop.TAG, "onDetachedFromWindow: 清除所有bitmap"); 133 | mAdapter.recycleAllBitmap(); 134 | } 135 | } 136 | 137 | @Override 138 | public void onKeyUp(int type, int leftPinIndex, int rightPinIndex) { 139 | int leftTime = (int) (mVideoDuration * leftPinIndex / 100); //ms 140 | int rightTime = (int) (mVideoDuration * rightPinIndex / 100); 141 | 142 | if (type == RangeSlider.TYPE_LEFT) { 143 | mVideoStartPos = leftTime; 144 | } else { 145 | mVideoEndPos = rightTime; 146 | } 147 | if (mRangeChangeListener != null) { 148 | mRangeChangeListener.onKeyUp((int) mVideoStartPos, (int) mVideoEndPos); 149 | } 150 | Log.d(VideoFFCrop.TAG, "onKeyUp: " + leftTime + "===" + rightTime); 151 | mTvTip.setText(String.format("左侧 : %s, 右侧 : %s ", CutUtils.duration(leftTime), CutUtils.duration(rightTime))); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/mill/cropcut/view/VHwCropView.java: -------------------------------------------------------------------------------- 1 | package com.mill.cropcut.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.view.LayoutInflater; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.widget.FrameLayout; 10 | 11 | import com.mill.cropcut.R; 12 | import com.mill.cropcut.utils.VideoCropHelper; 13 | 14 | /** 15 | * 视频播放器 & 尺寸裁切高亮框 16 | * Created by lulei-ms on 2017/8/23. 17 | */ 18 | public class VHwCropView extends FrameLayout implements LocalVideoView.OnVideoSizeChangeedListener, View.OnTouchListener { 19 | private LocalVideoView mVideoView; 20 | private OverlayView mOverlayView; 21 | 22 | private float lastX, lastY; 23 | 24 | public VHwCropView(Context context) { 25 | this(context, null); 26 | } 27 | 28 | public VHwCropView(Context context, AttributeSet attrs) { 29 | this(context, attrs, 0); 30 | } 31 | 32 | public VHwCropView(Context context, AttributeSet attrs, int defStyleAttr) { 33 | super(context, attrs, defStyleAttr); 34 | 35 | LayoutInflater.from(context).inflate(R.layout.video_view_crop, this, true); 36 | 37 | mOverlayView = (OverlayView) findViewById(R.id.overlay_view); 38 | mVideoView = (LocalVideoView) findViewById(R.id.video_view); 39 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_UCropView); 40 | mOverlayView.processStyledAttributes(a); 41 | a.recycle(); 42 | 43 | mVideoView.setOnVideoSizeChangeedListener(this); 44 | 45 | mOverlayView.setShowCropGrid(false); 46 | mOverlayView.setTargetAspectRatio(VideoCropHelper.WHA); 47 | mOverlayView.setOnTouchListener(this); 48 | } 49 | 50 | @Override 51 | public boolean onTouch(View v, MotionEvent event) { 52 | if(event.getPointerCount() != 1) { 53 | return true; 54 | } 55 | //高亮部分 可以左右移动 56 | if (event.getAction()== MotionEvent.ACTION_MOVE) { 57 | float dx = event.getX() - lastX; 58 | float newLeft = mOverlayView.getCropViewRect().left + dx; 59 | float newRight = mOverlayView.getCropViewRect().right + dx; 60 | if(newLeft >= mOverlayView.getLeft() && newRight <= mOverlayView.getRight()) { 61 | mOverlayView.getCropViewRect().offset(dx, 0); 62 | mOverlayView.invalidate(); 63 | } 64 | } 65 | lastX = event.getX(); 66 | lastY = event.getY(); 67 | return true; 68 | } 69 | 70 | @Override 71 | public void onVideoSizeChanged(int width, int height) { 72 | if (mOverlayView != null && mOverlayView.getLayoutParams() != null) { 73 | //重新设置高亮部分,宽高 74 | int ovW = (int) (height * VideoCropHelper.WHA); 75 | int ovH = height; 76 | mOverlayView.setCropViewRect((width - ovW) / 2, (getHeight() - ovH) / 2, (width + ovW) / 2, (getHeight() + ovH) / 2); 77 | } 78 | } 79 | 80 | public void setStarEndPo(int start, int end) { 81 | mVideoView.setStarEndPo(start, end); 82 | } 83 | 84 | @Override 85 | protected void onDetachedFromWindow() { 86 | mVideoView.stopPlayback(); 87 | super.onDetachedFromWindow(); 88 | } 89 | 90 | public LocalVideoView getVideoView() { 91 | return mVideoView; 92 | } 93 | 94 | public OverlayView getOverlayView() { 95 | return mOverlayView; 96 | } 97 | 98 | 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/ffmpeg.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pangu-Immortal/VideoCropping/c6588f0ee71557eef73545f0cf168ae7f4e07ec1/app/src/main/jniLibs/arm64-v8a/ffmpeg.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/ffmpeg.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pangu-Immortal/VideoCropping/c6588f0ee71557eef73545f0cf168ae7f4e07ec1/app/src/main/jniLibs/armeabi-v7a/ffmpeg.so -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_progress_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pangu-Immortal/VideoCropping/c6588f0ee71557eef73545f0cf168ae7f4e07ec1/app/src/main/res/drawable/ic_progress_left.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_progress_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pangu-Immortal/VideoCropping/c6588f0ee71557eef73545f0cf168ae7f4e07ec1/app/src/main/res/drawable/ic_progress_right.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_edit_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 20 | 21 | 27 | 28 | 29 | 36 | 37 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 37 | 38 | 43 | 44 | 45 | 51 | 52 | 53 | 58 | 59 | 60 | 66 | 67 | 68 | 73 | 74 | 75 | 81 | 82 | 83 |