├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── k4l-video-trimmer ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── life │ │ └── knowledge4 │ │ └── videotrimmer │ │ └── ExampleInstrumentationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── life │ │ │ └── knowledge4 │ │ │ └── videotrimmer │ │ │ ├── K4LVideoTrimmer.java │ │ │ ├── interfaces │ │ │ ├── OnK4LVideoListener.java │ │ │ ├── OnProgressVideoListener.java │ │ │ ├── OnRangeSeekBarListener.java │ │ │ └── OnTrimVideoListener.java │ │ │ ├── utils │ │ │ ├── BackgroundExecutor.java │ │ │ ├── FileUtils.java │ │ │ ├── TrimVideoUtils.java │ │ │ └── UiThreadExecutor.java │ │ │ └── view │ │ │ ├── ProgressBarView.java │ │ │ ├── RangeSeekBarView.java │ │ │ ├── Thumb.java │ │ │ └── TimeLineView.java │ └── res │ │ ├── drawable-hdpi │ │ ├── apptheme_text_select_handle_left.png │ │ ├── apptheme_text_select_handle_middle.png │ │ ├── apptheme_text_select_handle_right.png │ │ └── icon_video_play.png │ │ ├── drawable-v21 │ │ ├── black_button_background.xml │ │ └── play_button.xml │ │ ├── drawable │ │ ├── black_button_background.xml │ │ └── play_button.xml │ │ ├── layout │ │ └── view_time_line.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── life │ └── knowledge4 │ └── videotrimmer │ └── ExampleUnitTest.java ├── mavenpush.gradle ├── sample-app ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── life │ │ └── knowledge4 │ │ └── videotrimmersample │ │ └── ExampleInstrumentationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── life │ │ │ └── knowledge4 │ │ │ └── videotrimmersample │ │ │ ├── MainActivity.java │ │ │ └── TrimmerActivity.java │ └── res │ │ ├── drawable-v21 │ │ └── background_button.xml │ │ ├── drawable │ │ ├── background_button.xml │ │ ├── ic_photo_size_select_actual_black_24dp.xml │ │ └── ic_videocam_black_24dp.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_trimmer.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── raw │ │ └── intro.mp4 │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── life │ └── knowledge4 │ └── videotrimmersample │ └── ExampleUnitTest.java ├── screenshot └── screenshot.png └── settings.gradle /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] I have verified there are no duplicate active or recent bugs, questions, or requests 2 | 3 | ###### Include the following: 4 | - k4l-video-trimmer version: `1.0.3` 5 | - Device OS version: `6.0.1` 6 | - Devide Manufacturer: `Motorola` 7 | - Device Name: `G4 Plus` 8 | 9 | ###### Reproduction Steps 10 | 1. 11 | 2. 12 | 3. 13 | 14 | ###### Expected Result 15 | 16 | ###### Actual Result 17 | 18 | ### Tell us what could be improved: 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ###### Fixes issue #. 2 | - [ ] This pull request follows the coding standards 3 | 4 | ###### This PR changes: 5 | - -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/.settings 3 | **/bin 4 | **/.classpath 5 | **/.project 6 | .DS_Store 7 | 8 | # Gradle 9 | .gradle 10 | gradle.properties 11 | 12 | # Built application files 13 | *.apk 14 | *.ap_ 15 | 16 | # Files for the dex VM 17 | *.dex 18 | 19 | # Java class files 20 | *.class 21 | 22 | # Generated files 23 | bin/ 24 | gen/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Windows thumbnail db 30 | Thumbs.db 31 | 32 | # OSX files 33 | .DS_Store 34 | 35 | # Eclipse project files 36 | .classpath 37 | .project 38 | 39 | # Android Studio 40 | .idea 41 | *.iml 42 | build/ 43 | 44 | /*/out 45 | /*/*/build 46 | /*/*/production 47 | *.iws 48 | *.ipr 49 | *~ 50 | *.swp 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Knowledge, education for life. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/hyperium/hyper/master/LICENSE) 2 | [![GitHub stars](https://img.shields.io/github/stars/knowledge4life/k4l-video-trimmer.svg)](https://github.com/knowledge4life/k4l-video-trimmer/stargazers) 3 | [![GitHub forks](https://img.shields.io/github/forks/knowledge4life/k4l-video-trimmer.svg)](https://github.com/knowledge4life/k4l-video-trimmer/network) 4 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-VideoTrimmer-green.svg?style=true)](https://android-arsenal.com/details/1/3714) 5 | 6 | # VideoTrimmer 7 | 8 | #### This project aims to provide an ultimate and flexible video trimmer experience. 9 | 10 | VideoTrimmer Screenshot 11 | 12 | ## [Watch a DEMO here](http://gfycat.com/UnnaturalConsiderateFiddlercrab) 13 | 14 | # Usage 15 | 16 | *For a working implementation, please have a look at the Sample Project - sample* 17 | 18 | 1. Include the library as local library project. 19 | 20 | ``` compile 'life.knowledge4:k4l-video-trimmer:1.0' ``` 21 | 22 | 2. Add K4LVideoTrimmer component into your layout. 23 | 24 | ``` 25 | 29 | ``` 30 | 31 | 3. Set the K4LVideoTrimmer selected video Uri. 32 | 33 | ```java 34 | K4LVideoTrimmer videoTrimmer = ((K4LVideoTrimmer) findViewById(R.id.timeLine)); 35 | if (videoTrimmer != null) { 36 | videoTrimmer.setVideoURI(Uri.parse(path)); 37 | } 38 | ``` 39 | 40 | # Default destination folder 41 | Environment.getExternalStorageDirectory() 42 | 43 | # Here is an example of a listener implementation. 44 | 45 | 1. Implements `OnTrimVideoListener` methods 46 | 47 | ```java 48 | @Override 49 | public void getResult(final Uri uri) { 50 | // handle K4LVideoTrimmer result. 51 | } 52 | 53 | @Override 54 | public void cancelAction() { 55 | // handle K4LVideoTrimmer cancel action 56 | } 57 | ``` 58 | 59 | # Customization 60 | 61 | * Custom destination folder 62 | ```java 63 | videoTrimmer.setDestinationPath("/storage/emulated/0/DCIM/CameraCustom/"); 64 | ``` 65 | 66 | * Set maximum video time interval 67 | ```java 68 | videoTrimmer.setMaxDuration(10); 69 | ``` 70 | 71 | # Incoming improvements 72 | 73 | - Customize K4LVideoTrimmer colors 74 | - Customize K4LVideoTrimmer drawables 75 | - Add support for `setMinDuration` 76 | - Add tests 77 | 78 | # Known issues and limitations 79 | - Thumbnails are only added to the timeline once all of them are created in a background thread 80 | - As for now there is no way of personalising the component 81 | - We only support MP4 files 82 | - Methods count: 5768 from Isoparser + 237 from K4l-video-trimmer 83 | 84 | # Compatibility 85 | 86 | * Library - Android ICS 4.1+ (API 16) 87 | * Sample - Android ICS 4.1+ (API 16) 88 | 89 | # Using SNAPSHOTS 90 | 91 | Add the sonatype snapshots repository. 92 | ``` 93 | 'https://oss.sonatype.org/content/repositories/snapshots/' 94 | ``` 95 | Example: 96 | ``` 97 | repositories{ 98 | flatDir{ 99 | dirs 'libs' 100 | } 101 | maven { 102 | url = 'https://oss.sonatype.org/content/repositories/snapshots/' 103 | } 104 | } 105 | ``` 106 | Then: 107 | ``` 108 | compile 'life.knowledge4:k4l-video-trimmer:1.1.3-SNAPSHOT' 109 | ``` 110 | 111 | ## Collaboration 112 | There are many ways of improving and adding more features, so feel free to collaborate with ideas, issues and/or pull requests. 113 | 114 | ### Let us know! 115 | 116 | We’d be really happy if you sent us links to your projects where you use our component. Just create an issue and let us know if you have any questions or suggestion regarding the library. 117 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.0-alpha7' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | project.ext{ 16 | VERSION_NAME='1.1.3-SNAPSHOT' 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titansgroup/k4l-video-trimmer/29601369245a34af57fe9f14b51475c46d31fb5b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 07 15:14:15 BRT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /k4l-video-trimmer/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from: '../mavenpush.gradle' 3 | 4 | android { 5 | compileSdkVersion 24 6 | buildToolsVersion "23.0.3" 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 24 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | compile 'com.android.support:appcompat-v7:24.0.0' 26 | compile 'com.googlecode.mp4parser:isoparser:1.1.20' 27 | testCompile 'junit:junit:4.12' 28 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 29 | androidTestCompile 'com.android.support.test:runner:0.5' 30 | androidTestCompile 'com.android.support:support-annotations:24.0.0' 31 | } 32 | -------------------------------------------------------------------------------- /k4l-video-trimmer/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/dogo/Development/sdks/android-sdk-macosx/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 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/androidTest/java/life/knowledge4/videotrimmer/ExampleInstrumentationTest.java: -------------------------------------------------------------------------------- 1 | package life.knowledge4.videotrimmer; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.filters.MediumTest; 6 | import android.support.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | 12 | import static org.junit.Assert.*; 13 | 14 | /** 15 | * Instrumentation test, which will execute on an Android device. 16 | * 17 | * @see Testing documentation 18 | */ 19 | @MediumTest 20 | @RunWith(AndroidJUnit4.class) 21 | public class ExampleInstrumentationTest { 22 | @Test 23 | public void useAppContext() throws Exception { 24 | // Context of the app under test. 25 | Context appContext = InstrumentationRegistry.getTargetContext(); 26 | 27 | assertEquals("life.knowledge4.videotrimmer", appContext.getPackageName()); 28 | } 29 | } -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/K4LVideoTrimmer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer; 25 | 26 | import android.content.Context; 27 | import android.media.MediaMetadataRetriever; 28 | import android.media.MediaPlayer; 29 | import android.net.Uri; 30 | import android.os.Environment; 31 | import android.os.Handler; 32 | import android.os.Message; 33 | import android.support.annotation.NonNull; 34 | import android.util.AttributeSet; 35 | import android.util.Log; 36 | import android.view.GestureDetector; 37 | import android.view.LayoutInflater; 38 | import android.view.MotionEvent; 39 | import android.view.View; 40 | import android.view.ViewGroup; 41 | import android.widget.FrameLayout; 42 | import android.widget.ImageView; 43 | import android.widget.RelativeLayout; 44 | import android.widget.SeekBar; 45 | import android.widget.TextView; 46 | import android.widget.VideoView; 47 | 48 | import java.io.File; 49 | import java.lang.ref.WeakReference; 50 | import java.util.ArrayList; 51 | import java.util.List; 52 | 53 | import life.knowledge4.videotrimmer.interfaces.OnK4LVideoListener; 54 | import life.knowledge4.videotrimmer.interfaces.OnProgressVideoListener; 55 | import life.knowledge4.videotrimmer.interfaces.OnRangeSeekBarListener; 56 | import life.knowledge4.videotrimmer.interfaces.OnTrimVideoListener; 57 | import life.knowledge4.videotrimmer.utils.BackgroundExecutor; 58 | import life.knowledge4.videotrimmer.utils.TrimVideoUtils; 59 | import life.knowledge4.videotrimmer.utils.UiThreadExecutor; 60 | import life.knowledge4.videotrimmer.view.ProgressBarView; 61 | import life.knowledge4.videotrimmer.view.RangeSeekBarView; 62 | import life.knowledge4.videotrimmer.view.Thumb; 63 | import life.knowledge4.videotrimmer.view.TimeLineView; 64 | 65 | import static life.knowledge4.videotrimmer.utils.TrimVideoUtils.stringForTime; 66 | 67 | public class K4LVideoTrimmer extends FrameLayout { 68 | 69 | private static final String TAG = K4LVideoTrimmer.class.getSimpleName(); 70 | private static final int MIN_TIME_FRAME = 1000; 71 | private static final int SHOW_PROGRESS = 2; 72 | 73 | private SeekBar mHolderTopView; 74 | private RangeSeekBarView mRangeSeekBarView; 75 | private RelativeLayout mLinearVideo; 76 | private View mTimeInfoContainer; 77 | private VideoView mVideoView; 78 | private ImageView mPlayView; 79 | private TextView mTextSize; 80 | private TextView mTextTimeFrame; 81 | private TextView mTextTime; 82 | private TimeLineView mTimeLineView; 83 | 84 | private ProgressBarView mVideoProgressIndicator; 85 | private Uri mSrc; 86 | private String mFinalPath; 87 | 88 | private int mMaxDuration; 89 | private List mListeners; 90 | 91 | private OnTrimVideoListener mOnTrimVideoListener; 92 | private OnK4LVideoListener mOnK4LVideoListener; 93 | 94 | private int mDuration = 0; 95 | private int mTimeVideo = 0; 96 | private int mStartPosition = 0; 97 | private int mEndPosition = 0; 98 | 99 | private long mOriginSizeFile; 100 | private boolean mResetSeekBar = true; 101 | private final MessageHandler mMessageHandler = new MessageHandler(this); 102 | 103 | public K4LVideoTrimmer(@NonNull Context context, AttributeSet attrs) { 104 | this(context, attrs, 0); 105 | } 106 | 107 | public K4LVideoTrimmer(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 108 | super(context, attrs, defStyleAttr); 109 | init(context); 110 | } 111 | 112 | private void init(Context context) { 113 | LayoutInflater.from(context).inflate(R.layout.view_time_line, this, true); 114 | 115 | mHolderTopView = ((SeekBar) findViewById(R.id.handlerTop)); 116 | mVideoProgressIndicator = ((ProgressBarView) findViewById(R.id.timeVideoView)); 117 | mRangeSeekBarView = ((RangeSeekBarView) findViewById(R.id.timeLineBar)); 118 | mLinearVideo = ((RelativeLayout) findViewById(R.id.layout_surface_view)); 119 | mVideoView = ((VideoView) findViewById(R.id.video_loader)); 120 | mPlayView = ((ImageView) findViewById(R.id.icon_video_play)); 121 | mTimeInfoContainer = findViewById(R.id.timeText); 122 | mTextSize = ((TextView) findViewById(R.id.textSize)); 123 | mTextTimeFrame = ((TextView) findViewById(R.id.textTimeSelection)); 124 | mTextTime = ((TextView) findViewById(R.id.textTime)); 125 | mTimeLineView = ((TimeLineView) findViewById(R.id.timeLineView)); 126 | 127 | setUpListeners(); 128 | setUpMargins(); 129 | } 130 | 131 | private void setUpListeners() { 132 | mListeners = new ArrayList<>(); 133 | mListeners.add(new OnProgressVideoListener() { 134 | @Override 135 | public void updateProgress(int time, int max, float scale) { 136 | updateVideoProgress(time); 137 | } 138 | }); 139 | mListeners.add(mVideoProgressIndicator); 140 | 141 | findViewById(R.id.btCancel) 142 | .setOnClickListener( 143 | new OnClickListener() { 144 | @Override 145 | public void onClick(View view) { 146 | onCancelClicked(); 147 | } 148 | } 149 | ); 150 | 151 | findViewById(R.id.btSave) 152 | .setOnClickListener( 153 | new OnClickListener() { 154 | @Override 155 | public void onClick(View view) { 156 | onSaveClicked(); 157 | } 158 | } 159 | ); 160 | 161 | final GestureDetector gestureDetector = new 162 | GestureDetector(getContext(), 163 | new GestureDetector.SimpleOnGestureListener() { 164 | @Override 165 | public boolean onSingleTapConfirmed(MotionEvent e) { 166 | onClickVideoPlayPause(); 167 | return true; 168 | } 169 | } 170 | ); 171 | 172 | mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { 173 | @Override 174 | public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { 175 | if (mOnTrimVideoListener != null) 176 | mOnTrimVideoListener.onError("Something went wrong reason : " + what); 177 | return false; 178 | } 179 | }); 180 | 181 | mVideoView.setOnTouchListener(new View.OnTouchListener() { 182 | @Override 183 | public boolean onTouch(View v, @NonNull MotionEvent event) { 184 | gestureDetector.onTouchEvent(event); 185 | return true; 186 | } 187 | }); 188 | 189 | mRangeSeekBarView.addOnRangeSeekBarListener(new OnRangeSeekBarListener() { 190 | @Override 191 | public void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 192 | // Do nothing 193 | } 194 | 195 | @Override 196 | public void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 197 | onSeekThumbs(index, value); 198 | } 199 | 200 | @Override 201 | public void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 202 | // Do nothing 203 | } 204 | 205 | @Override 206 | public void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 207 | onStopSeekThumbs(); 208 | } 209 | }); 210 | mRangeSeekBarView.addOnRangeSeekBarListener(mVideoProgressIndicator); 211 | 212 | mHolderTopView.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 213 | @Override 214 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 215 | onPlayerIndicatorSeekChanged(progress, fromUser); 216 | } 217 | 218 | @Override 219 | public void onStartTrackingTouch(SeekBar seekBar) { 220 | onPlayerIndicatorSeekStart(); 221 | } 222 | 223 | @Override 224 | public void onStopTrackingTouch(SeekBar seekBar) { 225 | onPlayerIndicatorSeekStop(seekBar); 226 | } 227 | }); 228 | 229 | mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 230 | @Override 231 | public void onPrepared(MediaPlayer mp) { 232 | onVideoPrepared(mp); 233 | } 234 | }); 235 | 236 | mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 237 | @Override 238 | public void onCompletion(MediaPlayer mp) { 239 | onVideoCompleted(); 240 | } 241 | }); 242 | } 243 | 244 | private void setUpMargins() { 245 | int marge = mRangeSeekBarView.getThumbs().get(0).getWidthBitmap(); 246 | int widthSeek = mHolderTopView.getThumb().getMinimumWidth() / 2; 247 | 248 | RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mHolderTopView.getLayoutParams(); 249 | lp.setMargins(marge - widthSeek, 0, marge - widthSeek, 0); 250 | mHolderTopView.setLayoutParams(lp); 251 | 252 | lp = (RelativeLayout.LayoutParams) mTimeLineView.getLayoutParams(); 253 | lp.setMargins(marge, 0, marge, 0); 254 | mTimeLineView.setLayoutParams(lp); 255 | 256 | lp = (RelativeLayout.LayoutParams) mVideoProgressIndicator.getLayoutParams(); 257 | lp.setMargins(marge, 0, marge, 0); 258 | mVideoProgressIndicator.setLayoutParams(lp); 259 | } 260 | 261 | private void onSaveClicked() { 262 | if (mStartPosition <= 0 && mEndPosition >= mDuration) { 263 | if (mOnTrimVideoListener != null) 264 | mOnTrimVideoListener.getResult(mSrc); 265 | } else { 266 | mPlayView.setVisibility(View.VISIBLE); 267 | mVideoView.pause(); 268 | 269 | MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); 270 | mediaMetadataRetriever.setDataSource(getContext(), mSrc); 271 | long METADATA_KEY_DURATION = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); 272 | 273 | final File file = new File(mSrc.getPath()); 274 | 275 | if (mTimeVideo < MIN_TIME_FRAME) { 276 | 277 | if ((METADATA_KEY_DURATION - mEndPosition) > (MIN_TIME_FRAME - mTimeVideo)) { 278 | mEndPosition += (MIN_TIME_FRAME - mTimeVideo); 279 | } else if (mStartPosition > (MIN_TIME_FRAME - mTimeVideo)) { 280 | mStartPosition -= (MIN_TIME_FRAME - mTimeVideo); 281 | } 282 | } 283 | 284 | //notify that video trimming started 285 | if (mOnTrimVideoListener != null) 286 | mOnTrimVideoListener.onTrimStarted(); 287 | 288 | BackgroundExecutor.execute( 289 | new BackgroundExecutor.Task("", 0L, "") { 290 | @Override 291 | public void execute() { 292 | try { 293 | TrimVideoUtils.startTrim(file, getDestinationPath(), mStartPosition, mEndPosition, mOnTrimVideoListener); 294 | } catch (final Throwable e) { 295 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); 296 | } 297 | } 298 | } 299 | ); 300 | } 301 | } 302 | 303 | private void onClickVideoPlayPause() { 304 | if (mVideoView.isPlaying()) { 305 | mPlayView.setVisibility(View.VISIBLE); 306 | mMessageHandler.removeMessages(SHOW_PROGRESS); 307 | mVideoView.pause(); 308 | } else { 309 | mPlayView.setVisibility(View.GONE); 310 | 311 | if (mResetSeekBar) { 312 | mResetSeekBar = false; 313 | mVideoView.seekTo(mStartPosition); 314 | } 315 | 316 | mMessageHandler.sendEmptyMessage(SHOW_PROGRESS); 317 | mVideoView.start(); 318 | } 319 | } 320 | 321 | private void onCancelClicked() { 322 | mVideoView.stopPlayback(); 323 | if (mOnTrimVideoListener != null) { 324 | mOnTrimVideoListener.cancelAction(); 325 | } 326 | } 327 | 328 | private String getDestinationPath() { 329 | if (mFinalPath == null) { 330 | File folder = Environment.getExternalStorageDirectory(); 331 | mFinalPath = folder.getPath() + File.separator; 332 | Log.d(TAG, "Using default path " + mFinalPath); 333 | } 334 | return mFinalPath; 335 | } 336 | 337 | private void onPlayerIndicatorSeekChanged(int progress, boolean fromUser) { 338 | 339 | int duration = (int) ((mDuration * progress) / 1000L); 340 | 341 | if (fromUser) { 342 | if (duration < mStartPosition) { 343 | setProgressBarPosition(mStartPosition); 344 | duration = mStartPosition; 345 | } else if (duration > mEndPosition) { 346 | setProgressBarPosition(mEndPosition); 347 | duration = mEndPosition; 348 | } 349 | setTimeVideo(duration); 350 | } 351 | } 352 | 353 | private void onPlayerIndicatorSeekStart() { 354 | mMessageHandler.removeMessages(SHOW_PROGRESS); 355 | mVideoView.pause(); 356 | mPlayView.setVisibility(View.VISIBLE); 357 | notifyProgressUpdate(false); 358 | } 359 | 360 | private void onPlayerIndicatorSeekStop(@NonNull SeekBar seekBar) { 361 | mMessageHandler.removeMessages(SHOW_PROGRESS); 362 | mVideoView.pause(); 363 | mPlayView.setVisibility(View.VISIBLE); 364 | 365 | int duration = (int) ((mDuration * seekBar.getProgress()) / 1000L); 366 | mVideoView.seekTo(duration); 367 | setTimeVideo(duration); 368 | notifyProgressUpdate(false); 369 | } 370 | 371 | private void onVideoPrepared(@NonNull MediaPlayer mp) { 372 | // Adjust the size of the video 373 | // so it fits on the screen 374 | int videoWidth = mp.getVideoWidth(); 375 | int videoHeight = mp.getVideoHeight(); 376 | float videoProportion = (float) videoWidth / (float) videoHeight; 377 | int screenWidth = mLinearVideo.getWidth(); 378 | int screenHeight = mLinearVideo.getHeight(); 379 | float screenProportion = (float) screenWidth / (float) screenHeight; 380 | ViewGroup.LayoutParams lp = mVideoView.getLayoutParams(); 381 | 382 | if (videoProportion > screenProportion) { 383 | lp.width = screenWidth; 384 | lp.height = (int) ((float) screenWidth / videoProportion); 385 | } else { 386 | lp.width = (int) (videoProportion * (float) screenHeight); 387 | lp.height = screenHeight; 388 | } 389 | mVideoView.setLayoutParams(lp); 390 | 391 | mPlayView.setVisibility(View.VISIBLE); 392 | 393 | mDuration = mVideoView.getDuration(); 394 | setSeekBarPosition(); 395 | 396 | setTimeFrames(); 397 | setTimeVideo(0); 398 | 399 | if (mOnK4LVideoListener != null) { 400 | mOnK4LVideoListener.onVideoPrepared(); 401 | } 402 | } 403 | 404 | private void setSeekBarPosition() { 405 | 406 | if (mDuration >= mMaxDuration) { 407 | mStartPosition = mDuration / 2 - mMaxDuration / 2; 408 | mEndPosition = mDuration / 2 + mMaxDuration / 2; 409 | 410 | mRangeSeekBarView.setThumbValue(0, (mStartPosition * 100) / mDuration); 411 | mRangeSeekBarView.setThumbValue(1, (mEndPosition * 100) / mDuration); 412 | 413 | } else { 414 | mStartPosition = 0; 415 | mEndPosition = mDuration; 416 | } 417 | 418 | setProgressBarPosition(mStartPosition); 419 | mVideoView.seekTo(mStartPosition); 420 | 421 | mTimeVideo = mDuration; 422 | mRangeSeekBarView.initMaxWidth(); 423 | } 424 | 425 | private void setTimeFrames() { 426 | String seconds = getContext().getString(R.string.short_seconds); 427 | mTextTimeFrame.setText(String.format("%s %s - %s %s", stringForTime(mStartPosition), seconds, stringForTime(mEndPosition), seconds)); 428 | } 429 | 430 | private void setTimeVideo(int position) { 431 | String seconds = getContext().getString(R.string.short_seconds); 432 | mTextTime.setText(String.format("%s %s", stringForTime(position), seconds)); 433 | } 434 | 435 | private void onSeekThumbs(int index, float value) { 436 | switch (index) { 437 | case Thumb.LEFT: { 438 | mStartPosition = (int) ((mDuration * value) / 100L); 439 | mVideoView.seekTo(mStartPosition); 440 | break; 441 | } 442 | case Thumb.RIGHT: { 443 | mEndPosition = (int) ((mDuration * value) / 100L); 444 | break; 445 | } 446 | } 447 | setProgressBarPosition(mStartPosition); 448 | 449 | setTimeFrames(); 450 | mTimeVideo = mEndPosition - mStartPosition; 451 | } 452 | 453 | private void onStopSeekThumbs() { 454 | mMessageHandler.removeMessages(SHOW_PROGRESS); 455 | mVideoView.pause(); 456 | mPlayView.setVisibility(View.VISIBLE); 457 | } 458 | 459 | private void onVideoCompleted() { 460 | mVideoView.seekTo(mStartPosition); 461 | } 462 | 463 | private void notifyProgressUpdate(boolean all) { 464 | if (mDuration == 0) return; 465 | 466 | int position = mVideoView.getCurrentPosition(); 467 | if (all) { 468 | for (OnProgressVideoListener item : mListeners) { 469 | item.updateProgress(position, mDuration, ((position * 100) / mDuration)); 470 | } 471 | } else { 472 | mListeners.get(1).updateProgress(position, mDuration, ((position * 100) / mDuration)); 473 | } 474 | } 475 | 476 | private void updateVideoProgress(int time) { 477 | if (mVideoView == null) { 478 | return; 479 | } 480 | 481 | if (time >= mEndPosition) { 482 | mMessageHandler.removeMessages(SHOW_PROGRESS); 483 | mVideoView.pause(); 484 | mPlayView.setVisibility(View.VISIBLE); 485 | mResetSeekBar = true; 486 | return; 487 | } 488 | 489 | if (mHolderTopView != null) { 490 | // use long to avoid overflow 491 | setProgressBarPosition(time); 492 | } 493 | setTimeVideo(time); 494 | } 495 | 496 | private void setProgressBarPosition(int position) { 497 | if (mDuration > 0) { 498 | long pos = 1000L * position / mDuration; 499 | mHolderTopView.setProgress((int) pos); 500 | } 501 | } 502 | 503 | /** 504 | * Set video information visibility. 505 | * For now this is for debugging 506 | * 507 | * @param visible whether or not the videoInformation will be visible 508 | */ 509 | public void setVideoInformationVisibility(boolean visible) { 510 | mTimeInfoContainer.setVisibility(visible ? VISIBLE : GONE); 511 | } 512 | 513 | /** 514 | * Listener for events such as trimming operation success and cancel 515 | * 516 | * @param onTrimVideoListener interface for events 517 | */ 518 | @SuppressWarnings("unused") 519 | public void setOnTrimVideoListener(OnTrimVideoListener onTrimVideoListener) { 520 | mOnTrimVideoListener = onTrimVideoListener; 521 | } 522 | 523 | /** 524 | * Listener for some {@link VideoView} events 525 | * 526 | * @param onK4LVideoListener interface for events 527 | */ 528 | @SuppressWarnings("unused") 529 | public void setOnK4LVideoListener(OnK4LVideoListener onK4LVideoListener) { 530 | mOnK4LVideoListener = onK4LVideoListener; 531 | } 532 | 533 | /** 534 | * Sets the path where the trimmed video will be saved 535 | * Ex: /storage/emulated/0/MyAppFolder/ 536 | * 537 | * @param finalPath the full path 538 | */ 539 | @SuppressWarnings("unused") 540 | public void setDestinationPath(final String finalPath) { 541 | mFinalPath = finalPath; 542 | Log.d(TAG, "Setting custom path " + mFinalPath); 543 | } 544 | 545 | /** 546 | * Cancel all current operations 547 | */ 548 | public void destroy() { 549 | BackgroundExecutor.cancelAll("", true); 550 | UiThreadExecutor.cancelAll(""); 551 | } 552 | 553 | /** 554 | * Set the maximum duration of the trimmed video. 555 | * The trimmer interface wont allow the user to set duration longer than maxDuration 556 | * 557 | * @param maxDuration the maximum duration of the trimmed video in seconds 558 | */ 559 | @SuppressWarnings("unused") 560 | public void setMaxDuration(int maxDuration) { 561 | mMaxDuration = maxDuration * 1000; 562 | } 563 | 564 | /** 565 | * Sets the uri of the video to be trimmer 566 | * 567 | * @param videoURI Uri of the video 568 | */ 569 | @SuppressWarnings("unused") 570 | public void setVideoURI(final Uri videoURI) { 571 | mSrc = videoURI; 572 | 573 | if (mOriginSizeFile == 0) { 574 | File file = new File(mSrc.getPath()); 575 | 576 | mOriginSizeFile = file.length(); 577 | long fileSizeInKB = mOriginSizeFile / 1024; 578 | 579 | if (fileSizeInKB > 1000) { 580 | long fileSizeInMB = fileSizeInKB / 1024; 581 | mTextSize.setText(String.format("%s %s", fileSizeInMB, getContext().getString(R.string.megabyte))); 582 | } else { 583 | mTextSize.setText(String.format("%s %s", fileSizeInKB, getContext().getString(R.string.kilobyte))); 584 | } 585 | } 586 | 587 | mVideoView.setVideoURI(mSrc); 588 | mVideoView.requestFocus(); 589 | 590 | mTimeLineView.setVideo(mSrc); 591 | } 592 | 593 | private static class MessageHandler extends Handler { 594 | 595 | @NonNull 596 | private final WeakReference mView; 597 | 598 | MessageHandler(K4LVideoTrimmer view) { 599 | mView = new WeakReference<>(view); 600 | } 601 | 602 | @Override 603 | public void handleMessage(Message msg) { 604 | K4LVideoTrimmer view = mView.get(); 605 | if (view == null || view.mVideoView == null) { 606 | return; 607 | } 608 | 609 | view.notifyProgressUpdate(true); 610 | if (view.mVideoView.isPlaying()) { 611 | sendEmptyMessageDelayed(0, 10); 612 | } 613 | } 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/interfaces/OnK4LVideoListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.interfaces; 25 | 26 | public interface OnK4LVideoListener { 27 | 28 | void onVideoPrepared(); 29 | } 30 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/interfaces/OnProgressVideoListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.interfaces; 25 | 26 | public interface OnProgressVideoListener { 27 | 28 | void updateProgress(int time, int max, float scale); 29 | } 30 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/interfaces/OnRangeSeekBarListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.interfaces; 25 | 26 | import life.knowledge4.videotrimmer.view.RangeSeekBarView; 27 | 28 | public interface OnRangeSeekBarListener { 29 | void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value); 30 | 31 | void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value); 32 | 33 | void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value); 34 | 35 | void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value); 36 | } 37 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/interfaces/OnTrimVideoListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.interfaces; 25 | 26 | import android.net.Uri; 27 | 28 | public interface OnTrimVideoListener { 29 | 30 | void onTrimStarted(); 31 | 32 | void getResult(final Uri uri); 33 | 34 | void cancelAction(); 35 | 36 | void onError(final String message); 37 | } 38 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/utils/BackgroundExecutor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2010-2016 eBusiness Information, Excilys Group 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed To in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package life.knowledge4.videotrimmer.utils; 17 | 18 | import android.util.Log; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.Executor; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.concurrent.Future; 26 | import java.util.concurrent.ScheduledExecutorService; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | 30 | public final class BackgroundExecutor { 31 | 32 | private static final String TAG = "BackgroundExecutor"; 33 | 34 | public static final Executor DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()); 35 | private static Executor executor = DEFAULT_EXECUTOR; 36 | private static final List TASKS = new ArrayList<>(); 37 | private static final ThreadLocal CURRENT_SERIAL = new ThreadLocal<>(); 38 | 39 | private BackgroundExecutor() { 40 | } 41 | 42 | /** 43 | * Execute a runnable after the given delay. 44 | * 45 | * @param runnable the task to execute 46 | * @param delay the time from now to delay execution, in milliseconds 47 | *

48 | * if delay is strictly positive and the current 49 | * executor does not support scheduling (if 50 | * Executor has been called with such an 51 | * executor) 52 | * @return Future associated to the running task 53 | * @throws IllegalArgumentException if the current executor set by Executor 54 | * does not support scheduling 55 | */ 56 | private static Future directExecute(Runnable runnable, long delay) { 57 | Future future = null; 58 | if (delay > 0) { 59 | /* no serial, but a delay: schedule the task */ 60 | if (!(executor instanceof ScheduledExecutorService)) { 61 | throw new IllegalArgumentException("The executor set does not support scheduling"); 62 | } 63 | ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor; 64 | future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS); 65 | } else { 66 | if (executor instanceof ExecutorService) { 67 | ExecutorService executorService = (ExecutorService) executor; 68 | future = executorService.submit(runnable); 69 | } else { 70 | /* non-cancellable task */ 71 | executor.execute(runnable); 72 | } 73 | } 74 | return future; 75 | } 76 | 77 | /** 78 | * Execute a task after (at least) its delay and after all 79 | * tasks added with the same non-null serial (if any) have 80 | * completed execution. 81 | * 82 | * @param task the task to execute 83 | * @throws IllegalArgumentException if task.delay is strictly positive and the 84 | * current executor does not support scheduling (if 85 | * Executor has been called with such an 86 | * executor) 87 | */ 88 | public static synchronized void execute(Task task) { 89 | Future future = null; 90 | if (task.serial == null || !hasSerialRunning(task.serial)) { 91 | task.executionAsked = true; 92 | future = directExecute(task, task.remainingDelay); 93 | } 94 | if ((task.id != null || task.serial != null) && !task.managed.get()) { 95 | /* keep task */ 96 | task.future = future; 97 | TASKS.add(task); 98 | } 99 | } 100 | 101 | /** 102 | * Indicates whether a task with the specified serial has been 103 | * submitted to the executor. 104 | * 105 | * @param serial the serial queue 106 | * @return true if such a task has been submitted, 107 | * false otherwise 108 | */ 109 | private static boolean hasSerialRunning(String serial) { 110 | for (Task task : TASKS) { 111 | if (task.executionAsked && serial.equals(task.serial)) { 112 | return true; 113 | } 114 | } 115 | return false; 116 | } 117 | 118 | /** 119 | * Retrieve and remove the first task having the specified 120 | * serial (if any). 121 | * 122 | * @param serial the serial queue 123 | * @return task if found, null otherwise 124 | */ 125 | private static Task take(String serial) { 126 | int len = TASKS.size(); 127 | for (int i = 0; i < len; i++) { 128 | if (serial.equals(TASKS.get(i).serial)) { 129 | return TASKS.remove(i); 130 | } 131 | } 132 | return null; 133 | } 134 | 135 | /** 136 | * Cancel all tasks having the specified id. 137 | * 138 | * @param id the cancellation identifier 139 | * @param mayInterruptIfRunning true if the thread executing this task should be 140 | * interrupted; otherwise, in-progress tasks are allowed to 141 | * complete 142 | */ 143 | public static synchronized void cancelAll(String id, boolean mayInterruptIfRunning) { 144 | for (int i = TASKS.size() - 1; i >= 0; i--) { 145 | Task task = TASKS.get(i); 146 | if (id.equals(task.id)) { 147 | if (task.future != null) { 148 | task.future.cancel(mayInterruptIfRunning); 149 | if (!task.managed.getAndSet(true)) { 150 | /* 151 | * the task has been submitted to the executor, but its 152 | * execution has not started yet, so that its run() 153 | * method will never call postExecute() 154 | */ 155 | task.postExecute(); 156 | } 157 | } else if (task.executionAsked) { 158 | Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)"); 159 | } else { 160 | /* this task has not been submitted to the executor */ 161 | TASKS.remove(i); 162 | } 163 | } 164 | } 165 | } 166 | 167 | public static abstract class Task implements Runnable { 168 | 169 | private String id; 170 | private long remainingDelay; 171 | private long targetTimeMillis; /* since epoch */ 172 | private String serial; 173 | private boolean executionAsked; 174 | private Future future; 175 | 176 | /* 177 | * A task can be cancelled after it has been submitted to the executor 178 | * but before its run() method is called. In that case, run() will never 179 | * be called, hence neither will postExecute(): the tasks with the same 180 | * serial identifier (if any) will never be submitted. 181 | * 182 | * Therefore, cancelAll() *must* call postExecute() if run() is not 183 | * started. 184 | * 185 | * This flag guarantees that either cancelAll() or run() manages this 186 | * task post execution, but not both. 187 | */ 188 | private AtomicBoolean managed = new AtomicBoolean(); 189 | 190 | public Task(String id, long delay, String serial) { 191 | if (!"".equals(id)) { 192 | this.id = id; 193 | } 194 | if (delay > 0) { 195 | remainingDelay = delay; 196 | targetTimeMillis = System.currentTimeMillis() + delay; 197 | } 198 | if (!"".equals(serial)) { 199 | this.serial = serial; 200 | } 201 | } 202 | 203 | @Override 204 | public void run() { 205 | if (managed.getAndSet(true)) { 206 | /* cancelled and postExecute() already called */ 207 | return; 208 | } 209 | 210 | try { 211 | CURRENT_SERIAL.set(serial); 212 | execute(); 213 | } finally { 214 | /* handle next tasks */ 215 | postExecute(); 216 | } 217 | } 218 | 219 | public abstract void execute(); 220 | 221 | private void postExecute() { 222 | if (id == null && serial == null) { 223 | /* nothing to do */ 224 | return; 225 | } 226 | CURRENT_SERIAL.set(null); 227 | synchronized (BackgroundExecutor.class) { 228 | /* execution complete */ 229 | TASKS.remove(this); 230 | 231 | if (serial != null) { 232 | Task next = take(serial); 233 | if (next != null) { 234 | if (next.remainingDelay != 0) { 235 | /* the delay may not have elapsed yet */ 236 | next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis()); 237 | } 238 | /* a task having the same serial was queued, execute it */ 239 | BackgroundExecutor.execute(next); 240 | } 241 | } 242 | } 243 | } 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.utils; 25 | 26 | import android.annotation.SuppressLint; 27 | import android.content.ContentUris; 28 | import android.content.Context; 29 | import android.database.Cursor; 30 | import android.net.Uri; 31 | import android.os.Build; 32 | import android.os.Environment; 33 | import android.provider.DocumentsContract; 34 | import android.provider.MediaStore; 35 | import android.support.annotation.NonNull; 36 | 37 | public class FileUtils { 38 | 39 | /** 40 | * Get a file path from a Uri. This will get the the path for Storage Access 41 | * Framework Documents, as well as the _data field for the MediaStore and 42 | * other file-based ContentProviders.
43 | *
44 | * Callers should check whether the path is local before assuming it 45 | * represents a local file. 46 | * 47 | * @param context The context. 48 | * @param uri The Uri to query. 49 | * @author paulburke 50 | */ 51 | @SuppressLint("NewApi") 52 | public static String getPath(final Context context, final Uri uri) { 53 | 54 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 55 | 56 | // DocumentProvider 57 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 58 | if (isExternalStorageDocument(uri)) { 59 | final String docId = DocumentsContract.getDocumentId(uri); 60 | final String[] split = docId.split(":"); 61 | final String type = split[0]; 62 | 63 | if ("primary".equalsIgnoreCase(type)) { 64 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 65 | } 66 | 67 | // TODO handle non-primary volumes 68 | } 69 | // DownloadsProvider 70 | else if (isDownloadsDocument(uri)) { 71 | 72 | final String id = DocumentsContract.getDocumentId(uri); 73 | final Uri contentUri = ContentUris.withAppendedId( 74 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 75 | 76 | return getDataColumn(context, contentUri, null, null); 77 | } 78 | // MediaProvider 79 | else if (isMediaDocument(uri)) { 80 | final String docId = DocumentsContract.getDocumentId(uri); 81 | final String[] split = docId.split(":"); 82 | final String type = split[0]; 83 | 84 | Uri contentUri = null; 85 | if ("image".equals(type)) { 86 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 87 | } else if ("video".equals(type)) { 88 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 89 | } else if ("audio".equals(type)) { 90 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 91 | } 92 | 93 | final String selection = "_id=?"; 94 | final String[] selectionArgs = new String[]{ 95 | split[1] 96 | }; 97 | 98 | return getDataColumn(context, contentUri, selection, selectionArgs); 99 | } 100 | } 101 | // MediaStore (and general) 102 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 103 | 104 | // Return the remote address 105 | if (isGooglePhotosUri(uri)) 106 | return uri.getLastPathSegment(); 107 | 108 | return getDataColumn(context, uri, null, null); 109 | } 110 | // File 111 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 112 | return uri.getPath(); 113 | } 114 | 115 | return null; 116 | } 117 | 118 | /** 119 | * Get the value of the data column for this Uri. This is useful for 120 | * MediaStore Uris, and other file-based ContentProviders. 121 | * 122 | * @param context The context. 123 | * @param uri The Uri to query. 124 | * @param selection (Optional) Filter used in the query. 125 | * @param selectionArgs (Optional) Selection arguments used in the query. 126 | * @return The value of the _data column, which is typically a file path. 127 | * @author paulburke 128 | */ 129 | private static String getDataColumn(@NonNull Context context, Uri uri, String selection, String[] selectionArgs) { 130 | 131 | Cursor cursor = null; 132 | final String column = "_data"; 133 | final String[] projection = {column}; 134 | 135 | try { 136 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); 137 | if (cursor != null && cursor.moveToFirst()) { 138 | final int column_index = cursor.getColumnIndexOrThrow(column); 139 | return cursor.getString(column_index); 140 | } 141 | } finally { 142 | if (cursor != null) 143 | cursor.close(); 144 | } 145 | return null; 146 | } 147 | 148 | /** 149 | * @param uri The Uri to check. 150 | * @return Whether the Uri authority is Google Photos. 151 | */ 152 | private static boolean isGooglePhotosUri(Uri uri) { 153 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 154 | } 155 | 156 | /** 157 | * @param uri The Uri to check. 158 | * @return Whether the Uri authority is ExternalStorageProvider. 159 | */ 160 | private static boolean isExternalStorageDocument(@NonNull Uri uri) { 161 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 162 | } 163 | 164 | /** 165 | * @param uri The Uri to check. 166 | * @return Whether the Uri authority is DownloadsProvider. 167 | */ 168 | private static boolean isDownloadsDocument(@NonNull Uri uri) { 169 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 170 | } 171 | 172 | /** 173 | * @param uri The Uri to check. 174 | * @return Whether the Uri authority is MediaProvider. 175 | */ 176 | private static boolean isMediaDocument(@NonNull Uri uri) { 177 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/utils/TrimVideoUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.utils; 25 | 26 | import android.net.Uri; 27 | import android.support.annotation.NonNull; 28 | import android.util.Log; 29 | 30 | import com.coremedia.iso.boxes.Container; 31 | import com.googlecode.mp4parser.FileDataSourceViaHeapImpl; 32 | import com.googlecode.mp4parser.authoring.Movie; 33 | import com.googlecode.mp4parser.authoring.Track; 34 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; 35 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; 36 | import com.googlecode.mp4parser.authoring.tracks.AppendTrack; 37 | import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; 38 | 39 | import java.io.File; 40 | import java.io.FileOutputStream; 41 | import java.io.IOException; 42 | import java.nio.channels.FileChannel; 43 | import java.text.SimpleDateFormat; 44 | import java.util.Arrays; 45 | import java.util.Date; 46 | import java.util.Formatter; 47 | import java.util.LinkedList; 48 | import java.util.List; 49 | import java.util.Locale; 50 | 51 | import life.knowledge4.videotrimmer.interfaces.OnTrimVideoListener; 52 | 53 | public class TrimVideoUtils { 54 | 55 | private static final String TAG = TrimVideoUtils.class.getSimpleName(); 56 | 57 | public static void startTrim(@NonNull File src, @NonNull String dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException { 58 | final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); 59 | final String fileName = "MP4_" + timeStamp + ".mp4"; 60 | final String filePath = dst + fileName; 61 | 62 | File file = new File(filePath); 63 | file.getParentFile().mkdirs(); 64 | Log.d(TAG, "Generated file path " + filePath); 65 | genVideoUsingMp4Parser(src, file, startMs, endMs, callback); 66 | } 67 | 68 | private static void genVideoUsingMp4Parser(@NonNull File src, @NonNull File dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException { 69 | // NOTE: Switched to using FileDataSourceViaHeapImpl since it does not use memory mapping (VM). 70 | // Otherwise we get OOM with large movie files. 71 | Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(src.getAbsolutePath())); 72 | 73 | List tracks = movie.getTracks(); 74 | movie.setTracks(new LinkedList()); 75 | // remove all tracks we will create new tracks from the old 76 | 77 | double startTime1 = startMs / 1000; 78 | double endTime1 = endMs / 1000; 79 | 80 | boolean timeCorrected = false; 81 | 82 | // Here we try to find a track that has sync samples. Since we can only start decoding 83 | // at such a sample we SHOULD make sure that the start of the new fragment is exactly 84 | // such a frame 85 | for (Track track : tracks) { 86 | if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { 87 | if (timeCorrected) { 88 | // This exception here could be a false positive in case we have multiple tracks 89 | // with sync samples at exactly the same positions. E.g. a single movie containing 90 | // multiple qualities of the same video (Microsoft Smooth Streaming file) 91 | 92 | throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported."); 93 | } 94 | startTime1 = correctTimeToSyncSample(track, startTime1, false); 95 | endTime1 = correctTimeToSyncSample(track, endTime1, true); 96 | timeCorrected = true; 97 | } 98 | } 99 | 100 | for (Track track : tracks) { 101 | long currentSample = 0; 102 | double currentTime = 0; 103 | double lastTime = -1; 104 | long startSample1 = -1; 105 | long endSample1 = -1; 106 | 107 | for (int i = 0; i < track.getSampleDurations().length; i++) { 108 | long delta = track.getSampleDurations()[i]; 109 | 110 | 111 | if (currentTime > lastTime && currentTime <= startTime1) { 112 | // current sample is still before the new starttime 113 | startSample1 = currentSample; 114 | } 115 | if (currentTime > lastTime && currentTime <= endTime1) { 116 | // current sample is after the new start time and still before the new endtime 117 | endSample1 = currentSample; 118 | } 119 | lastTime = currentTime; 120 | currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale(); 121 | currentSample++; 122 | } 123 | movie.addTrack(new AppendTrack(new CroppedTrack(track, startSample1, endSample1))); 124 | } 125 | 126 | dst.getParentFile().mkdirs(); 127 | 128 | if (!dst.exists()) { 129 | dst.createNewFile(); 130 | } 131 | 132 | Container out = new DefaultMp4Builder().build(movie); 133 | 134 | FileOutputStream fos = new FileOutputStream(dst); 135 | FileChannel fc = fos.getChannel(); 136 | out.writeContainer(fc); 137 | 138 | fc.close(); 139 | fos.close(); 140 | if (callback != null) 141 | callback.getResult(Uri.parse(dst.toString())); 142 | } 143 | 144 | private static double correctTimeToSyncSample(@NonNull Track track, double cutHere, boolean next) { 145 | double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; 146 | long currentSample = 0; 147 | double currentTime = 0; 148 | for (int i = 0; i < track.getSampleDurations().length; i++) { 149 | long delta = track.getSampleDurations()[i]; 150 | 151 | if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { 152 | // samples always start with 1 but we start with zero therefore +1 153 | timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime; 154 | } 155 | currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale(); 156 | currentSample++; 157 | 158 | } 159 | double previous = 0; 160 | for (double timeOfSyncSample : timeOfSyncSamples) { 161 | if (timeOfSyncSample > cutHere) { 162 | if (next) { 163 | return timeOfSyncSample; 164 | } else { 165 | return previous; 166 | } 167 | } 168 | previous = timeOfSyncSample; 169 | } 170 | return timeOfSyncSamples[timeOfSyncSamples.length - 1]; 171 | } 172 | 173 | public static String stringForTime(int timeMs) { 174 | int totalSeconds = timeMs / 1000; 175 | 176 | int seconds = totalSeconds % 60; 177 | int minutes = (totalSeconds / 60) % 60; 178 | int hours = totalSeconds / 3600; 179 | 180 | Formatter mFormatter = new Formatter(); 181 | if (hours > 0) { 182 | return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 183 | } else { 184 | return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/utils/UiThreadExecutor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2010-2016 eBusiness Information, Excilys Group 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed To in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package life.knowledge4.videotrimmer.utils; 17 | 18 | import android.os.Handler; 19 | import android.os.Looper; 20 | import android.os.Message; 21 | import android.os.SystemClock; 22 | 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | /** 27 | * This class provide operations for 28 | * UiThread tasks. 29 | */ 30 | public final class UiThreadExecutor { 31 | 32 | private static final Handler HANDLER = new Handler(Looper.getMainLooper()) { 33 | @Override 34 | public void handleMessage(Message msg) { 35 | Runnable callback = msg.getCallback(); 36 | if (callback != null) { 37 | callback.run(); 38 | decrementToken((Token) msg.obj); 39 | } else { 40 | super.handleMessage(msg); 41 | } 42 | } 43 | }; 44 | 45 | private static final Map TOKENS = new HashMap<>(); 46 | 47 | private UiThreadExecutor() { 48 | // should not be instantiated 49 | } 50 | 51 | /** 52 | * Store a new task in the map for providing cancellation. This method is 53 | * used by AndroidAnnotations and not intended to be called by clients. 54 | * 55 | * @param id the identifier of the task 56 | * @param task the task itself 57 | * @param delay the delay or zero to run immediately 58 | */ 59 | public static void runTask(String id, Runnable task, long delay) { 60 | if ("".equals(id)) { 61 | HANDLER.postDelayed(task, delay); 62 | return; 63 | } 64 | long time = SystemClock.uptimeMillis() + delay; 65 | HANDLER.postAtTime(task, nextToken(id), time); 66 | } 67 | 68 | private static Token nextToken(String id) { 69 | synchronized (TOKENS) { 70 | Token token = TOKENS.get(id); 71 | if (token == null) { 72 | token = new Token(id); 73 | TOKENS.put(id, token); 74 | } 75 | token.runnablesCount++; 76 | return token; 77 | } 78 | } 79 | 80 | private static void decrementToken(Token token) { 81 | synchronized (TOKENS) { 82 | if (--token.runnablesCount == 0) { 83 | String id = token.id; 84 | Token old = TOKENS.remove(id); 85 | if (old != token) { 86 | // a runnable finished after cancelling, we just removed a 87 | // wrong token, lets put it back 88 | TOKENS.put(id, old); 89 | } 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Cancel all tasks having the specified id. 96 | * 97 | * @param id the cancellation identifier 98 | */ 99 | public static void cancelAll(String id) { 100 | Token token; 101 | synchronized (TOKENS) { 102 | token = TOKENS.remove(id); 103 | } 104 | if (token == null) { 105 | // nothing to cancel 106 | return; 107 | } 108 | HANDLER.removeCallbacksAndMessages(token); 109 | } 110 | 111 | private static final class Token { 112 | int runnablesCount = 0; 113 | final String id; 114 | 115 | private Token(String id) { 116 | this.id = id; 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/view/ProgressBarView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.view; 25 | 26 | import android.content.Context; 27 | import android.graphics.Canvas; 28 | import android.graphics.Paint; 29 | import android.graphics.Rect; 30 | import android.support.annotation.NonNull; 31 | import android.support.v4.content.ContextCompat; 32 | import android.util.AttributeSet; 33 | import android.view.View; 34 | 35 | import life.knowledge4.videotrimmer.R; 36 | import life.knowledge4.videotrimmer.interfaces.OnProgressVideoListener; 37 | import life.knowledge4.videotrimmer.interfaces.OnRangeSeekBarListener; 38 | 39 | public class ProgressBarView extends View implements OnRangeSeekBarListener, OnProgressVideoListener { 40 | 41 | private int mProgressHeight; 42 | private int mViewWidth; 43 | 44 | private final Paint mBackgroundColor = new Paint(); 45 | private final Paint mProgressColor = new Paint(); 46 | 47 | private Rect mBackgroundRect; 48 | private Rect mProgressRect; 49 | 50 | public ProgressBarView(@NonNull Context context, AttributeSet attrs) { 51 | this(context, attrs, 0); 52 | } 53 | 54 | public ProgressBarView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 55 | super(context, attrs, defStyleAttr); 56 | init(); 57 | } 58 | 59 | private void init() { 60 | int lineProgress = ContextCompat.getColor(getContext(), R.color.progress_color); 61 | int lineBackground = ContextCompat.getColor(getContext(), R.color.background_progress_color); 62 | 63 | mProgressHeight = getContext().getResources().getDimensionPixelOffset(R.dimen.progress_video_line_height); 64 | 65 | mBackgroundColor.setAntiAlias(true); 66 | mBackgroundColor.setColor(lineBackground); 67 | 68 | mProgressColor.setAntiAlias(true); 69 | mProgressColor.setColor(lineProgress); 70 | } 71 | 72 | @Override 73 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 74 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 75 | 76 | int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 77 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1); 78 | 79 | int minH = getPaddingBottom() + getPaddingTop() + mProgressHeight; 80 | int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1); 81 | 82 | setMeasuredDimension(mViewWidth, viewHeight); 83 | } 84 | 85 | @Override 86 | protected void onDraw(@NonNull Canvas canvas) { 87 | super.onDraw(canvas); 88 | 89 | drawLineBackground(canvas); 90 | drawLineProgress(canvas); 91 | } 92 | 93 | private void drawLineBackground(@NonNull Canvas canvas) { 94 | if (mBackgroundRect != null) { 95 | canvas.drawRect(mBackgroundRect, mBackgroundColor); 96 | } 97 | } 98 | 99 | private void drawLineProgress(@NonNull Canvas canvas) { 100 | if (mProgressRect != null) { 101 | canvas.drawRect(mProgressRect, mProgressColor); 102 | } 103 | } 104 | 105 | @Override 106 | public void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 107 | updateBackgroundRect(index, value); 108 | } 109 | 110 | @Override 111 | public void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 112 | updateBackgroundRect(index, value); 113 | } 114 | 115 | @Override 116 | public void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 117 | updateBackgroundRect(index, value); 118 | } 119 | 120 | @Override 121 | public void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 122 | updateBackgroundRect(index, value); 123 | } 124 | 125 | private void updateBackgroundRect(int index, float value) { 126 | 127 | if (mBackgroundRect == null) { 128 | mBackgroundRect = new Rect(0, 0, mViewWidth, mProgressHeight); 129 | } 130 | 131 | int newValue = (int) ((mViewWidth * value) / 100); 132 | if (index == 0) { 133 | mBackgroundRect = new Rect(newValue, mBackgroundRect.top, mBackgroundRect.right, mBackgroundRect.bottom); 134 | } else { 135 | mBackgroundRect = new Rect(mBackgroundRect.left, mBackgroundRect.top, newValue, mBackgroundRect.bottom); 136 | } 137 | 138 | updateProgress(0, 0, 0.0f); 139 | } 140 | 141 | @Override 142 | public void updateProgress(int time, int max, float scale) { 143 | 144 | if (scale == 0) { 145 | mProgressRect = new Rect(0, mBackgroundRect.top, 0, mBackgroundRect.bottom); 146 | } else { 147 | int newValue = (int) ((mViewWidth * scale) / 100); 148 | mProgressRect = new Rect(mBackgroundRect.left, mBackgroundRect.top, newValue, mBackgroundRect.bottom); 149 | } 150 | 151 | invalidate(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/view/RangeSeekBarView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.view; 25 | 26 | import android.content.Context; 27 | import android.graphics.Canvas; 28 | import android.graphics.Paint; 29 | import android.graphics.Rect; 30 | import android.support.annotation.NonNull; 31 | import android.support.v4.content.ContextCompat; 32 | import android.util.AttributeSet; 33 | import android.view.MotionEvent; 34 | import android.view.View; 35 | 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | 39 | import life.knowledge4.videotrimmer.R; 40 | import life.knowledge4.videotrimmer.interfaces.OnRangeSeekBarListener; 41 | 42 | public class RangeSeekBarView extends View { 43 | 44 | private static final String TAG = RangeSeekBarView.class.getSimpleName(); 45 | 46 | private int mHeightTimeLine; 47 | private List mThumbs; 48 | private List mListeners; 49 | private float mMaxWidth; 50 | private float mThumbWidth; 51 | private float mThumbHeight; 52 | private int mViewWidth; 53 | private float mPixelRangeMin; 54 | private float mPixelRangeMax; 55 | private float mScaleRangeMax; 56 | private boolean mFirstRun; 57 | 58 | private final Paint mShadow = new Paint(); 59 | private final Paint mLine = new Paint(); 60 | 61 | public RangeSeekBarView(@NonNull Context context, AttributeSet attrs) { 62 | this(context, attrs, 0); 63 | } 64 | 65 | public RangeSeekBarView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 66 | super(context, attrs, defStyleAttr); 67 | init(); 68 | } 69 | 70 | private void init() { 71 | mThumbs = Thumb.initThumbs(getResources()); 72 | mThumbWidth = Thumb.getWidthBitmap(mThumbs); 73 | mThumbHeight = Thumb.getHeightBitmap(mThumbs); 74 | 75 | mScaleRangeMax = 100; 76 | mHeightTimeLine = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); 77 | 78 | setFocusable(true); 79 | setFocusableInTouchMode(true); 80 | 81 | mFirstRun = true; 82 | 83 | int shadowColor = ContextCompat.getColor(getContext(), R.color.shadow_color); 84 | mShadow.setAntiAlias(true); 85 | mShadow.setColor(shadowColor); 86 | mShadow.setAlpha(177); 87 | 88 | int lineColor = ContextCompat.getColor(getContext(), R.color.line_color); 89 | mLine.setAntiAlias(true); 90 | mLine.setColor(lineColor); 91 | mLine.setAlpha(200); 92 | } 93 | 94 | public void initMaxWidth() { 95 | mMaxWidth = mThumbs.get(1).getPos() - mThumbs.get(0).getPos(); 96 | 97 | onSeekStop(this, 0, mThumbs.get(0).getVal()); 98 | onSeekStop(this, 1, mThumbs.get(1).getVal()); 99 | } 100 | 101 | @Override 102 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 103 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 104 | 105 | int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 106 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1); 107 | 108 | int minH = getPaddingBottom() + getPaddingTop() + (int) mThumbHeight + mHeightTimeLine; 109 | int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1); 110 | 111 | setMeasuredDimension(mViewWidth, viewHeight); 112 | 113 | mPixelRangeMin = 0; 114 | mPixelRangeMax = mViewWidth - mThumbWidth; 115 | 116 | if (mFirstRun) { 117 | for (int i = 0; i < mThumbs.size(); i++) { 118 | Thumb th = mThumbs.get(i); 119 | th.setVal(mScaleRangeMax * i); 120 | th.setPos(mPixelRangeMax * i); 121 | } 122 | // Fire listener callback 123 | onCreate(this, currentThumb, getThumbValue(currentThumb)); 124 | mFirstRun = false; 125 | } 126 | } 127 | 128 | @Override 129 | protected void onDraw(@NonNull Canvas canvas) { 130 | super.onDraw(canvas); 131 | 132 | drawShadow(canvas); 133 | drawThumbs(canvas); 134 | } 135 | 136 | private int currentThumb = 0; 137 | 138 | @Override 139 | public boolean onTouchEvent(@NonNull MotionEvent ev) { 140 | final Thumb mThumb; 141 | final Thumb mThumb2; 142 | final float coordinate = ev.getX(); 143 | final int action = ev.getAction(); 144 | 145 | switch (action) { 146 | case MotionEvent.ACTION_DOWN: { 147 | // Remember where we started 148 | currentThumb = getClosestThumb(coordinate); 149 | 150 | if (currentThumb == -1) { 151 | return false; 152 | } 153 | 154 | mThumb = mThumbs.get(currentThumb); 155 | mThumb.setLastTouchX(coordinate); 156 | onSeekStart(this, currentThumb, mThumb.getVal()); 157 | return true; 158 | } 159 | case MotionEvent.ACTION_UP: { 160 | 161 | if (currentThumb == -1) { 162 | return false; 163 | } 164 | 165 | mThumb = mThumbs.get(currentThumb); 166 | onSeekStop(this, currentThumb, mThumb.getVal()); 167 | return true; 168 | } 169 | 170 | case MotionEvent.ACTION_MOVE: { 171 | mThumb = mThumbs.get(currentThumb); 172 | mThumb2 = mThumbs.get(currentThumb == 0 ? 1 : 0); 173 | // Calculate the distance moved 174 | final float dx = coordinate - mThumb.getLastTouchX(); 175 | final float newX = mThumb.getPos() + dx; 176 | if (currentThumb == 0) { 177 | 178 | if ((newX + mThumb.getWidthBitmap()) >= mThumb2.getPos()) { 179 | mThumb.setPos(mThumb2.getPos() - mThumb.getWidthBitmap()); 180 | } else if (newX <= mPixelRangeMin) { 181 | mThumb.setPos(mPixelRangeMin); 182 | } else { 183 | //Check if thumb is not out of max width 184 | checkPositionThumb(mThumb, mThumb2, dx, true); 185 | // Move the object 186 | mThumb.setPos(mThumb.getPos() + dx); 187 | 188 | // Remember this touch position for the next move event 189 | mThumb.setLastTouchX(coordinate); 190 | } 191 | 192 | } else { 193 | if (newX <= mThumb2.getPos() + mThumb2.getWidthBitmap()) { 194 | mThumb.setPos(mThumb2.getPos() + mThumb.getWidthBitmap()); 195 | } else if (newX >= mPixelRangeMax) { 196 | mThumb.setPos(mPixelRangeMax); 197 | } else { 198 | //Check if thumb is not out of max width 199 | checkPositionThumb(mThumb2, mThumb, dx, false); 200 | // Move the object 201 | mThumb.setPos(mThumb.getPos() + dx); 202 | // Remember this touch position for the next move event 203 | mThumb.setLastTouchX(coordinate); 204 | } 205 | } 206 | 207 | setThumbPos(currentThumb, mThumb.getPos()); 208 | 209 | // Invalidate to request a redraw 210 | invalidate(); 211 | return true; 212 | } 213 | } 214 | return false; 215 | } 216 | 217 | private void checkPositionThumb(@NonNull Thumb mThumbLeft, @NonNull Thumb mThumbRight, float dx, boolean isLeftMove) { 218 | if (isLeftMove && dx < 0) { 219 | if ((mThumbRight.getPos() - (mThumbLeft.getPos() + dx)) > mMaxWidth) { 220 | mThumbRight.setPos(mThumbLeft.getPos() + dx + mMaxWidth); 221 | setThumbPos(1, mThumbRight.getPos()); 222 | } 223 | } else if (!isLeftMove && dx > 0) { 224 | if (((mThumbRight.getPos() + dx) - mThumbLeft.getPos()) > mMaxWidth) { 225 | mThumbLeft.setPos(mThumbRight.getPos() + dx - mMaxWidth); 226 | setThumbPos(0, mThumbLeft.getPos()); 227 | } 228 | } 229 | } 230 | 231 | private int getUnstuckFrom(int index) { 232 | int unstuck = 0; 233 | float lastVal = mThumbs.get(index).getVal(); 234 | for (int i = index - 1; i >= 0; i--) { 235 | Thumb th = mThumbs.get(i); 236 | if (th.getVal() != lastVal) 237 | return i + 1; 238 | } 239 | return unstuck; 240 | } 241 | 242 | private float pixelToScale(int index, float pixelValue) { 243 | float scale = (pixelValue * 100) / mPixelRangeMax; 244 | if (index == 0) { 245 | float pxThumb = (scale * mThumbWidth) / 100; 246 | return scale + (pxThumb * 100) / mPixelRangeMax; 247 | } else { 248 | float pxThumb = ((100 - scale) * mThumbWidth) / 100; 249 | return scale - (pxThumb * 100) / mPixelRangeMax; 250 | } 251 | } 252 | 253 | private float scaleToPixel(int index, float scaleValue) { 254 | float px = (scaleValue * mPixelRangeMax) / 100; 255 | if (index == 0) { 256 | float pxThumb = (scaleValue * mThumbWidth) / 100; 257 | return px - pxThumb; 258 | } else { 259 | float pxThumb = ((100 - scaleValue) * mThumbWidth) / 100; 260 | return px + pxThumb; 261 | } 262 | } 263 | 264 | private void calculateThumbValue(int index) { 265 | if (index < mThumbs.size() && !mThumbs.isEmpty()) { 266 | Thumb th = mThumbs.get(index); 267 | th.setVal(pixelToScale(index, th.getPos())); 268 | onSeek(this, index, th.getVal()); 269 | } 270 | } 271 | 272 | private void calculateThumbPos(int index) { 273 | if (index < mThumbs.size() && !mThumbs.isEmpty()) { 274 | Thumb th = mThumbs.get(index); 275 | th.setPos(scaleToPixel(index, th.getVal())); 276 | } 277 | } 278 | 279 | private float getThumbValue(int index) { 280 | return mThumbs.get(index).getVal(); 281 | } 282 | 283 | public void setThumbValue(int index, float value) { 284 | mThumbs.get(index).setVal(value); 285 | calculateThumbPos(index); 286 | // Tell the view we want a complete redraw 287 | invalidate(); 288 | } 289 | 290 | private void setThumbPos(int index, float pos) { 291 | mThumbs.get(index).setPos(pos); 292 | calculateThumbValue(index); 293 | // Tell the view we want a complete redraw 294 | invalidate(); 295 | } 296 | 297 | private int getClosestThumb(float coordinate) { 298 | int closest = -1; 299 | if (!mThumbs.isEmpty()) { 300 | for (int i = 0; i < mThumbs.size(); i++) { 301 | // Find thumb closest to x coordinate 302 | final float tcoordinate = mThumbs.get(i).getPos() + mThumbWidth; 303 | if (coordinate >= mThumbs.get(i).getPos() && coordinate <= tcoordinate) { 304 | closest = mThumbs.get(i).getIndex(); 305 | } 306 | } 307 | } 308 | return closest; 309 | } 310 | 311 | private void drawShadow(@NonNull Canvas canvas) { 312 | if (!mThumbs.isEmpty()) { 313 | 314 | for (Thumb th : mThumbs) { 315 | if (th.getIndex() == 0) { 316 | final float x = th.getPos() + getPaddingLeft(); 317 | if (x > mPixelRangeMin) { 318 | Rect mRect = new Rect((int) mThumbWidth, 0, (int) (x + mThumbWidth), mHeightTimeLine); 319 | canvas.drawRect(mRect, mShadow); 320 | } 321 | } else { 322 | final float x = th.getPos() - getPaddingRight(); 323 | if (x < mPixelRangeMax) { 324 | Rect mRect = new Rect((int) x, 0, (int) (mViewWidth - mThumbWidth), mHeightTimeLine); 325 | canvas.drawRect(mRect, mShadow); 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | private void drawThumbs(@NonNull Canvas canvas) { 333 | 334 | if (!mThumbs.isEmpty()) { 335 | for (Thumb th : mThumbs) { 336 | if (th.getIndex() == 0) { 337 | canvas.drawBitmap(th.getBitmap(), th.getPos() + getPaddingLeft(), getPaddingTop() + mHeightTimeLine, null); 338 | } else { 339 | canvas.drawBitmap(th.getBitmap(), th.getPos() - getPaddingRight(), getPaddingTop() + mHeightTimeLine, null); 340 | } 341 | } 342 | } 343 | } 344 | 345 | public void addOnRangeSeekBarListener(OnRangeSeekBarListener listener) { 346 | 347 | if (mListeners == null) { 348 | mListeners = new ArrayList<>(); 349 | } 350 | 351 | mListeners.add(listener); 352 | } 353 | 354 | private void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 355 | if (mListeners == null) 356 | return; 357 | 358 | for (OnRangeSeekBarListener item : mListeners) { 359 | item.onCreate(rangeSeekBarView, index, value); 360 | } 361 | } 362 | 363 | private void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 364 | if (mListeners == null) 365 | return; 366 | 367 | for (OnRangeSeekBarListener item : mListeners) { 368 | item.onSeek(rangeSeekBarView, index, value); 369 | } 370 | } 371 | 372 | private void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 373 | if (mListeners == null) 374 | return; 375 | 376 | for (OnRangeSeekBarListener item : mListeners) { 377 | item.onSeekStart(rangeSeekBarView, index, value); 378 | } 379 | } 380 | 381 | private void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 382 | if (mListeners == null) 383 | return; 384 | 385 | for (OnRangeSeekBarListener item : mListeners) { 386 | item.onSeekStop(rangeSeekBarView, index, value); 387 | } 388 | } 389 | 390 | public List getThumbs() { 391 | return mThumbs; 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/view/Thumb.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.view; 25 | 26 | import android.content.res.Resources; 27 | import android.graphics.Bitmap; 28 | import android.graphics.BitmapFactory; 29 | import android.support.annotation.NonNull; 30 | 31 | import java.util.List; 32 | import java.util.Vector; 33 | 34 | import life.knowledge4.videotrimmer.R; 35 | 36 | public class Thumb { 37 | 38 | public static final int LEFT = 0; 39 | public static final int RIGHT = 1; 40 | 41 | private int mIndex; 42 | private float mVal; 43 | private float mPos; 44 | private Bitmap mBitmap; 45 | private int mWidthBitmap; 46 | private int mHeightBitmap; 47 | 48 | private float mLastTouchX; 49 | 50 | private Thumb() { 51 | mVal = 0; 52 | mPos = 0; 53 | } 54 | 55 | public int getIndex() { 56 | return mIndex; 57 | } 58 | 59 | private void setIndex(int index) { 60 | mIndex = index; 61 | } 62 | 63 | public float getVal() { 64 | return mVal; 65 | } 66 | 67 | public void setVal(float val) { 68 | mVal = val; 69 | } 70 | 71 | public float getPos() { 72 | return mPos; 73 | } 74 | 75 | public void setPos(float pos) { 76 | mPos = pos; 77 | } 78 | 79 | public Bitmap getBitmap() { 80 | return mBitmap; 81 | } 82 | 83 | private void setBitmap(@NonNull Bitmap bitmap) { 84 | mBitmap = bitmap; 85 | mWidthBitmap = bitmap.getWidth(); 86 | mHeightBitmap = bitmap.getHeight(); 87 | } 88 | 89 | @NonNull 90 | public static List initThumbs(Resources resources) { 91 | 92 | List thumbs = new Vector<>(); 93 | 94 | for (int i = 0; i < 2; i++) { 95 | Thumb th = new Thumb(); 96 | th.setIndex(i); 97 | if (i == 0) { 98 | int resImageLeft = R.drawable.apptheme_text_select_handle_left; 99 | th.setBitmap(BitmapFactory.decodeResource(resources, resImageLeft)); 100 | } else { 101 | int resImageRight = R.drawable.apptheme_text_select_handle_right; 102 | th.setBitmap(BitmapFactory.decodeResource(resources, resImageRight)); 103 | } 104 | 105 | thumbs.add(th); 106 | } 107 | 108 | return thumbs; 109 | } 110 | 111 | public static int getWidthBitmap(@NonNull List thumbs) { 112 | return thumbs.get(0).getWidthBitmap(); 113 | } 114 | 115 | public static int getHeightBitmap(@NonNull List thumbs) { 116 | return thumbs.get(0).getHeightBitmap(); 117 | } 118 | 119 | public float getLastTouchX() { 120 | return mLastTouchX; 121 | } 122 | 123 | public void setLastTouchX(float lastTouchX) { 124 | mLastTouchX = lastTouchX; 125 | } 126 | 127 | public int getWidthBitmap() { 128 | return mWidthBitmap; 129 | } 130 | 131 | private int getHeightBitmap() { 132 | return mHeightBitmap; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/view/TimeLineView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package life.knowledge4.videotrimmer.view; 25 | 26 | import android.content.Context; 27 | import android.graphics.Bitmap; 28 | import android.graphics.Canvas; 29 | import android.media.MediaMetadataRetriever; 30 | import android.net.Uri; 31 | import android.support.annotation.NonNull; 32 | import android.util.AttributeSet; 33 | import android.util.LongSparseArray; 34 | import android.view.View; 35 | 36 | import life.knowledge4.videotrimmer.R; 37 | import life.knowledge4.videotrimmer.utils.BackgroundExecutor; 38 | import life.knowledge4.videotrimmer.utils.UiThreadExecutor; 39 | 40 | public class TimeLineView extends View { 41 | 42 | private Uri mVideoUri; 43 | private int mHeightView; 44 | private LongSparseArray mBitmapList = null; 45 | 46 | public TimeLineView(@NonNull Context context, AttributeSet attrs) { 47 | this(context, attrs, 0); 48 | } 49 | 50 | public TimeLineView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | init(); 53 | } 54 | 55 | private void init() { 56 | mHeightView = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); 57 | } 58 | 59 | @Override 60 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 61 | final int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 62 | int w = resolveSizeAndState(minW, widthMeasureSpec, 1); 63 | 64 | final int minH = getPaddingBottom() + getPaddingTop() + mHeightView; 65 | int h = resolveSizeAndState(minH, heightMeasureSpec, 1); 66 | 67 | setMeasuredDimension(w, h); 68 | } 69 | 70 | @Override 71 | protected void onSizeChanged(final int w, int h, final int oldW, int oldH) { 72 | super.onSizeChanged(w, h, oldW, oldH); 73 | 74 | if (w != oldW) { 75 | getBitmap(w); 76 | } 77 | } 78 | 79 | private void getBitmap(final int viewWidth) { 80 | BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") { 81 | @Override 82 | public void execute() { 83 | try { 84 | LongSparseArray thumbnailList = new LongSparseArray<>(); 85 | 86 | MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); 87 | mediaMetadataRetriever.setDataSource(getContext(), mVideoUri); 88 | 89 | // Retrieve media data 90 | long videoLengthInMs = Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000; 91 | 92 | // Set thumbnail properties (Thumbs are squares) 93 | final int thumbWidth = mHeightView; 94 | final int thumbHeight = mHeightView; 95 | 96 | int numThumbs = (int) Math.ceil(((float) viewWidth) / thumbWidth); 97 | 98 | final long interval = videoLengthInMs / numThumbs; 99 | 100 | for (int i = 0; i < numThumbs; ++i) { 101 | Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); 102 | // TODO: bitmap might be null here, hence throwing NullPointerException. You were right 103 | try { 104 | bitmap = Bitmap.createScaledBitmap(bitmap, thumbWidth, thumbHeight, false); 105 | } catch (Exception e) { 106 | e.printStackTrace(); 107 | } 108 | thumbnailList.put(i, bitmap); 109 | } 110 | 111 | mediaMetadataRetriever.release(); 112 | returnBitmaps(thumbnailList); 113 | } catch (final Throwable e) { 114 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); 115 | } 116 | } 117 | } 118 | ); 119 | } 120 | 121 | private void returnBitmaps(final LongSparseArray thumbnailList) { 122 | UiThreadExecutor.runTask("", new Runnable() { 123 | @Override 124 | public void run() { 125 | mBitmapList = thumbnailList; 126 | invalidate(); 127 | } 128 | } 129 | , 0L); 130 | } 131 | 132 | @Override 133 | protected void onDraw(@NonNull Canvas canvas) { 134 | super.onDraw(canvas); 135 | 136 | if (mBitmapList != null) { 137 | canvas.save(); 138 | int x = 0; 139 | 140 | for (int i = 0; i < mBitmapList.size(); i++) { 141 | Bitmap bitmap = mBitmapList.get(i); 142 | 143 | if (bitmap != null) { 144 | canvas.drawBitmap(bitmap, x, 0, null); 145 | x = x + bitmap.getWidth(); 146 | } 147 | } 148 | } 149 | } 150 | 151 | public void setVideo(@NonNull Uri data) { 152 | mVideoUri = data; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titansgroup/k4l-video-trimmer/29601369245a34af57fe9f14b51475c46d31fb5b/k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_left.png -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titansgroup/k4l-video-trimmer/29601369245a34af57fe9f14b51475c46d31fb5b/k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_middle.png -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titansgroup/k4l-video-trimmer/29601369245a34af57fe9f14b51475c46d31fb5b/k4l-video-trimmer/src/main/res/drawable-hdpi/apptheme_text_select_handle_right.png -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-hdpi/icon_video_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titansgroup/k4l-video-trimmer/29601369245a34af57fe9f14b51475c46d31fb5b/k4l-video-trimmer/src/main/res/drawable-hdpi/icon_video_play.png -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-v21/black_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable-v21/play_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable/black_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/drawable/play_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /k4l-video-trimmer/src/main/res/layout/view_time_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 22 | 23 | 30 | 31 | 32 | 33 | 39 | 40 | 49 | 50 | 55 | 56 | 61 | 62 | 67 | 68 | 74 | 75 | 80 | 81 |