├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── dictionaries │ └── guxiuzhong.xml ├── encodings.xml ├── gradle.xml ├── kotlinc.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── gxz │ │ └── example │ │ └── videoedit │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── gxz │ │ │ └── example │ │ │ └── videoedit │ │ │ ├── EditSpacingItemDecoration.java │ │ │ ├── ExtractFrameWorkThread.java │ │ │ ├── ExtractVideoInfoUtil.java │ │ │ ├── MainActivity.java │ │ │ ├── PictureUtils.java │ │ │ ├── RangeSeekBar.java │ │ │ ├── RangeSeekBar2.java │ │ │ ├── UIUtil.java │ │ │ ├── VideoEditActivity.java │ │ │ ├── VideoEditAdapter.java │ │ │ ├── VideoEditInfo.java │ │ │ └── VideoExtractFrameAsyncUtils.java │ └── res │ │ ├── drawable-hdpi │ │ ├── handle_left.png │ │ ├── lf_ugc_publish_pos.xml │ │ ├── upload_overlay_black.9.png │ │ └── upload_overlay_trans.9.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_video_edit.xml │ │ └── video_item.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── gxz │ └── example │ └── videoedit │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pic1.png ├── pic2.png ├── settings.gradle └── sr-new.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/dictionaries/guxiuzhong.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 34 | 35 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Abstraction issuesJava 46 | 47 | 48 | Android 49 | 50 | 51 | Android > Lint > Accessibility 52 | 53 | 54 | Android > Lint > Correctness 55 | 56 | 57 | Android > Lint > Correctness > Messages 58 | 59 | 60 | Android > Lint > Internationalization 61 | 62 | 63 | Android > Lint > Internationalization > Bidirectional Text 64 | 65 | 66 | Android > Lint > Performance 67 | 68 | 69 | Android > Lint > Security 70 | 71 | 72 | Android > Lint > Usability 73 | 74 | 75 | Android > Lint > Usability > Icons 76 | 77 | 78 | Android > Lint > Usability > Typography 79 | 80 | 81 | Android Lint for Kotlin 82 | 83 | 84 | Assignment issuesJava 85 | 86 | 87 | BashSupport 88 | 89 | 90 | Bitwise operation issuesJava 91 | 92 | 93 | C/C++ 94 | 95 | 96 | Class metricsJava 97 | 98 | 99 | Class structureJava 100 | 101 | 102 | Cloning issuesJava 103 | 104 | 105 | Code maturity issuesJava 106 | 107 | 108 | Code style issuesJava 109 | 110 | 111 | Compiler issuesJava 112 | 113 | 114 | Concurrency annotation issuesJava 115 | 116 | 117 | Control FlowGroovy 118 | 119 | 120 | Control flow issuesJava 121 | 122 | 123 | Data flow issuesJava 124 | 125 | 126 | Dependency issuesJava 127 | 128 | 129 | Encapsulation issuesJava 130 | 131 | 132 | Error handlingJava 133 | 134 | 135 | Finalization issuesJava 136 | 137 | 138 | General 139 | 140 | 141 | GeneralC/C++ 142 | 143 | 144 | GeneralJava 145 | 146 | 147 | Google Cloud Endpoints 148 | 149 | 150 | Gradle 151 | 152 | 153 | Groovy 154 | 155 | 156 | HTML 157 | 158 | 159 | ImportsJava 160 | 161 | 162 | Inheritance issuesJava 163 | 164 | 165 | Initialization issuesJava 166 | 167 | 168 | Internationalization issues 169 | 170 | 171 | Internationalization issuesJava 172 | 173 | 174 | J2ME issuesJava 175 | 176 | 177 | JUnit issuesJava 178 | 179 | 180 | Java 181 | 182 | 183 | Java language level issuesJava 184 | 185 | 186 | Java language level migration aidsJava 187 | 188 | 189 | JavaBeans issuesJava 190 | 191 | 192 | Javadoc issuesJava 193 | 194 | 195 | Kotlin 196 | 197 | 198 | Logging issuesJava 199 | 200 | 201 | Manifest 202 | 203 | 204 | Memory issuesJava 205 | 206 | 207 | Method MetricsGroovy 208 | 209 | 210 | Method metricsJava 211 | 212 | 213 | Modularization issuesJava 214 | 215 | 216 | Naming ConventionsGroovy 217 | 218 | 219 | Naming conventionsJava 220 | 221 | 222 | Numeric issuesJava 223 | 224 | 225 | Packaging issuesJava 226 | 227 | 228 | Pattern Validation 229 | 230 | 231 | Performance issuesJava 232 | 233 | 234 | Portability issuesJava 235 | 236 | 237 | Potentially confusing code constructsGroovy 238 | 239 | 240 | Probable bugsGradle 241 | 242 | 243 | Probable bugsGroovy 244 | 245 | 246 | Probable bugsJava 247 | 248 | 249 | Properties Files 250 | 251 | 252 | Properties FilesJava 253 | 254 | 255 | Python 256 | 257 | 258 | Resource management issuesJava 259 | 260 | 261 | Security issuesJava 262 | 263 | 264 | Serialization issuesJava 265 | 266 | 267 | StyleGroovy 268 | 269 | 270 | TestNGJava 271 | 272 | 273 | Threading issuesGroovy 274 | 275 | 276 | Threading issuesJava 277 | 278 | 279 | Visibility issuesJava 280 | 281 | 282 | XML 283 | 284 | 285 | toString() issuesJava 286 | 287 | 288 | 289 | 290 | Android 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 312 | 313 | $USER_HOME$/.subversion 314 | 315 | 316 | 317 | 318 | 319 | 320 | 325 | 326 | 327 | 328 | 329 | 330 | 1.8 331 | 332 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 350 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 高仿微信视频编辑页 2 | android视频编辑页面-高仿微信视频编辑页 3 | 4 | # 效果图 5 | 6 | 7 | 8 | 9 | # 介绍 10 | http://blog.csdn.net/ta893115871/article/details/70188162 11 | 12 | http://www.jianshu.com/p/55dcb62ca0b3 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.1" 6 | defaultConfig { 7 | applicationId "com.gxz.example.videoedit" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.3.1' 28 | compile 'com.android.support:design:25.3.1' 29 | 30 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9' 31 | testCompile 'junit:junit:4.12' 32 | compile 'com.github.bumptech.glide:glide:3.7.0' 33 | 34 | } 35 | -------------------------------------------------------------------------------- /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 /Users/guxiuzhong/Library/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/androidTest/java/com/gxz/example/videoedit/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.gxz.example.videoedit", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/EditSpacingItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | /** 8 | * ================================================ 9 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 10 | * 版 本: 11 | * 创建日期:2017/2/18-上午1:03 12 | * 描 述: 13 | * 修订历史: 14 | * ================================================ 15 | */ 16 | public class EditSpacingItemDecoration extends RecyclerView.ItemDecoration { 17 | 18 | private int space; 19 | private int thumbnailsCount; 20 | 21 | public EditSpacingItemDecoration(int space, int thumbnailsCount) { 22 | this.space = space; 23 | this.thumbnailsCount = thumbnailsCount; 24 | } 25 | 26 | @Override 27 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 28 | // 第一个的前面和最后一个的后面 29 | int position = parent.getChildAdapterPosition(view); 30 | if (position == 0) { 31 | outRect.left = space; 32 | outRect.right = 0; 33 | } else if (thumbnailsCount > 10 && position == thumbnailsCount - 1) { 34 | outRect.left = 0; 35 | outRect.right = space; 36 | } else { 37 | outRect.left = 0; 38 | outRect.right = 0; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/ExtractFrameWorkThread.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.os.Handler; 4 | 5 | /** 6 | * ================================================ 7 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 8 | * 版 本: 9 | * 创建日期:2017/3/2-下午7:53 10 | * 描 述: 11 | * 修订历史: 12 | * ================================================ 13 | */ 14 | 15 | public class ExtractFrameWorkThread extends Thread { 16 | public static final int MSG_SAVE_SUCCESS = 0; 17 | private String videoPath; 18 | private String OutPutFileDirPath; 19 | private long startPosition; 20 | private long endPosition; 21 | private int thumbnailsCount; 22 | private VideoExtractFrameAsyncUtils mVideoExtractFrameAsyncUtils; 23 | 24 | public ExtractFrameWorkThread(int extractW, int extractH, Handler mHandler, String videoPath, String OutPutFileDirPath, 25 | long startPosition, long endPosition, int thumbnailsCount) { 26 | this.videoPath = videoPath; 27 | this.OutPutFileDirPath = OutPutFileDirPath; 28 | this.startPosition = startPosition; 29 | this.endPosition = endPosition; 30 | this.thumbnailsCount = thumbnailsCount; 31 | this.mVideoExtractFrameAsyncUtils = new VideoExtractFrameAsyncUtils(extractW,extractH,mHandler); 32 | } 33 | 34 | @Override 35 | public void run() { 36 | super.run(); 37 | mVideoExtractFrameAsyncUtils.getVideoThumbnailsInfoForEdit( 38 | videoPath, 39 | OutPutFileDirPath, 40 | startPosition, 41 | endPosition, 42 | thumbnailsCount); 43 | } 44 | 45 | public void stopExtract() { 46 | if (mVideoExtractFrameAsyncUtils != null) { 47 | mVideoExtractFrameAsyncUtils.stopExtract(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/ExtractVideoInfoUtil.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.graphics.Bitmap; 4 | import android.media.MediaMetadataRetriever; 5 | import android.os.Build; 6 | import android.text.TextUtils; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * ================================================ 12 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 13 | * 版 本: 14 | * 创建日期:2017/2/23-上午11:05 15 | * 描 述: 16 | * 修订历史: 17 | * ================================================ 18 | */ 19 | 20 | public class ExtractVideoInfoUtil { 21 | private MediaMetadataRetriever mMetadataRetriever; 22 | private long fileLength = 0;//毫秒 23 | 24 | public ExtractVideoInfoUtil(String path) { 25 | if (TextUtils.isEmpty(path)) { 26 | throw new RuntimeException("path must be not null !"); 27 | } 28 | File file = new File(path); 29 | if (!file.exists()) { 30 | throw new RuntimeException("path file not exists !"); 31 | } 32 | mMetadataRetriever = new MediaMetadataRetriever(); 33 | mMetadataRetriever.setDataSource(file.getAbsolutePath()); 34 | String len = getVideoLength(); 35 | fileLength = TextUtils.isEmpty(len) ? 0 : Long.valueOf(len); 36 | 37 | } 38 | 39 | public int getVideoWidth() { 40 | String w = mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); 41 | int width = -1; 42 | if (!TextUtils.isEmpty(w)) { 43 | width = Integer.valueOf(w); 44 | } 45 | return width; 46 | } 47 | 48 | public int getVideoHeight() { 49 | String h = mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); 50 | int height = -1; 51 | if (!TextUtils.isEmpty(h)) { 52 | height = Integer.valueOf(h); 53 | } 54 | return height; 55 | } 56 | 57 | /** 58 | * 获取视频的典型的一帧图片,不耗时 59 | * 60 | * @return Bitmap 61 | */ 62 | public Bitmap extractFrame() { 63 | return mMetadataRetriever.getFrameAtTime(); 64 | } 65 | 66 | /** 67 | * 获取视频某一帧,不一定是关键帧 68 | * 69 | * @param timeMs 毫秒 70 | */ 71 | public Bitmap extractFrame(long timeMs) { 72 | //第一个参数是传入时间,只能是us(微秒) 73 | //OPTION_CLOSEST ,在给定的时间,检索最近一个帧,这个帧不一定是关键帧。 74 | //OPTION_CLOSEST_SYNC 在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧) 75 | //OPTION_NEXT_SYNC 在给定时间之后检索一个同步与数据源相关联的关键帧。 76 | //OPTION_PREVIOUS_SYNC 顾名思义,同上 77 | // Bitmap bitmap = mMetadataRetriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST); 78 | Bitmap bitmap = null; 79 | for (long i = timeMs; i < fileLength; i += 1000) { 80 | bitmap = mMetadataRetriever.getFrameAtTime(i * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); 81 | if (bitmap != null) { 82 | break; 83 | } 84 | } 85 | return bitmap; 86 | } 87 | 88 | 89 | 90 | /*** 91 | * 获取视频的长度时间 92 | * 93 | * @return String 毫秒 94 | */ 95 | public String getVideoLength() { 96 | return mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 97 | } 98 | 99 | /** 100 | * 获取视频旋转角度 101 | * 102 | * @return 103 | */ 104 | public int getVideoDegree() { 105 | int degree = 0; 106 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 107 | String degreeStr = mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); 108 | if (!TextUtils.isEmpty(degreeStr)) { 109 | degree = Integer.valueOf(degreeStr); 110 | } 111 | } 112 | return degree; 113 | } 114 | 115 | public void release() { 116 | if (mMetadataRetriever != null) { 117 | mMetadataRetriever.release(); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | public class MainActivity extends AppCompatActivity { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_main); 14 | } 15 | public void videoEdit(View view){ 16 | startActivity(new Intent(this,VideoEditActivity.class)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/PictureUtils.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.os.Environment; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | 11 | /** 12 | * ================================================ 13 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 14 | * 版 本: 15 | * 创建日期:2017/4/4-下午6:56 16 | * 描 述: 17 | * 修订历史: 18 | * ================================================ 19 | */ 20 | 21 | public class PictureUtils { 22 | public static final String POSTFIX = ".jpeg"; 23 | private static final String EDIT_PATH = "/EditVideo/"; 24 | 25 | public static String saveImageToSD(Bitmap bmp, String dirPath) { 26 | if (bmp == null) { 27 | return ""; 28 | } 29 | File appDir = new File(dirPath); 30 | if (!appDir.exists()) { 31 | appDir.mkdir(); 32 | } 33 | String fileName = System.currentTimeMillis() + ".jpg"; 34 | File file = new File(appDir, fileName); 35 | try { 36 | FileOutputStream fos = new FileOutputStream(file); 37 | bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); 38 | fos.flush(); 39 | fos.close(); 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | } 43 | return file.getAbsolutePath(); 44 | } 45 | 46 | 47 | 48 | public static String saveImageToSDForEdit(Bitmap bmp, String dirPath, String fileName) { 49 | if (bmp == null) { 50 | return ""; 51 | } 52 | File appDir = new File(dirPath); 53 | if (!appDir.exists()) { 54 | appDir.mkdir(); 55 | } 56 | File file = new File(appDir, fileName); 57 | try { 58 | FileOutputStream fos = new FileOutputStream(file); 59 | bmp.compress(Bitmap.CompressFormat.JPEG, 80, fos); 60 | fos.flush(); 61 | fos.close(); 62 | } catch (IOException e) { 63 | e.printStackTrace(); 64 | } 65 | return file.getAbsolutePath(); 66 | } 67 | 68 | public static void deleteFile(File f) { 69 | if (f.isDirectory()) { 70 | File[] files = f.listFiles(); 71 | if (files != null && files.length > 0) { 72 | for (int i = 0; i < files.length; ++i) { 73 | deleteFile(files[i]); 74 | } 75 | } 76 | } 77 | f.delete(); 78 | } 79 | 80 | public static String getSaveEditThumbnailDir(Context context) { 81 | String state = Environment.getExternalStorageState(); 82 | File rootDir = state.equals(Environment.MEDIA_MOUNTED) ? Environment.getExternalStorageDirectory() : context.getCacheDir(); 83 | File folderDir = new File(rootDir.getAbsolutePath() + EDIT_PATH); 84 | if (!folderDir.exists() && folderDir.mkdirs()) { 85 | 86 | } 87 | return folderDir.getAbsolutePath(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/RangeSeekBar.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.os.Bundle; 11 | import android.os.Parcelable; 12 | import android.support.annotation.Nullable; 13 | import android.util.AttributeSet; 14 | import android.util.Log; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewConfiguration; 18 | 19 | import java.text.DecimalFormat; 20 | 21 | 22 | /** 23 | * ================================================ 24 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 25 | * 版 本: 26 | * 创建日期:2017/4/4-下午1:22 27 | * 描 述: 28 | * 修订历史: 29 | * ================================================ 30 | */ 31 | 32 | public class RangeSeekBar extends View { 33 | private static final String TAG = RangeSeekBar.class.getSimpleName(); 34 | private double absoluteMinValuePrim, absoluteMaxValuePrim; 35 | private double normalizedMinValue = 0d;//点坐标占总长度的比例值,范围从0-1 36 | private double normalizedMaxValue = 1d;//点坐标占总长度的比例值,范围从0-1 37 | private long min_cut_time = 3000; 38 | private double normalizedMinValueTime = 0d; 39 | private double normalizedMaxValueTime = 1d;// normalized:规格化的--点坐标占总长度的比例值,范围从0-1 40 | private int mScaledTouchSlop; 41 | private Bitmap thumbImageLeft; 42 | private Bitmap thumbImageRight; 43 | private Bitmap thumbPressedImage; 44 | private Bitmap mBitmapBlack; 45 | private Bitmap mBitmapPro; 46 | private Paint paint; 47 | private Paint rectPaint; 48 | private int thumbWidth; 49 | private float thumbHalfWidth; 50 | private final float padding = 0; 51 | 52 | private float thumbPaddingTop = 0; 53 | private float thumbPressPaddingTop = 0; 54 | private boolean isTouchDown; 55 | public static final int INVALID_POINTER_ID = 255; 56 | public static final int ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8; 57 | private int mActivePointerId = INVALID_POINTER_ID; 58 | private float mDownMotionX; 59 | private boolean mIsDragging; 60 | private Thumb pressedThumb; 61 | private boolean isMin; 62 | private double min_width = 1;//最小裁剪距离 63 | private boolean notifyWhileDragging = false; 64 | 65 | public enum Thumb { 66 | MIN, MAX 67 | } 68 | 69 | public RangeSeekBar(Context context) { 70 | super(context); 71 | } 72 | 73 | public RangeSeekBar(Context context, @Nullable AttributeSet attrs) { 74 | super(context, attrs); 75 | } 76 | 77 | public RangeSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 78 | super(context, attrs, defStyleAttr); 79 | } 80 | 81 | public RangeSeekBar(Context context, long absoluteMinValuePrim, long absoluteMaxValuePrim) { 82 | super(context); 83 | this.absoluteMinValuePrim = absoluteMinValuePrim; 84 | this.absoluteMaxValuePrim = absoluteMaxValuePrim; 85 | setFocusable(true); 86 | setFocusableInTouchMode(true); 87 | init(); 88 | } 89 | 90 | private void init() { 91 | mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 92 | //等比例缩放图片 93 | thumbImageLeft = BitmapFactory.decodeResource(getResources(), R.drawable.handle_left); 94 | int width = thumbImageLeft.getWidth(); 95 | int height = thumbImageLeft.getHeight(); 96 | int newWidth = dip2px(11); 97 | int newHeight = dip2px(55); 98 | float scaleWidth = newWidth * 1.0f / width; 99 | float scaleHeight = newHeight * 1.0f / height; 100 | Matrix matrix = new Matrix(); 101 | matrix.postScale(scaleWidth, scaleHeight); 102 | thumbImageLeft = Bitmap.createBitmap(thumbImageLeft, 0, 0, width, height, matrix, true); 103 | thumbImageRight = thumbImageLeft; 104 | thumbPressedImage = thumbImageLeft; 105 | thumbWidth = newWidth; 106 | thumbHalfWidth = thumbWidth / 2; 107 | 108 | 109 | mBitmapBlack = BitmapFactory.decodeResource(getResources(), R.drawable.upload_overlay_black); 110 | mBitmapPro = BitmapFactory.decodeResource(getResources(), R.drawable.upload_overlay_trans); 111 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 112 | rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 113 | rectPaint.setStyle(Paint.Style.FILL); 114 | rectPaint.setColor(Color.parseColor("#ffffff")); 115 | } 116 | 117 | @Override 118 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 119 | int width = 300; 120 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) { 121 | width = MeasureSpec.getSize(widthMeasureSpec); 122 | } 123 | int height = 120; 124 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) { 125 | height = MeasureSpec.getSize(heightMeasureSpec); 126 | } 127 | setMeasuredDimension(width, height); 128 | } 129 | 130 | @Override 131 | protected void onDraw(Canvas canvas) { 132 | super.onDraw(canvas); 133 | float bg_middle_left = 0; 134 | float bg_middle_right = getWidth() - getPaddingRight(); 135 | float scale = (bg_middle_right - bg_middle_left) / mBitmapPro.getWidth(); 136 | 137 | float rangeL = normalizedToScreen(normalizedMinValue); 138 | float rangeR = normalizedToScreen(normalizedMaxValue); 139 | float scale_pro = (rangeR - rangeL) / mBitmapPro.getWidth(); 140 | if (scale_pro > 0) { 141 | try { 142 | Matrix pro_mx = new Matrix(); 143 | pro_mx.postScale(scale_pro, 1f); 144 | Bitmap m_bitmap_pro_new = Bitmap.createBitmap(mBitmapPro, 0, 0, mBitmapPro.getWidth(), 145 | mBitmapPro.getHeight(), pro_mx, true); 146 | 147 | //画中间的透明遮罩 148 | canvas.drawBitmap(m_bitmap_pro_new, rangeL, thumbPaddingTop, paint); 149 | 150 | Matrix mx = new Matrix(); 151 | mx.postScale(scale, 1f); 152 | Bitmap m_bitmap_black_new = Bitmap.createBitmap(mBitmapBlack, 0, 0, mBitmapBlack.getWidth(), mBitmapBlack.getHeight(), mx, true); 153 | 154 | //画左边的半透明遮罩 155 | Bitmap m_bg_new1 = Bitmap.createBitmap(m_bitmap_black_new, 0, 0, (int) (rangeL - bg_middle_left) + (int) thumbWidth / 2, mBitmapBlack.getHeight()); 156 | canvas.drawBitmap(m_bg_new1, bg_middle_left, thumbPaddingTop, paint); 157 | 158 | //画右边的半透明遮罩 159 | Bitmap m_bg_new2 = Bitmap.createBitmap(m_bitmap_black_new, (int) (rangeR - thumbWidth / 2), 0, (int) (getWidth() - rangeR) + (int) thumbWidth / 2, mBitmapBlack.getHeight()); 160 | canvas.drawBitmap(m_bg_new2, (int) (rangeR - thumbWidth / 2), thumbPaddingTop, paint); 161 | 162 | //画上下的矩形 163 | canvas.drawRect(rangeL, thumbPaddingTop, rangeR, thumbPaddingTop + dip2px(2), rectPaint); 164 | canvas.drawRect(rangeL, getHeight() - dip2px(2), rangeR, getHeight(), rectPaint); 165 | //画左右thumb 166 | drawThumb(normalizedToScreen(normalizedMinValue), false, canvas, true); 167 | drawThumb(normalizedToScreen(normalizedMaxValue), false, canvas, false); 168 | } catch (Exception e) { 169 | // 当pro_scale非常小,例如width=12,Height=48,pro_scale=0.01979065时, 170 | // 宽高按比例计算后值为0.237、0.949,系统强转为int型后宽就变成0了。就出现非法参数异常 171 | Log.e(TAG, 172 | "IllegalArgumentException--width=" + mBitmapPro.getWidth() + "Height=" + mBitmapPro.getHeight() 173 | + "scale_pro=" + scale_pro, e); 174 | } 175 | } 176 | } 177 | 178 | 179 | private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean isLeft) { 180 | canvas.drawBitmap(pressed ? thumbPressedImage : (isLeft ? thumbImageLeft : thumbImageRight), screenCoord - (isLeft ? 0 : thumbWidth), (pressed ? thumbPressPaddingTop : thumbPaddingTop), paint); 181 | } 182 | 183 | 184 | @Override 185 | public boolean onTouchEvent(MotionEvent event) { 186 | if (isTouchDown) { 187 | return super.onTouchEvent(event); 188 | } 189 | if (event.getPointerCount() > 1) { 190 | return super.onTouchEvent(event); 191 | } 192 | 193 | if (!isEnabled()) 194 | return false; 195 | if (absoluteMaxValuePrim <= min_cut_time) { 196 | return super.onTouchEvent(event); 197 | } 198 | int pointerIndex;// 记录点击点的index 199 | final int action = event.getAction(); 200 | switch (action & MotionEvent.ACTION_MASK) { 201 | case MotionEvent.ACTION_DOWN: 202 | //记住最后一个手指点击屏幕的点的坐标x,mDownMotionX 203 | mActivePointerId = event.getPointerId(event.getPointerCount() - 1); 204 | pointerIndex = event.findPointerIndex(mActivePointerId); 205 | mDownMotionX = event.getX(pointerIndex); 206 | // 判断touch到的是最大值thumb还是最小值thumb 207 | pressedThumb = evalPressedThumb(mDownMotionX); 208 | if (pressedThumb == null) 209 | return super.onTouchEvent(event); 210 | setPressed(true);// 设置该控件被按下了 211 | onStartTrackingTouch();// 置mIsDragging为true,开始追踪touch事件 212 | trackTouchEvent(event); 213 | attemptClaimDrag(); 214 | if (listener != null) { 215 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_DOWN, isMin, pressedThumb); 216 | } 217 | break; 218 | case MotionEvent.ACTION_MOVE: 219 | if (pressedThumb != null) { 220 | if (mIsDragging) { 221 | trackTouchEvent(event); 222 | } else { 223 | // Scroll to follow the motion event 224 | pointerIndex = event.findPointerIndex(mActivePointerId); 225 | final float x = event.getX(pointerIndex);// 手指在控件上点的X坐标 226 | // 手指没有点在最大最小值上,并且在控件上有滑动事件 227 | if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) { 228 | setPressed(true); 229 | Log.e(TAG, "没有拖住最大最小值");// 一直不会执行? 230 | invalidate(); 231 | onStartTrackingTouch(); 232 | trackTouchEvent(event); 233 | attemptClaimDrag(); 234 | } 235 | } 236 | if (notifyWhileDragging && listener != null) { 237 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_MOVE, isMin, pressedThumb); 238 | } 239 | } 240 | break; 241 | case MotionEvent.ACTION_UP: 242 | if (mIsDragging) { 243 | trackTouchEvent(event); 244 | onStopTrackingTouch(); 245 | setPressed(false); 246 | } else { 247 | onStartTrackingTouch(); 248 | trackTouchEvent(event); 249 | onStopTrackingTouch(); 250 | } 251 | 252 | invalidate(); 253 | if (listener != null) { 254 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_UP, isMin, pressedThumb); 255 | } 256 | pressedThumb = null;// 手指抬起,则置被touch到的thumb为空 257 | break; 258 | case MotionEvent.ACTION_POINTER_DOWN: 259 | final int index = event.getPointerCount() - 1; 260 | // final int index = ev.getActionIndex(); 261 | mDownMotionX = event.getX(index); 262 | mActivePointerId = event.getPointerId(index); 263 | invalidate(); 264 | break; 265 | case MotionEvent.ACTION_POINTER_UP: 266 | onSecondaryPointerUp(event); 267 | invalidate(); 268 | break; 269 | case MotionEvent.ACTION_CANCEL: 270 | if (mIsDragging) { 271 | onStopTrackingTouch(); 272 | setPressed(false); 273 | } 274 | invalidate(); // see above explanation 275 | break; 276 | default: 277 | break; 278 | } 279 | return true; 280 | } 281 | 282 | private void onSecondaryPointerUp(MotionEvent ev) { 283 | final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; 284 | 285 | final int pointerId = ev.getPointerId(pointerIndex); 286 | if (pointerId == mActivePointerId) { 287 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 288 | mDownMotionX = ev.getX(newPointerIndex); 289 | mActivePointerId = ev.getPointerId(newPointerIndex); 290 | } 291 | } 292 | 293 | private void trackTouchEvent(MotionEvent event) { 294 | if (event.getPointerCount() > 1) return; 295 | Log.e(TAG, "trackTouchEvent: " + event.getAction() + " x: " + event.getX()); 296 | final int pointerIndex = event.findPointerIndex(mActivePointerId);// 得到按下点的index 297 | float x = 0; 298 | try { 299 | x = event.getX(pointerIndex); 300 | } catch (Exception e) { 301 | return; 302 | } 303 | if (Thumb.MIN.equals(pressedThumb)) { 304 | // screenToNormalized(x)-->得到规格化的0-1的值 305 | setNormalizedMinValue(screenToNormalized(x, 0)); 306 | } else if (Thumb.MAX.equals(pressedThumb)) { 307 | setNormalizedMaxValue(screenToNormalized(x, 1)); 308 | } 309 | } 310 | 311 | private double screenToNormalized(float screenCoord, int position) { 312 | int width = getWidth(); 313 | if (width <= 2 * padding) { 314 | // prevent division by zero, simply return 0. 315 | return 0d; 316 | } else { 317 | isMin = false; 318 | double current_width = screenCoord; 319 | float rangeL = normalizedToScreen(normalizedMinValue); 320 | float rangeR = normalizedToScreen(normalizedMaxValue); 321 | double min = min_cut_time / (absoluteMaxValuePrim - absoluteMinValuePrim) * (width - thumbWidth * 2); 322 | 323 | if (absoluteMaxValuePrim > 5 * 60 * 1000) {//大于5分钟的精确小数四位 324 | DecimalFormat df = new DecimalFormat("0.0000"); 325 | min_width = Double.parseDouble(df.format(min)); 326 | } else { 327 | min_width = Math.round(min + 0.5d); 328 | } 329 | if (position == 0) { 330 | if (isInThumbRangeLeft(screenCoord, normalizedMinValue, 0.5)) { 331 | return normalizedMinValue; 332 | } 333 | 334 | float rightPosition = (getWidth() - rangeR) >= 0 ? (getWidth() - rangeR) : 0; 335 | double left_length = getValueLength() - (rightPosition + min_width); 336 | 337 | 338 | if (current_width > rangeL) { 339 | current_width = rangeL + (current_width - rangeL); 340 | } else if (current_width <= rangeL) { 341 | current_width = rangeL - (rangeL - current_width); 342 | } 343 | 344 | if (current_width > left_length) { 345 | isMin = true; 346 | current_width = left_length; 347 | } 348 | 349 | if (current_width < thumbWidth * 2 / 3) { 350 | current_width = 0; 351 | } 352 | 353 | double resultTime = (current_width - padding) / (width - 2 * thumbWidth); 354 | normalizedMinValueTime = Math.min(1d, Math.max(0d, resultTime)); 355 | double result = (current_width - padding) / (width - 2 * padding); 356 | return Math.min(1d, Math.max(0d, result));// 保证该该值为0-1之间,但是什么时候这个判断有用呢? 357 | } else { 358 | if (isInThumbRange(screenCoord, normalizedMaxValue, 0.5)) { 359 | return normalizedMaxValue; 360 | } 361 | 362 | double right_length = getValueLength() - (rangeL + min_width); 363 | if (current_width > rangeR) { 364 | current_width = rangeR + (current_width - rangeR); 365 | } else if (current_width <= rangeR) { 366 | current_width = rangeR - (rangeR - current_width); 367 | } 368 | 369 | double paddingRight = getWidth() - current_width; 370 | 371 | if (paddingRight > right_length) { 372 | isMin = true; 373 | current_width = getWidth() - right_length; 374 | paddingRight = right_length; 375 | } 376 | 377 | if (paddingRight < thumbWidth * 2 / 3) { 378 | current_width = getWidth(); 379 | paddingRight = 0; 380 | } 381 | 382 | double resultTime = (paddingRight - padding) / (width - 2 * thumbWidth); 383 | resultTime = 1 - resultTime; 384 | normalizedMaxValueTime = Math.min(1d, Math.max(0d, resultTime)); 385 | double result = (current_width - padding) / (width - 2 * padding); 386 | return Math.min(1d, Math.max(0d, result));// 保证该该值为0-1之间,但是什么时候这个判断有用呢? 387 | } 388 | 389 | } 390 | } 391 | 392 | private int getValueLength() { 393 | return (getWidth() - 2 * thumbWidth); 394 | } 395 | 396 | /** 397 | * 计算位于哪个Thumb内 398 | * 399 | * @param touchX touchX 400 | * @return 被touch的是空还是最大值或最小值 401 | */ 402 | private Thumb evalPressedThumb(float touchX) { 403 | Thumb result = null; 404 | boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue, 2);// 触摸点是否在最小值图片范围内 405 | boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue, 2); 406 | if (minThumbPressed && maxThumbPressed) { 407 | // 如果两个thumbs重叠在一起,无法判断拖动哪个,做以下处理 408 | // 触摸点在屏幕右侧,则判断为touch到了最小值thumb,反之判断为touch到了最大值thumb 409 | result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX; 410 | } else if (minThumbPressed) { 411 | result = Thumb.MIN; 412 | } else if (maxThumbPressed) { 413 | result = Thumb.MAX; 414 | } 415 | return result; 416 | } 417 | 418 | private boolean isInThumbRange(float touchX, double normalizedThumbValue, double scale) { 419 | // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般 420 | // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。 421 | return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth * scale; 422 | } 423 | 424 | private boolean isInThumbRangeLeft(float touchX, double normalizedThumbValue, double scale) { 425 | // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般 426 | // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。 427 | return Math.abs(touchX - normalizedToScreen(normalizedThumbValue) - thumbWidth) <= thumbHalfWidth * scale; 428 | } 429 | 430 | /** 431 | * 试图告诉父view不要拦截子控件的drag 432 | */ 433 | private void attemptClaimDrag() { 434 | if (getParent() != null) { 435 | getParent().requestDisallowInterceptTouchEvent(true); 436 | } 437 | } 438 | 439 | void onStartTrackingTouch() { 440 | mIsDragging = true; 441 | } 442 | 443 | 444 | void onStopTrackingTouch() { 445 | mIsDragging = false; 446 | } 447 | 448 | public void setMin_cut_time(long min_cut_time) { 449 | this.min_cut_time = min_cut_time; 450 | } 451 | 452 | 453 | private float normalizedToScreen(double normalizedCoord) { 454 | return (float) (getPaddingLeft() + normalizedCoord * (getWidth() - getPaddingLeft() - getPaddingRight())); 455 | } 456 | 457 | private double valueToNormalized(long value) { 458 | if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) { 459 | return 0d; 460 | } 461 | return (value - absoluteMinValuePrim) / (absoluteMaxValuePrim - absoluteMinValuePrim); 462 | } 463 | 464 | public void setSelectedMinValue(long value) { 465 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 466 | setNormalizedMinValue(0d); 467 | } else { 468 | setNormalizedMinValue(valueToNormalized(value)); 469 | } 470 | } 471 | 472 | public void setSelectedMaxValue(long value) { 473 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 474 | setNormalizedMaxValue(1d); 475 | } else { 476 | setNormalizedMaxValue(valueToNormalized(value)); 477 | } 478 | } 479 | 480 | public void setNormalizedMinValue(double value) { 481 | normalizedMinValue = Math.max(0d, Math.min(1d, Math.min(value, normalizedMaxValue))); 482 | invalidate();// 重新绘制此view 483 | } 484 | 485 | 486 | public void setNormalizedMaxValue(double value) { 487 | normalizedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, normalizedMinValue))); 488 | invalidate();// 重新绘制此view 489 | } 490 | 491 | 492 | public long getSelectedMinValue() { 493 | return normalizedToValue(normalizedMinValueTime); 494 | } 495 | 496 | public long getSelectedMaxValue() { 497 | return normalizedToValue(normalizedMaxValueTime); 498 | } 499 | 500 | private long normalizedToValue(double normalized) { 501 | return (long) (absoluteMinValuePrim + normalized 502 | * (absoluteMaxValuePrim - absoluteMinValuePrim)); 503 | } 504 | 505 | /** 506 | * 供外部activity调用,控制是都在拖动的时候打印log信息,默认是false不打印 507 | */ 508 | public boolean isNotifyWhileDragging() { 509 | return notifyWhileDragging; 510 | } 511 | 512 | 513 | public void setNotifyWhileDragging(boolean flag) { 514 | this.notifyWhileDragging = flag; 515 | } 516 | 517 | public int dip2px(int dip) { 518 | float scale = getContext().getResources().getDisplayMetrics().density; 519 | return (int) ((float) dip * scale + 0.5F); 520 | } 521 | 522 | public void setTouchDown(boolean touchDown) { 523 | isTouchDown = touchDown; 524 | } 525 | 526 | @Override 527 | protected Parcelable onSaveInstanceState() { 528 | final Bundle bundle = new Bundle(); 529 | bundle.putParcelable("SUPER", super.onSaveInstanceState()); 530 | bundle.putDouble("MIN", normalizedMinValue); 531 | bundle.putDouble("MAX", normalizedMaxValue); 532 | bundle.putDouble("MIN_TIME", normalizedMinValueTime); 533 | bundle.putDouble("MAX_TIME", normalizedMaxValueTime); 534 | return bundle; 535 | } 536 | 537 | @Override 538 | protected void onRestoreInstanceState(Parcelable parcel) { 539 | final Bundle bundle = (Bundle) parcel; 540 | super.onRestoreInstanceState(bundle.getParcelable("SUPER")); 541 | normalizedMinValue = bundle.getDouble("MIN"); 542 | normalizedMaxValue = bundle.getDouble("MAX"); 543 | normalizedMinValueTime = bundle.getDouble("MIN_TIME"); 544 | normalizedMaxValueTime = bundle.getDouble("MAX_TIME"); 545 | } 546 | 547 | private OnRangeSeekBarChangeListener listener; 548 | 549 | public interface OnRangeSeekBarChangeListener { 550 | void onRangeSeekBarValuesChanged(RangeSeekBar bar, long minValue, long maxValue, int action, boolean isMin, Thumb pressedThumb); 551 | } 552 | 553 | public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener listener) { 554 | this.listener = listener; 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/RangeSeekBar2.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.os.Bundle; 11 | import android.os.Parcelable; 12 | import android.text.TextUtils; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.ViewConfiguration; 16 | import android.widget.ImageView; 17 | 18 | import java.io.File; 19 | import java.math.BigDecimal; 20 | import java.text.DecimalFormat; 21 | 22 | public class RangeSeekBar2 extends ImageView { 23 | private static final String TAG = RangeSeekBar.class.getSimpleName(); 24 | private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 25 | private Paint rectPaint; 26 | private Bitmap thumbImage_left; 27 | private Bitmap thumbImage_right; 28 | private Bitmap thumbPressedImage; 29 | private int thumbWidth; 30 | private float thumbHalfWidth; 31 | private float thumbHalfHeight; 32 | private final float thumbPaddingTop = 0; 33 | private final float thumbPressPaddingTop = 0; 34 | private final float padding = 0; 35 | private final T absoluteMinValue, absoluteMaxValue; 36 | private final NumberType numberType; 37 | private final double absoluteMinValuePrim, absoluteMaxValuePrim; 38 | private double normalizedMinValue = 0d; 39 | private double normalizedMaxValue = 1d;// normalized:规格化的--点坐标占总长度的比例值,范围从0-1 40 | private double normalizedMinValueTime = 0d; 41 | private double normalizedMaxValueTime = 1d;// normalized:规格化的--点坐标占总长度的比例值,范围从0-1 42 | private Thumb pressedThumb = null; 43 | private boolean notifyWhileDragging = false; 44 | private OnRangeSeekBarChangeListener listener; 45 | private double min_width = 1;//最小裁剪距离 46 | private long min_cut_time = 2000; 47 | private boolean isMin; 48 | public static final int FILE_NOT_EXIST_ACTION = 144; 49 | public static final int INVALID_POINTER_ID = 255; 50 | public static final int ACTION_POINTER_INDEX_MASK = 0x0000ff00, 51 | ACTION_POINTER_INDEX_SHIFT = 8; 52 | private float mDownMotionX;// 记录touchEvent按下时的X坐标 53 | private int mActivePointerId = INVALID_POINTER_ID; 54 | private int mScaledTouchSlop; 55 | private boolean mIsDragging; 56 | private String videoPath; 57 | private boolean isTouchDown; 58 | private Bitmap m_bg; 59 | private Bitmap m_progress; 60 | 61 | 62 | public RangeSeekBar2(Context context,T absoluteMinValue, T absoluteMaxValue) throws IllegalArgumentException { 63 | super(context); 64 | this.absoluteMinValue = absoluteMinValue; 65 | this.absoluteMaxValue = absoluteMaxValue; 66 | absoluteMinValuePrim = absoluteMinValue.doubleValue();// 都转换为double类型的值 67 | absoluteMaxValuePrim = absoluteMaxValue.doubleValue(); 68 | numberType = NumberType.fromNumber(absoluteMinValue);// 得到输入数字的枚举类型 69 | setFocusable(true); 70 | setFocusableInTouchMode(true); 71 | init(); 72 | } 73 | 74 | private void init() { 75 | // 被认为是触摸滑动的最短距离 76 | mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 77 | thumbImage_left = BitmapFactory.decodeResource(getResources(), R.drawable.handle_left); 78 | int width = thumbImage_left.getWidth(); 79 | int height = thumbImage_left.getHeight(); 80 | int newWidth = UIUtil.dip2px(getContext(),11); 81 | int newHeight = UIUtil.dip2px(getContext(),55); 82 | float scaleWidth = newWidth*1.0f/ width; 83 | float scaleHeight = newHeight*1.0f / height; 84 | Matrix matrix = new Matrix(); 85 | matrix.postScale(scaleWidth, scaleHeight); 86 | thumbImage_left= Bitmap.createBitmap(thumbImage_left, 0, 0, width, height, matrix, true); 87 | 88 | thumbImage_right=thumbImage_left; 89 | thumbPressedImage=thumbImage_left; 90 | // thumbImage_right = BitmapFactory.decodeResource(getResources(), R.drawable.upload_cut_handle_r); 91 | // thumbPressedImage = BitmapFactory.decodeResource(getResources(), R.drawable.upload_cut_handle_pressed); 92 | thumbWidth = newWidth; 93 | thumbHalfWidth = thumbWidth/2; 94 | thumbHalfHeight = newHeight; 95 | rectPaint = getRectPaint(); 96 | m_bg = BitmapFactory.decodeResource(getResources(), R.drawable.upload_overlay_black); 97 | m_progress = BitmapFactory.decodeResource(getResources(), R.drawable.upload_overlay_trans); 98 | } 99 | 100 | public void setMin_cut_time(long min_cut_time) { 101 | this.min_cut_time = min_cut_time; 102 | } 103 | 104 | public void setTouchDown(boolean touchDown) { 105 | isTouchDown = touchDown; 106 | } 107 | 108 | public void setVideoPath(String videoPath) { 109 | this.videoPath = videoPath; 110 | } 111 | 112 | /** 113 | * 供外部activity调用,控制是都在拖动的时候打印log信息,默认是false不打印 114 | */ 115 | public boolean isNotifyWhileDragging() { 116 | return notifyWhileDragging; 117 | } 118 | 119 | 120 | public void setNotifyWhileDragging(boolean flag) { 121 | this.notifyWhileDragging = flag; 122 | } 123 | 124 | public T getAbsoluteMinValue() { 125 | return absoluteMinValue; 126 | } 127 | 128 | 129 | public T getAbsoluteMaxValue() { 130 | return absoluteMaxValue; 131 | } 132 | 133 | 134 | public T getSelectedMinValue() { 135 | return normalizedToValue(normalizedMinValueTime); 136 | } 137 | 138 | public void setSelectedMinValue(T value) { 139 | // in case absoluteMinValue == absoluteMaxValue, avoid division by zero 140 | // when normalizing. 141 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 142 | // activity设置的最大值和最小值相等 143 | setNormalizedMinValue(0d); 144 | } else { 145 | setNormalizedMinValue(valueToNormalized(value)); 146 | } 147 | } 148 | 149 | public T getSelectedMaxValue() { 150 | return normalizedToValue(normalizedMaxValueTime); 151 | } 152 | 153 | 154 | public void setSelectedMaxValue(T value) { 155 | // in case absoluteMinValue == absoluteMaxValue, avoid division by zero 156 | // when normalizing. 157 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 158 | setNormalizedMaxValue(1d); 159 | } else { 160 | setNormalizedMaxValue(valueToNormalized(value)); 161 | } 162 | } 163 | 164 | 165 | public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener listener) { 166 | this.listener = listener; 167 | } 168 | 169 | 170 | private boolean checkVideoFile(String file_path) { 171 | if (TextUtils.isEmpty(file_path)) { 172 | return true; 173 | } 174 | File check_src_file = new File(file_path); 175 | if (!check_src_file.exists()) { 176 | return false; 177 | } 178 | return true; 179 | } 180 | 181 | 182 | /** 183 | * ACTION_MASK在Android中是应用于多点触摸操作,字面上的意思大概是动作掩码的意思吧。 184 | * 在onTouchEvent(MotionEvent event)中,使用switch 185 | * (event.getAction())可以处理ACTION_DOWN和ACTION_UP事件;使用switch 186 | * (event.getAction() & MotionEvent.ACTION_MASK) 187 | * 就可以处理处理多点触摸的ACTION_POINTER_DOWN和ACTION_POINTER_UP事件。 188 | */ 189 | @Override 190 | public boolean onTouchEvent(MotionEvent event) { 191 | 192 | if (isTouchDown) { 193 | return super.onTouchEvent(event); 194 | } 195 | 196 | if (event.getPointerCount() > 1) { 197 | return super.onTouchEvent(event); 198 | } 199 | 200 | if (!isEnabled()) 201 | return false; 202 | 203 | if (!checkVideoFile(videoPath)) { 204 | if (listener != null) { 205 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), FILE_NOT_EXIST_ACTION, isMin, pressedThumb); 206 | } 207 | return super.onTouchEvent(event); 208 | } 209 | 210 | if (absoluteMaxValuePrim <= min_cut_time) { 211 | return super.onTouchEvent(event); 212 | } 213 | 214 | int pointerIndex;// 记录点击点的index 215 | 216 | final int action = event.getAction(); 217 | switch (action & MotionEvent.ACTION_MASK) { 218 | 219 | case MotionEvent.ACTION_DOWN: 220 | 221 | // Remember where the motion event started 222 | // event.getPointerCount() - 223 | // 1得到最后一个点击屏幕的点,点击的点id从0到event.getPointerCount() - 1 224 | mActivePointerId = event.getPointerId(event.getPointerCount() - 1); 225 | pointerIndex = event.findPointerIndex(mActivePointerId); 226 | mDownMotionX = event.getX(pointerIndex);// 得到pointerIndex点击点的X坐标 227 | pressedThumb = evalPressedThumb(mDownMotionX);// 判断touch到的是最大值thumb还是最小值thumb 228 | 229 | // Only handle thumb presses. 230 | if (pressedThumb == null) 231 | return super.onTouchEvent(event); 232 | 233 | setPressed(true);// 设置该控件被按下了 234 | // invalidate();// 通知执行onDraw方法 235 | onStartTrackingTouch();// 置mIsDragging为true,开始追踪touch事件 236 | 237 | trackTouchEvent(event); 238 | attemptClaimDrag(); 239 | if (listener != null) { 240 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_DOWN, isMin, pressedThumb); 241 | } 242 | break; 243 | case MotionEvent.ACTION_MOVE: 244 | if (pressedThumb != null) { 245 | 246 | if (mIsDragging) { 247 | trackTouchEvent(event); 248 | } else { 249 | // Scroll to follow the motion event 250 | pointerIndex = event.findPointerIndex(mActivePointerId); 251 | final float x = event.getX(pointerIndex);// 手指在控件上点的X坐标 252 | // 手指没有点在最大最小值上,并且在控件上有滑动事件 253 | if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) { 254 | setPressed(true); 255 | Log.e(TAG, "没有拖住最大最小值");// 一直不会执行? 256 | invalidate(); 257 | onStartTrackingTouch(); 258 | trackTouchEvent(event); 259 | attemptClaimDrag(); 260 | } 261 | } 262 | 263 | if (notifyWhileDragging && listener != null) { 264 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_MOVE, isMin, pressedThumb); 265 | } 266 | } 267 | break; 268 | case MotionEvent.ACTION_UP: 269 | if (mIsDragging) { 270 | trackTouchEvent(event); 271 | onStopTrackingTouch(); 272 | setPressed(false); 273 | } else { 274 | onStartTrackingTouch(); 275 | trackTouchEvent(event); 276 | onStopTrackingTouch(); 277 | } 278 | 279 | invalidate(); 280 | if (listener != null) { 281 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_UP, isMin, pressedThumb); 282 | } 283 | pressedThumb = null;// 手指抬起,则置被touch到的thumb为空 284 | break; 285 | case MotionEvent.ACTION_POINTER_DOWN: { 286 | final int index = event.getPointerCount() - 1; 287 | // final int index = ev.getActionIndex(); 288 | mDownMotionX = event.getX(index); 289 | mActivePointerId = event.getPointerId(index); 290 | invalidate(); 291 | break; 292 | } 293 | case MotionEvent.ACTION_POINTER_UP: 294 | onSecondaryPointerUp(event); 295 | invalidate(); 296 | break; 297 | case MotionEvent.ACTION_CANCEL: 298 | if (mIsDragging) { 299 | onStopTrackingTouch(); 300 | setPressed(false); 301 | } 302 | invalidate(); // see above explanation 303 | break; 304 | } 305 | return true; 306 | } 307 | 308 | 309 | private final void onSecondaryPointerUp(MotionEvent ev) { 310 | final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; 311 | 312 | final int pointerId = ev.getPointerId(pointerIndex); 313 | if (pointerId == mActivePointerId) { 314 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 315 | mDownMotionX = ev.getX(newPointerIndex); 316 | mActivePointerId = ev.getPointerId(newPointerIndex); 317 | } 318 | } 319 | 320 | /** 321 | * 一直追踪touch事件,刷新view 322 | * 323 | * @param event 324 | */ 325 | private final void trackTouchEvent(MotionEvent event) { 326 | if (event.getPointerCount() > 1) return; 327 | Log.e(TAG, "trackTouchEvent: " + event.getAction() + " x: " + event.getX()); 328 | final int pointerIndex = event.findPointerIndex(mActivePointerId);// 得到按下点的index 329 | float x = 0; 330 | try { 331 | x = event.getX(pointerIndex);// 332 | } catch (Exception e) { 333 | return; 334 | } 335 | 336 | if (Thumb.MIN.equals(pressedThumb)) { 337 | // screenToNormalized(x)-->得到规格化的0-1的值 338 | setNormalizedMinValue(screenToNormalized(x, 0)); 339 | } else if (Thumb.MAX.equals(pressedThumb)) { 340 | setNormalizedMaxValue(screenToNormalized(x, 1)); 341 | } 342 | } 343 | 344 | /** 345 | * 试图告诉父view不要拦截子控件的drag 346 | */ 347 | private void attemptClaimDrag() { 348 | if (getParent() != null) { 349 | getParent().requestDisallowInterceptTouchEvent(true); 350 | } 351 | } 352 | 353 | void onStartTrackingTouch() { 354 | mIsDragging = true; 355 | } 356 | 357 | 358 | void onStopTrackingTouch() { 359 | mIsDragging = false; 360 | } 361 | 362 | 363 | @Override 364 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 365 | int width = 250; 366 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) { 367 | width = MeasureSpec.getSize(widthMeasureSpec); 368 | } 369 | int height = 100; 370 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) { 371 | height = MeasureSpec.getSize(heightMeasureSpec); 372 | } 373 | setMeasuredDimension(width, height); 374 | } 375 | 376 | 377 | @Override 378 | protected synchronized void onDraw(Canvas canvas) { 379 | super.onDraw(canvas); 380 | float bg_middle_left = 0;// 需要平铺的中间背景的开始坐标 381 | float bg_middle_right = getWidth();// 需要平铺的中间背景的开始坐标 382 | 383 | float scale = (bg_middle_right - bg_middle_left) / m_progress.getWidth();// 上层最大最小值间距离与m_progress比例 384 | float rangeL = normalizedToScreen(normalizedMinValue); 385 | float rangeR = normalizedToScreen(normalizedMaxValue); 386 | float pro_scale = (rangeR - rangeL) / m_progress.getWidth();// 上层最大最小值间距离与m_progress比例 387 | if (pro_scale > 0) { 388 | try { 389 | Matrix mx = new Matrix(); 390 | mx.postScale(scale, 1f); 391 | Bitmap m_bg_new = Bitmap.createBitmap(m_bg, 0, 0, m_bg.getWidth(), m_progress.getHeight(), mx, true); 392 | 393 | Matrix pro_mx = new Matrix(); 394 | pro_mx.postScale(pro_scale, 1f); 395 | Bitmap m_progress_new = Bitmap.createBitmap(m_progress, 0, 0, m_progress.getWidth(), 396 | m_progress.getHeight(), pro_mx, true); 397 | 398 | canvas.drawBitmap(m_progress_new, rangeL, thumbPaddingTop, paint); 399 | Bitmap m_bg_new1 = Bitmap.createBitmap(m_bg_new, 0, 0, (int) (rangeL - bg_middle_left) + (int) thumbWidth / 2, m_progress.getHeight()); 400 | canvas.drawBitmap(m_bg_new1, bg_middle_left, thumbPaddingTop, paint); 401 | 402 | Bitmap m_bg_new2 = Bitmap.createBitmap(m_bg_new, (int) (rangeR - thumbWidth / 2), 0, (int) (getWidth() - rangeR) + (int) thumbWidth / 2, m_progress.getHeight()); 403 | canvas.drawBitmap(m_bg_new2, (int) (rangeR - thumbWidth / 2), thumbPaddingTop, paint); 404 | 405 | canvas.drawRect(rangeL, thumbPaddingTop, rangeR, thumbPaddingTop + UIUtil.dip2px(getContext(),2), rectPaint); 406 | canvas.drawRect(rangeL, getHeight() - UIUtil.dip2px(getContext(),2), rangeR, getHeight(), rectPaint); 407 | } catch (Exception e) { 408 | // 当pro_scale非常小,例如width=12,Height=48,pro_scale=0.01979065时, 409 | // 宽高按比例计算后值为0.237、0.949,系统强转为int型后宽就变成0了。就出现非法参数异常 410 | Log.e(TAG, 411 | "IllegalArgumentException--width=" + m_progress.getWidth() + "Height=" + m_progress.getHeight() 412 | + "pro_scale=" + pro_scale, e); 413 | } 414 | 415 | } 416 | // draw minimum thumb 417 | drawThumb(normalizedToScreen(normalizedMinValue), false, canvas, true); 418 | // draw maximum thumb 419 | float drawRight = normalizedToScreen(normalizedMaxValue); 420 | // if (drawRight>(getWidth()-thumbWidth)) { 421 | // drawRight = getWidth()-thumbWidth; 422 | // } 423 | drawThumb(drawRight, false, canvas, false); 424 | } 425 | 426 | @Override 427 | protected Parcelable onSaveInstanceState() { 428 | final Bundle bundle = new Bundle(); 429 | bundle.putParcelable("SUPER", super.onSaveInstanceState()); 430 | bundle.putDouble("MIN", normalizedMinValue); 431 | bundle.putDouble("MAX", normalizedMaxValue); 432 | bundle.putDouble("MIN_TIME", normalizedMinValueTime); 433 | bundle.putDouble("MAX_TIME", normalizedMaxValueTime); 434 | return bundle; 435 | } 436 | 437 | @Override 438 | protected void onRestoreInstanceState(Parcelable parcel) { 439 | final Bundle bundle = (Bundle) parcel; 440 | super.onRestoreInstanceState(bundle.getParcelable("SUPER")); 441 | normalizedMinValue = bundle.getDouble("MIN"); 442 | normalizedMaxValue = bundle.getDouble("MAX"); 443 | normalizedMinValueTime = bundle.getDouble("MIN_TIME"); 444 | normalizedMaxValueTime = bundle.getDouble("MAX_TIME"); 445 | } 446 | 447 | private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean isLeft) { 448 | canvas.drawBitmap(pressed ? thumbPressedImage : (isLeft ? thumbImage_left : thumbImage_right), screenCoord - (isLeft ? 0 : thumbWidth), (pressed ? thumbPressPaddingTop : thumbPaddingTop), paint); 449 | } 450 | 451 | private Paint getRectPaint() { 452 | Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); 453 | p.setStyle(Paint.Style.FILL); 454 | p.setColor(Color.parseColor("#ffffff")); 455 | return p; 456 | } 457 | 458 | /** 459 | * 重新运算求出参数的内容 460 | * 461 | * @param touchX touchX 462 | * @return 被touch的是空还是最大值或最小值 463 | */ 464 | private Thumb evalPressedThumb(float touchX) { 465 | Thumb result = null; 466 | boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue, 2);// 触摸点是否在最小值图片范围内 467 | boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue, 2); 468 | if (minThumbPressed && maxThumbPressed) { 469 | // 如果两个thumbs重叠在一起,无法判断拖动哪个,做以下处理 470 | // 触摸点在屏幕右侧,则判断为touch到了最小值thumb,反之判断为touch到了最大值thumb 471 | result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX; 472 | } else if (minThumbPressed) { 473 | result = Thumb.MIN; 474 | } else if (maxThumbPressed) { 475 | result = Thumb.MAX; 476 | } 477 | return result; 478 | } 479 | 480 | private boolean isInThumbRange(float touchX, double normalizedThumbValue, double scale) { 481 | // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般 482 | // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。 483 | return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth * scale; 484 | } 485 | 486 | 487 | private boolean isInThumbRangeLeft(float touchX, double normalizedThumbValue, double scale) { 488 | // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般 489 | // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。 490 | return Math.abs(touchX - normalizedToScreen(normalizedThumbValue) - thumbWidth) <= thumbHalfWidth * scale; 491 | } 492 | 493 | 494 | public void setNormalizedMinValue(double value) { 495 | normalizedMinValue = Math.max(0d, Math.min(1d, Math.min(value, normalizedMaxValue))); 496 | invalidate();// 重新绘制此view 497 | } 498 | 499 | 500 | public void setNormalizedMaxValue(double value) { 501 | normalizedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, normalizedMinValue))); 502 | invalidate();// 重新绘制此view 503 | } 504 | 505 | 506 | @SuppressWarnings("unchecked") 507 | private T normalizedToValue(double normalized) { 508 | return (T) numberType.toNumber(absoluteMinValuePrim + normalized 509 | * (absoluteMaxValuePrim - absoluteMinValuePrim)); 510 | } 511 | 512 | private double valueToNormalized(T value) { 513 | if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) { 514 | // prevent division by zero, simply return 0. 515 | return 0d; 516 | } 517 | return (value.doubleValue() - absoluteMinValuePrim) / (absoluteMaxValuePrim - absoluteMinValuePrim); 518 | } 519 | 520 | 521 | private float normalizedToScreen(double normalizedCoord) { 522 | // getWidth() - 2 * padding --> 整个View宽度减去左右padding, 523 | // 即减去一个thumb的宽度,即两个thumb可滑动的范围长度 524 | 525 | // normalizedCoord * (getWidth() - 2 * padding) 526 | // 规格化值与长度的成绩,即该点在屏幕上的相对x坐标值 527 | 528 | // padding + normalizedCoord * (getWidth() - 2 * padding) 529 | // 该点在屏幕上的绝对x坐标值 530 | 531 | return (float) (padding + normalizedCoord * (getWidth() - 2 * padding)); 532 | // return (float) (normalizedCoord * getWidth()); 533 | } 534 | 535 | 536 | private double screenToNormalized(float screenCoord, int position) { 537 | int width = getWidth(); 538 | if (width <= 2 * padding) { 539 | // prevent division by zero, simply return 0. 540 | return 0d; 541 | } else { 542 | isMin = false; 543 | double current_width = screenCoord; 544 | float rangeL = normalizedToScreen(normalizedMinValue); 545 | float rangeR = normalizedToScreen(normalizedMaxValue); 546 | 547 | 548 | double min = min_cut_time / (absoluteMaxValuePrim - absoluteMinValuePrim) * (width - thumbWidth * 2); 549 | 550 | // if (absoluteMaxValuePrim>15*60*1000) { 551 | // 552 | // } else { 553 | // min_width = Math.round(min+0.5d); 554 | // } 555 | // min_width = min; 556 | if (absoluteMaxValuePrim > 5 * 60 * 1000) {//大于5分钟的精确小数四位 557 | DecimalFormat df = new DecimalFormat("0.0000"); 558 | min_width = Double.parseDouble(df.format(min)); 559 | } else { 560 | min_width = Math.round(min + 0.5d); 561 | } 562 | if (position == 0) { 563 | if (isInThumbRangeLeft(screenCoord, normalizedMinValue, 0.5)) { 564 | return normalizedMinValue; 565 | } 566 | 567 | float rightPosition = (getWidth() - rangeR) >= 0 ? (getWidth() - rangeR) : 0; 568 | double left_length = getValueLength() - (rightPosition + min_width); 569 | 570 | 571 | if (current_width > rangeL) { 572 | current_width = rangeL + (current_width - rangeL); 573 | } else if (current_width <= rangeL) { 574 | current_width = rangeL - (rangeL - current_width); 575 | } 576 | 577 | if (current_width > left_length) { 578 | isMin = true; 579 | current_width = left_length; 580 | } 581 | 582 | if (current_width < thumbWidth * 2 / 3) { 583 | current_width = 0; 584 | } 585 | 586 | double resultTime = (current_width - padding) / (width - 2 * thumbWidth); 587 | normalizedMinValueTime = Math.min(1d, Math.max(0d, resultTime)); 588 | double result = (current_width - padding) / (width - 2 * padding); 589 | return Math.min(1d, Math.max(0d, result));// 保证该该值为0-1之间,但是什么时候这个判断有用呢? 590 | } else { 591 | if (isInThumbRange(screenCoord, normalizedMaxValue, 0.5)) { 592 | return normalizedMaxValue; 593 | } 594 | 595 | double right_length = getValueLength() - (rangeL + min_width); 596 | if (current_width > rangeR) { 597 | current_width = rangeR + (current_width - rangeR); 598 | } else if (current_width <= rangeR) { 599 | current_width = rangeR - (rangeR - current_width); 600 | } 601 | 602 | double paddingRight = getWidth() - current_width; 603 | 604 | if (paddingRight > right_length) { 605 | isMin = true; 606 | current_width = getWidth() - right_length; 607 | paddingRight = right_length; 608 | } 609 | 610 | if (paddingRight < thumbWidth * 2 / 3) { 611 | current_width = getWidth(); 612 | paddingRight = 0; 613 | } 614 | 615 | double resultTime = (paddingRight - padding) / (width - 2 * thumbWidth); 616 | resultTime = 1 - resultTime; 617 | normalizedMaxValueTime = Math.min(1d, Math.max(0d, resultTime)); 618 | double result = (current_width - padding) / (width - 2 * padding); 619 | return Math.min(1d, Math.max(0d, result));// 保证该该值为0-1之间,但是什么时候这个判断有用呢? 620 | } 621 | 622 | } 623 | } 624 | 625 | 626 | private int getValueLength() { 627 | return (getWidth() - 2 * thumbWidth); 628 | } 629 | 630 | 631 | public interface OnRangeSeekBarChangeListener { 632 | void onRangeSeekBarValuesChanged(RangeSeekBar2 bar, T minValue, T maxValue, int action, boolean isMin, Thumb pressedThumb); 633 | } 634 | 635 | /** 636 | * 只有两个值,一个代表滑动条上的最大值,一个代表滑动条上的最小值 637 | */ 638 | public enum Thumb { 639 | MIN, MAX 640 | } 641 | 642 | private enum NumberType { 643 | LONG, DOUBLE, INTEGER, FLOAT, SHORT, BYTE, BIG_DECIMAL; 644 | 645 | public static NumberType fromNumber(E value) throws IllegalArgumentException { 646 | if (value instanceof Long) { 647 | return LONG; 648 | } 649 | if (value instanceof Double) { 650 | return DOUBLE; 651 | } 652 | if (value instanceof Integer) { 653 | return INTEGER; 654 | } 655 | if (value instanceof Float) { 656 | return FLOAT; 657 | } 658 | if (value instanceof Short) { 659 | return SHORT; 660 | } 661 | if (value instanceof Byte) { 662 | return BYTE; 663 | } 664 | if (value instanceof BigDecimal) { 665 | return BIG_DECIMAL; 666 | } 667 | throw new IllegalArgumentException("Number class '" + value.getClass().getName() + "' is not supported"); 668 | } 669 | 670 | public Number toNumber(double value) { 671 | // this代表调用该方法的对象,即枚举类中的枚举类型之一 672 | switch (this) { 673 | case LONG: 674 | return new Long((long) value); 675 | case DOUBLE: 676 | return value; 677 | case INTEGER: 678 | return new Integer((int) value); 679 | case FLOAT: 680 | return new Float(value); 681 | case SHORT: 682 | return new Short((short) value); 683 | case BYTE: 684 | return new Byte((byte) value); 685 | case BIG_DECIMAL: 686 | return new BigDecimal(value); 687 | } 688 | throw new InstantiationError("can't convert " + this + " to a Number object"); 689 | } 690 | } 691 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/UIUtil.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.WindowManager; 6 | 7 | import static android.content.Context.WINDOW_SERVICE; 8 | 9 | /** 10 | * ================================================ 11 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 12 | * 版 本: 13 | * 创建日期:2017/4/8-下午3:48 14 | * 描 述: 15 | * 修订历史: 16 | * ================================================ 17 | */ 18 | 19 | public class UIUtil { 20 | public static int dip2px(Context context, int dip) { 21 | float scale = context.getResources().getDisplayMetrics().density; 22 | return (int) ((float) dip * scale + 0.5F); 23 | } 24 | 25 | public static int getScreenWidth(Context context) { 26 | DisplayMetrics metric = new DisplayMetrics(); 27 | WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE); 28 | wm.getDefaultDisplay().getMetrics(metric); 29 | return metric.widthPixels; 30 | } 31 | 32 | public static int getScreenHeight(Context context) { 33 | DisplayMetrics metric = new DisplayMetrics(); 34 | WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE); 35 | wm.getDefaultDisplay().getMetrics(metric); 36 | return metric.heightPixels; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/VideoEditActivity.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.media.MediaPlayer; 5 | import android.os.Bundle; 6 | import android.os.Environment; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.text.TextUtils; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.ViewConfiguration; 17 | import android.view.animation.LinearInterpolator; 18 | import android.widget.FrameLayout; 19 | import android.widget.ImageView; 20 | import android.widget.LinearLayout; 21 | import android.widget.Toast; 22 | import android.widget.VideoView; 23 | 24 | import java.io.File; 25 | import java.lang.ref.WeakReference; 26 | 27 | /** 28 | * ================================================ 29 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 30 | * 版 本: 31 | * 创建日期:2017/4/8-下午3:48 32 | * 描 述: 33 | * 修订历史: 34 | * ================================================ 35 | */ 36 | public class VideoEditActivity extends AppCompatActivity { 37 | private static final String TAG = VideoEditActivity.class.getSimpleName(); 38 | private static final long MIN_CUT_DURATION = 3 * 1000L;// 最小剪辑时间3s 39 | private static final long MAX_CUT_DURATION = 60 * 1000L;//视频最多剪切多长时间 40 | private static final int MAX_COUNT_RANGE = 10;//seekBar的区域内一共有多少张图片 41 | private LinearLayout seekBarLayout; 42 | private ExtractVideoInfoUtil mExtractVideoInfoUtil; 43 | private int mMaxWidth; 44 | 45 | private long duration; 46 | private RangeSeekBar seekBar; 47 | private VideoView mVideoView; 48 | private RecyclerView mRecyclerView; 49 | private ImageView positionIcon; 50 | private VideoEditAdapter videoEditAdapter; 51 | private float averageMsPx;//每毫秒所占的px 52 | private float averagePxMs;//每px所占用的ms毫秒 53 | private String OutPutFileDirPath; 54 | private ExtractFrameWorkThread mExtractFrameWorkThread; 55 | private String path; 56 | private long leftProgress, rightProgress; 57 | private long scrollPos = 0; 58 | private int mScaledTouchSlop; 59 | private int lastScrollX; 60 | private boolean isSeeking; 61 | 62 | @Override 63 | protected void onCreate(Bundle savedInstanceState) { 64 | super.onCreate(savedInstanceState); 65 | setContentView(R.layout.activity_video_edit); 66 | initData(); 67 | initView(); 68 | initEditVideo(); 69 | initPlay(); 70 | } 71 | 72 | private void initData() { 73 | path = Environment.getExternalStorageDirectory() + "/2.mp4"; 74 | //for video check 75 | if (!new File(path).exists()) { 76 | Toast.makeText(this, "视频文件不存在", Toast.LENGTH_LONG).show(); 77 | finish(); 78 | } 79 | mExtractVideoInfoUtil = new ExtractVideoInfoUtil(path); 80 | duration = Long.valueOf(mExtractVideoInfoUtil.getVideoLength()); 81 | 82 | 83 | mMaxWidth = UIUtil.getScreenWidth(this) - UIUtil.dip2px(this, 70); 84 | mScaledTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); 85 | 86 | } 87 | 88 | private void initView() { 89 | seekBarLayout = (LinearLayout) findViewById(R.id.id_seekBarLayout); 90 | mVideoView = (VideoView) findViewById(R.id.uVideoView); 91 | positionIcon = (ImageView) findViewById(R.id.positionIcon); 92 | mRecyclerView = (RecyclerView) findViewById(R.id.id_rv_id); 93 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); 94 | videoEditAdapter = new VideoEditAdapter(this, 95 | (UIUtil.getScreenWidth(this) - UIUtil.dip2px(this, 70)) / 10); 96 | mRecyclerView.setAdapter(videoEditAdapter); 97 | mRecyclerView.addOnScrollListener(mOnScrollListener); 98 | } 99 | 100 | 101 | private void initEditVideo() { 102 | //for video edit 103 | long startPosition = 0; 104 | long endPosition = duration; 105 | int thumbnailsCount; 106 | int rangeWidth; 107 | boolean isOver_60_s; 108 | if (endPosition <= MAX_CUT_DURATION) { 109 | isOver_60_s = false; 110 | thumbnailsCount = MAX_COUNT_RANGE; 111 | rangeWidth = mMaxWidth; 112 | } else { 113 | isOver_60_s = true; 114 | thumbnailsCount = (int) (endPosition * 1.0f / (MAX_CUT_DURATION * 1.0f) * MAX_COUNT_RANGE); 115 | rangeWidth = mMaxWidth / MAX_COUNT_RANGE * thumbnailsCount; 116 | } 117 | mRecyclerView.addItemDecoration(new EditSpacingItemDecoration(UIUtil.dip2px(this, 35), thumbnailsCount)); 118 | 119 | //init seekBar 120 | if (isOver_60_s) { 121 | seekBar = new RangeSeekBar(this, 0L, MAX_CUT_DURATION); 122 | seekBar.setSelectedMinValue(0L); 123 | seekBar.setSelectedMaxValue(MAX_CUT_DURATION); 124 | } else { 125 | seekBar = new RangeSeekBar(this, 0L, endPosition); 126 | seekBar.setSelectedMinValue(0L); 127 | seekBar.setSelectedMaxValue(endPosition); 128 | } 129 | seekBar.setMin_cut_time(MIN_CUT_DURATION);//设置最小裁剪时间 130 | seekBar.setNotifyWhileDragging(true); 131 | seekBar.setOnRangeSeekBarChangeListener(mOnRangeSeekBarChangeListener); 132 | seekBarLayout.addView(seekBar); 133 | 134 | Log.d(TAG, "-------thumbnailsCount--->>>>" + thumbnailsCount); 135 | averageMsPx = duration * 1.0f / rangeWidth * 1.0f; 136 | Log.d(TAG, "-------rangeWidth--->>>>" + rangeWidth); 137 | Log.d(TAG, "-------localMedia.getDuration()--->>>>" + duration); 138 | Log.d(TAG, "-------averageMsPx--->>>>" + averageMsPx); 139 | OutPutFileDirPath = PictureUtils.getSaveEditThumbnailDir(this); 140 | int extractW = (UIUtil.getScreenWidth(this) - UIUtil.dip2px(this, 70)) / MAX_COUNT_RANGE; 141 | int extractH = UIUtil.dip2px(this, 55); 142 | mExtractFrameWorkThread = new ExtractFrameWorkThread(extractW, extractH, mUIHandler, path, OutPutFileDirPath, startPosition, endPosition, thumbnailsCount); 143 | mExtractFrameWorkThread.start(); 144 | 145 | //init pos icon start 146 | leftProgress = 0; 147 | if (isOver_60_s) { 148 | rightProgress = MAX_CUT_DURATION; 149 | } else { 150 | rightProgress = endPosition; 151 | } 152 | averagePxMs = (mMaxWidth * 1.0f / (rightProgress - leftProgress)); 153 | Log.d(TAG, "------averagePxMs----:>>>>>" + averagePxMs); 154 | 155 | 156 | } 157 | 158 | 159 | private void initPlay() { 160 | mVideoView.setVideoPath(path); 161 | //设置videoview的OnPrepared监听 162 | mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 163 | @Override 164 | public void onPrepared(MediaPlayer mp) { 165 | //设置MediaPlayer的OnSeekComplete监听 166 | mp.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { 167 | @Override 168 | public void onSeekComplete(MediaPlayer mp) { 169 | Log.d(TAG, "------ok----real---start-----"); 170 | Log.d(TAG, "------isSeeking-----"+isSeeking); 171 | if (!isSeeking) { 172 | videoStart(); 173 | } 174 | } 175 | }); 176 | } 177 | }); 178 | //first 179 | videoStart(); 180 | } 181 | 182 | private boolean isOverScaledTouchSlop; 183 | 184 | private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { 185 | @Override 186 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 187 | super.onScrollStateChanged(recyclerView, newState); 188 | Log.d(TAG, "-------newState:>>>>>" + newState); 189 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 190 | isSeeking = false; 191 | // videoStart(); 192 | } else { 193 | isSeeking = true; 194 | if (isOverScaledTouchSlop && mVideoView != null && mVideoView.isPlaying()) { 195 | videoPause(); 196 | } 197 | } 198 | } 199 | 200 | @Override 201 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 202 | super.onScrolled(recyclerView, dx, dy); 203 | isSeeking = false; 204 | int scrollX = getScrollXDistance(); 205 | //达不到滑动的距离 206 | if (Math.abs(lastScrollX - scrollX) < mScaledTouchSlop) { 207 | isOverScaledTouchSlop = false; 208 | return; 209 | } 210 | isOverScaledTouchSlop = true; 211 | Log.d(TAG, "-------scrollX:>>>>>" + scrollX); 212 | //初始状态,why ? 因为默认的时候有35dp的空白! 213 | if (scrollX == -UIUtil.dip2px(VideoEditActivity.this, 35)) { 214 | scrollPos = 0; 215 | } else { 216 | // why 在这里处理一下,因为onScrollStateChanged早于onScrolled回调 217 | if (mVideoView != null && mVideoView.isPlaying()) { 218 | videoPause(); 219 | } 220 | isSeeking = true; 221 | scrollPos = (long) (averageMsPx * (UIUtil.dip2px(VideoEditActivity.this, 35) + scrollX)); 222 | Log.d(TAG, "-------scrollPos:>>>>>" + scrollPos); 223 | leftProgress = seekBar.getSelectedMinValue() + scrollPos; 224 | rightProgress = seekBar.getSelectedMaxValue() + scrollPos; 225 | Log.d(TAG, "-------leftProgress:>>>>>" + leftProgress); 226 | mVideoView.seekTo((int) leftProgress); 227 | } 228 | lastScrollX = scrollX; 229 | } 230 | }; 231 | 232 | /** 233 | * 水平滑动了多少px 234 | * 235 | * @return int px 236 | */ 237 | private int getScrollXDistance() { 238 | LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager(); 239 | int position = layoutManager.findFirstVisibleItemPosition(); 240 | View firstVisibleChildView = layoutManager.findViewByPosition(position); 241 | int itemWidth = firstVisibleChildView.getWidth(); 242 | return (position) * itemWidth - firstVisibleChildView.getLeft(); 243 | } 244 | 245 | private ValueAnimator animator; 246 | 247 | private void anim() { 248 | Log.d(TAG, "--anim--onProgressUpdate---->>>>>>>" + mVideoView.getCurrentPosition()); 249 | if (positionIcon.getVisibility() == View.GONE) { 250 | positionIcon.setVisibility(View.VISIBLE); 251 | } 252 | final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) positionIcon.getLayoutParams(); 253 | int start = (int) (UIUtil.dip2px(this, 35) + (leftProgress/*mVideoView.getCurrentPosition()*/ - scrollPos) * averagePxMs); 254 | int end = (int) (UIUtil.dip2px(this, 35) + (rightProgress - scrollPos) * averagePxMs); 255 | animator = ValueAnimator 256 | .ofInt(start, end) 257 | .setDuration((rightProgress - scrollPos) - (leftProgress/*mVideoView.getCurrentPosition()*/ - scrollPos)); 258 | animator.setInterpolator(new LinearInterpolator()); 259 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 260 | @Override 261 | public void onAnimationUpdate(ValueAnimator animation) { 262 | params.leftMargin = (int) animation.getAnimatedValue(); 263 | positionIcon.setLayoutParams(params); 264 | } 265 | }); 266 | animator.start(); 267 | } 268 | 269 | private final MainHandler mUIHandler = new MainHandler(this); 270 | 271 | private static class MainHandler extends Handler { 272 | private final WeakReference mActivity; 273 | 274 | MainHandler(VideoEditActivity activity) { 275 | mActivity = new WeakReference<>(activity); 276 | } 277 | 278 | @Override 279 | public void handleMessage(Message msg) { 280 | VideoEditActivity activity = mActivity.get(); 281 | if (activity != null) { 282 | if (msg.what == ExtractFrameWorkThread.MSG_SAVE_SUCCESS) { 283 | if (activity.videoEditAdapter != null) { 284 | VideoEditInfo info = (VideoEditInfo) msg.obj; 285 | activity.videoEditAdapter.addItemVideoInfo(info); 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | private final RangeSeekBar.OnRangeSeekBarChangeListener mOnRangeSeekBarChangeListener = new RangeSeekBar.OnRangeSeekBarChangeListener() { 293 | @Override 294 | public void onRangeSeekBarValuesChanged(RangeSeekBar bar, long minValue, long maxValue, int action, boolean isMin, RangeSeekBar.Thumb pressedThumb) { 295 | Log.d(TAG, "-----minValue----->>>>>>" + minValue); 296 | Log.d(TAG, "-----maxValue----->>>>>>" + maxValue); 297 | leftProgress = minValue + scrollPos; 298 | rightProgress = maxValue + scrollPos; 299 | Log.d(TAG, "-----leftProgress----->>>>>>" + leftProgress); 300 | Log.d(TAG, "-----rightProgress----->>>>>>" + rightProgress); 301 | switch (action) { 302 | case MotionEvent.ACTION_DOWN: 303 | Log.d(TAG, "-----ACTION_DOWN---->>>>>>"); 304 | isSeeking = false; 305 | videoPause(); 306 | break; 307 | case MotionEvent.ACTION_MOVE: 308 | Log.d(TAG, "-----ACTION_MOVE---->>>>>>"); 309 | isSeeking = true; 310 | mVideoView.seekTo((int) (pressedThumb == RangeSeekBar.Thumb.MIN ? 311 | leftProgress : rightProgress)); 312 | break; 313 | case MotionEvent.ACTION_UP: 314 | Log.d(TAG, "-----ACTION_UP--leftProgress--->>>>>>" + leftProgress); 315 | isSeeking = false; 316 | //从minValue开始播 317 | mVideoView.seekTo((int) leftProgress); 318 | // videoStart(); 319 | break; 320 | default: 321 | break; 322 | } 323 | } 324 | }; 325 | 326 | 327 | private void videoStart() { 328 | Log.d(TAG, "----videoStart----->>>>>>>"); 329 | mVideoView.start(); 330 | positionIcon.clearAnimation(); 331 | if (animator != null && animator.isRunning()) { 332 | animator.cancel(); 333 | } 334 | anim(); 335 | handler.removeCallbacks(run); 336 | handler.post(run); 337 | } 338 | 339 | private void videoProgressUpdate() { 340 | long currentPosition = mVideoView.getCurrentPosition(); 341 | Log.d(TAG, "----onProgressUpdate-cp---->>>>>>>" + currentPosition); 342 | if (currentPosition >= (rightProgress)) { 343 | mVideoView.seekTo((int) leftProgress); 344 | positionIcon.clearAnimation(); 345 | if (animator != null && animator.isRunning()) { 346 | animator.cancel(); 347 | } 348 | anim(); 349 | } 350 | } 351 | 352 | private void videoPause() { 353 | isSeeking = false; 354 | if (mVideoView != null && mVideoView.isPlaying()) { 355 | mVideoView.pause(); 356 | handler.removeCallbacks(run); 357 | } 358 | Log.d(TAG, "----videoPause----->>>>>>>"); 359 | if (positionIcon.getVisibility() == View.VISIBLE) { 360 | positionIcon.setVisibility(View.GONE); 361 | } 362 | positionIcon.clearAnimation(); 363 | if (animator != null && animator.isRunning()) { 364 | animator.cancel(); 365 | } 366 | } 367 | 368 | 369 | @Override 370 | protected void onResume() { 371 | super.onResume(); 372 | if (mVideoView != null) { 373 | mVideoView.seekTo((int) leftProgress); 374 | // videoStart(); 375 | } 376 | } 377 | 378 | @Override 379 | protected void onPause() { 380 | super.onPause(); 381 | if (mVideoView != null && mVideoView.isPlaying()) { 382 | videoPause(); 383 | } 384 | } 385 | 386 | private Handler handler = new Handler(); 387 | private Runnable run = new Runnable() { 388 | 389 | @Override 390 | public void run() { 391 | videoProgressUpdate(); 392 | handler.postDelayed(run, 1000); 393 | } 394 | }; 395 | 396 | @Override 397 | protected void onDestroy() { 398 | super.onDestroy(); 399 | if (animator != null) { 400 | animator.cancel(); 401 | } 402 | if (mVideoView != null) { 403 | mVideoView.stopPlayback(); 404 | } 405 | if (mExtractVideoInfoUtil != null) { 406 | mExtractVideoInfoUtil.release(); 407 | } 408 | mRecyclerView.removeOnScrollListener(mOnScrollListener); 409 | if (mExtractFrameWorkThread != null) { 410 | mExtractFrameWorkThread.stopExtract(); 411 | } 412 | mUIHandler.removeCallbacksAndMessages(null); 413 | handler.removeCallbacksAndMessages(null); 414 | if (!TextUtils.isEmpty(OutPutFileDirPath)) { 415 | PictureUtils.deleteFile(new File(OutPutFileDirPath)); 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/VideoEditAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.LinearLayout; 10 | 11 | import com.bumptech.glide.Glide; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * ================================================ 18 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 19 | * 版 本: 20 | * 创建日期:2017/3/2-下午7:46 21 | * 描 述: 22 | * 修订历史: 23 | * ================================================ 24 | */ 25 | 26 | public class VideoEditAdapter extends RecyclerView.Adapter { 27 | private List lists = new ArrayList<>(); 28 | private LayoutInflater inflater; 29 | 30 | private int itemW; 31 | private Context context; 32 | 33 | public VideoEditAdapter(Context context, int itemW) { 34 | this.context = context; 35 | this.inflater = LayoutInflater.from(context); 36 | this.itemW = itemW; 37 | } 38 | 39 | @Override 40 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 41 | return new EditViewHolder(inflater.inflate(R.layout.video_item, parent, false)); 42 | } 43 | 44 | @Override 45 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 46 | EditViewHolder viewHolder = (EditViewHolder) holder; 47 | Glide.with(context) 48 | .load("file://" + lists.get(position).path) 49 | .into(viewHolder.img); 50 | } 51 | 52 | @Override 53 | public int getItemCount() { 54 | return lists.size(); 55 | } 56 | 57 | private final class EditViewHolder extends RecyclerView.ViewHolder { 58 | public ImageView img; 59 | 60 | EditViewHolder(View itemView) { 61 | super(itemView); 62 | img = (ImageView) itemView.findViewById(R.id.id_image); 63 | LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) img.getLayoutParams(); 64 | layoutParams.width = itemW; 65 | img.setLayoutParams(layoutParams); 66 | } 67 | } 68 | 69 | public void addItemVideoInfo(VideoEditInfo info) { 70 | lists.add(info); 71 | notifyItemInserted(lists.size()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/VideoEditInfo.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * ================================================ 7 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 8 | * 版 本: 9 | * 创建日期:2017/3/2-下午8:52 10 | * 描 述: 11 | * 修订历史: 12 | * ================================================ 13 | */ 14 | 15 | public class VideoEditInfo implements Serializable { 16 | 17 | public String path; //图片的sd卡路径 18 | public long time;//图片所在视频的时间 毫秒 19 | 20 | public VideoEditInfo() { 21 | } 22 | 23 | 24 | @Override 25 | public String toString() { 26 | return "VideoEditInfo{" + 27 | "path='" + path + '\'' + 28 | ", time='" + time + '\'' + 29 | '}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/gxz/example/videoedit/VideoExtractFrameAsyncUtils.java: -------------------------------------------------------------------------------- 1 | package com.gxz.example.videoedit; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Matrix; 5 | import android.media.MediaMetadataRetriever; 6 | import android.os.Handler; 7 | import android.os.Message; 8 | import android.util.Log; 9 | 10 | /** 11 | * ================================================ 12 | * 作 者:顾修忠-guxiuzhong@youku.com/gfj19900401@163.com 13 | * 版 本: 14 | * 创建日期:2017/3/2-下午7:06 15 | * 描 述: 16 | * 修订历史: 17 | * ================================================ 18 | */ 19 | 20 | public class VideoExtractFrameAsyncUtils { 21 | 22 | private Handler mHandler; 23 | private int extractW; 24 | private int extractH; 25 | 26 | public VideoExtractFrameAsyncUtils(int extractW, int extractH, Handler mHandler) { 27 | this.mHandler = mHandler; 28 | this.extractW=extractW; 29 | this.extractH=extractH; 30 | } 31 | 32 | public void getVideoThumbnailsInfoForEdit(String videoPath, String OutPutFileDirPath, long startPosition, long endPosition, int thumbnailsCount) { 33 | MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); 34 | metadataRetriever.setDataSource(videoPath); 35 | long interval = (endPosition - startPosition) / (thumbnailsCount - 1); 36 | for (int i = 0; i < thumbnailsCount; i++) { 37 | if (stop) { 38 | Log.d("ExtractFrame", "-------ok-stop-stop-->>>>>>>>>"); 39 | metadataRetriever.release(); 40 | break; 41 | } 42 | long time = startPosition + interval * i; 43 | if (i == thumbnailsCount - 1) { 44 | if (interval > 1000) { 45 | String path = extractFrame(metadataRetriever, endPosition - 800, OutPutFileDirPath); 46 | sendAPic(path, endPosition - 800); 47 | } else { 48 | String path = extractFrame(metadataRetriever, endPosition, OutPutFileDirPath); 49 | sendAPic(path, endPosition); 50 | } 51 | } else { 52 | String path = extractFrame(metadataRetriever, time, OutPutFileDirPath); 53 | sendAPic(path, time); 54 | } 55 | } 56 | metadataRetriever.release(); 57 | } 58 | 59 | /** 60 | * 成功一张add一张 61 | * 62 | * @param path path 63 | * @param time time 64 | */ 65 | private void sendAPic(String path, long time) { 66 | VideoEditInfo info = new VideoEditInfo(); 67 | info.path = path; 68 | info.time = time; 69 | Message msg = mHandler.obtainMessage(ExtractFrameWorkThread.MSG_SAVE_SUCCESS); 70 | msg.obj = info; 71 | mHandler.sendMessage(msg); 72 | } 73 | 74 | private String extractFrame(MediaMetadataRetriever metadataRetriever, long time, String OutPutFileDirPath) { 75 | Bitmap bitmap = metadataRetriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); 76 | if (bitmap != null) { 77 | Bitmap bitmapNew= scaleImage(bitmap); 78 | String path= PictureUtils.saveImageToSDForEdit(bitmapNew, OutPutFileDirPath, System.currentTimeMillis() + "_" + time + PictureUtils.POSTFIX); 79 | if (bitmapNew!=null &&!bitmapNew.isRecycled()) { 80 | bitmapNew.recycle(); 81 | bitmapNew = null; 82 | } 83 | return path; 84 | } 85 | return null; 86 | } 87 | 88 | /** 89 | * 设置固定的宽度,高度随之变化,使图片不会变形 90 | * 91 | * @param bm Bitmap 92 | * @return Bitmap 93 | */ 94 | private Bitmap scaleImage(Bitmap bm) { 95 | if (bm == null) { 96 | return null; 97 | } 98 | int width = bm.getWidth(); 99 | int height = bm.getHeight(); 100 | float scaleWidth = extractW * 1.0f / width; 101 | // float scaleHeight =extractH*1.0f / height; 102 | Matrix matrix = new Matrix(); 103 | matrix.postScale(scaleWidth, scaleWidth); 104 | Bitmap newBm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, 105 | true); 106 | if (!bm.isRecycled()) { 107 | bm.recycle(); 108 | bm = null; 109 | } 110 | return newBm; 111 | } 112 | 113 | 114 | private volatile boolean stop; 115 | 116 | public void stopExtract() { 117 | stop = true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/handle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ta893115871/VideoEdit/3c93e3c3ba17d0576ff6bcd6d0fca3f3350efe34/app/src/main/res/drawable-hdpi/handle_left.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/lf_ugc_publish_pos.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/upload_overlay_black.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ta893115871/VideoEdit/3c93e3c3ba17d0576ff6bcd6d0fca3f3350efe34/app/src/main/res/drawable-hdpi/upload_overlay_black.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/upload_overlay_trans.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ta893115871/VideoEdit/3c93e3c3ba17d0576ff6bcd6d0fca3f3350efe34/app/src/main/res/drawable-hdpi/upload_overlay_trans.9.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |