├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── deep │ │ └── videotrimmerexample │ │ ├── BaseActivity.java │ │ ├── Constants.java │ │ ├── MainActivity.java │ │ ├── OnSnackbarActionListener.java │ │ ├── VideoPicker.java │ │ └── VideoTrimmerActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_vector_arrow_back_black.xml │ ├── selector_camera_bottomsheet.xml │ ├── selector_gallery_bottomsheet.xml │ ├── v_ic_camera.xml │ └── v_ic_gallery.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_video_trimmer.xml │ ├── dialog_video_picker.xml │ └── toolbar.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── path.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── device-2018-06-06-170411.png ├── device-2018-06-06-170642.png ├── device-2018-06-06-170717.png └── device-2018-06-06-170736.png ├── settings.gradle └── videotrimmer ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── deep │ └── videotrimmer │ ├── DeepVideoTrimmer.java │ ├── interfaces │ ├── 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 ├── select_handle_right.png ├── text_select_handle_left.png └── text_select_handle_middle.png ├── drawable-v21 └── shape_black_button.xml ├── drawable-v24 └── ic_launcher_foreground.xml ├── drawable ├── ic_launcher_background.xml ├── ic_vector_play_gray.xml └── shape_black_button.xml ├── layout └── view_time_line.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png └── values ├── colors.xml ├── dimens.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screenshots: 2 | **Screenshot 1 :** 3 | 4 | Video Trimmer Screenshot 1 5 | 6 | 7 | **Screenshot 2 :** 8 | 9 | Video Trimmer Screenshot 2 10 | 11 | 12 | **Screenshot 3 :** 13 | 14 | Video Trimmer Screenshot 3 15 | 16 | 17 | # Video Trimmer 18 | Whatsapp like video trimmer to trim videos within a defined file size. 19 | 20 | # Add in your project 21 | 22 | **Gradle :** 23 | 24 | maven { 25 | url 'https://dl.bintray.com/deeppatel13/maven/' 26 | } 27 | 28 | implementation 'com.deep.videotrimmer:videotrimmer:1.0' 29 | 30 | >**Note:** If you have jCenter() added, then no need to write maven dependancy. only using implementation line it will be integrated. 31 | 32 | **XML :** 33 | 34 | 35 | 38 | 39 | # **Customization Settings :** 40 | 41 | Mention your own path to save trimmed videos: 42 | **setDestinationPath(StringPath);** 43 | 44 | Mention your desired max duration for trimmed videos: 45 | **setMaxDuration(int seconds);** //Defaults to 100Seconds 46 | 47 | Mention your desired max file size for trimmed videos: 48 | **setMaxFileSize(int mb);** //Defaults to 25Mb 49 | 50 | Mention your desired video URI to get trimmed video: 51 | **setVideoURI(Uri for video to trim);** 52 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | android { 3 | compileSdkVersion 27 4 | defaultConfig { 5 | applicationId "com.deep.videotrimmerexample" 6 | minSdkVersion 16 7 | targetSdkVersion 27 8 | versionCode 1 9 | versionName "1.0" 10 | vectorDrawables.useSupportLibrary = true 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | dataBinding { 19 | enabled = true 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation 'com.android.support:appcompat-v7:27.1.1' 29 | implementation 'com.android.support:design:27.1.1' 30 | implementation 'com.android.support:cardview-v7:27.1.1' 31 | implementation 'com.intuit.sdp:sdp-android:1.0.5' 32 | implementation project(':videotrimmer') 33 | implementation 'com.github.bumptech.glide:glide:4.5.0' 34 | annotationProcessor 'com.github.bumptech.glide:compiler:4.5.0' 35 | } 36 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | import android.content.Context; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.os.SystemClock; 10 | import android.provider.Settings; 11 | import android.support.annotation.NonNull; 12 | import android.support.annotation.Nullable; 13 | import android.support.design.widget.Snackbar; 14 | import android.support.v4.app.ActivityCompat; 15 | import android.support.v4.content.ContextCompat; 16 | import android.support.v7.app.ActionBar; 17 | import android.support.v7.app.AlertDialog; 18 | import android.support.v7.app.AppCompatActivity; 19 | import android.support.v7.app.AppCompatDelegate; 20 | import android.support.v7.widget.Toolbar; 21 | import android.view.MenuItem; 22 | import android.view.MotionEvent; 23 | import android.view.View; 24 | import android.view.inputmethod.InputMethodManager; 25 | import android.widget.EditText; 26 | import android.widget.TextView; 27 | import android.widget.Toast; 28 | 29 | 30 | /** 31 | * Created by Deep Patel 32 | * (Sr. Android Developer) 33 | * on 2/6/2018 34 | */ 35 | 36 | public class BaseActivity extends AppCompatActivity { 37 | protected boolean shouldPerformDispatchTouch = true; 38 | protected long lastClickTime = 0; 39 | public TextView title; 40 | public Toolbar toolbar; 41 | private Snackbar snackbar; 42 | 43 | public setPermissionListener permissionListener; 44 | static { 45 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); 46 | } 47 | 48 | @Override 49 | protected void onCreate(@Nullable Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | } 52 | 53 | @Override 54 | protected void onStart() { 55 | super.onStart(); 56 | } 57 | 58 | @Override 59 | protected void onDestroy() { 60 | super.onDestroy(); 61 | if (snackbar != null && snackbar.isShown()) snackbar.dismiss(); 62 | } 63 | 64 | public void showToastShort(String message) { 65 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 66 | } 67 | 68 | public void showToastLong(String message) { 69 | Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 70 | } 71 | 72 | public void showSnackbar(View view, String msg, int LENGTH) { 73 | if (view == null) return; 74 | snackbar = Snackbar.make(view, msg, LENGTH); 75 | View sbView = snackbar.getView(); 76 | TextView textView = (TextView) sbView.findViewById(android.support.design.R.id.snackbar_text); 77 | textView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.white)); 78 | snackbar.show(); 79 | } 80 | 81 | public void showSnackbar(View view, String msg, int LENGTH, String action, final OnSnackbarActionListener actionListener) { 82 | if (view == null) return; 83 | snackbar = Snackbar.make(view, msg, LENGTH); 84 | snackbar.setActionTextColor(ContextCompat.getColor(this, R.color.colorAccent)); 85 | if (actionListener != null) { 86 | snackbar.setAction(action, new View.OnClickListener() { 87 | @Override 88 | public void onClick(View view) { 89 | snackbar.dismiss(); 90 | actionListener.onAction(); 91 | } 92 | }); 93 | } 94 | View sbView = snackbar.getView(); 95 | TextView textView = (TextView) sbView.findViewById(android.support.design.R.id.snackbar_text); 96 | textView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.white)); 97 | snackbar.show(); 98 | } 99 | 100 | public void setUpToolbar(String strTitle) { 101 | setUpToolbarWithBackArrow(strTitle, false); 102 | } 103 | 104 | public void setUpToolbarWithBackArrow(String strTitle) { 105 | setUpToolbarWithBackArrow(strTitle, true); 106 | } 107 | 108 | public void setUpToolbarWithBackArrow(String strTitle, boolean isBackArrow) { 109 | toolbar = (Toolbar) findViewById(R.id.toolbar); 110 | setSupportActionBar(toolbar); 111 | ActionBar actionBar = getSupportActionBar(); 112 | if (actionBar != null) { 113 | actionBar.setDisplayShowTitleEnabled(false); 114 | actionBar.setDisplayHomeAsUpEnabled(isBackArrow); 115 | actionBar.setHomeAsUpIndicator(R.drawable.ic_vector_arrow_back_black); 116 | title = (TextView) toolbar.findViewById(R.id.title); 117 | title.setText(strTitle); 118 | } 119 | } 120 | 121 | @Override 122 | public boolean onOptionsItemSelected(MenuItem item) { 123 | switch (item.getItemId()) { 124 | case android.R.id.home: 125 | onBackPressed(); 126 | break; 127 | } 128 | return super.onOptionsItemSelected(item); 129 | } 130 | 131 | public void preventDoubleClick(View view) { 132 | /*// preventing double, using threshold of 1000 ms*/ 133 | if (SystemClock.elapsedRealtime() - lastClickTime < 1000) { 134 | return; 135 | } 136 | lastClickTime = SystemClock.elapsedRealtime(); 137 | } 138 | 139 | 140 | public void showSoftKeyboard(EditText editText) { 141 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 142 | imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); 143 | } 144 | 145 | public void hideSoftKeyboard() { 146 | try { 147 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 148 | imm.hideSoftInputFromWindow(getWindow().getCurrentFocus().getWindowToken(), 0); 149 | } catch (Exception e) { 150 | e.printStackTrace(); 151 | } 152 | } 153 | 154 | @Override 155 | public boolean dispatchTouchEvent(MotionEvent event) { 156 | boolean ret = false; 157 | try { 158 | View view = getCurrentFocus(); 159 | ret = super.dispatchTouchEvent(event); 160 | if (shouldPerformDispatchTouch) { 161 | if (view instanceof EditText) { 162 | View w = getCurrentFocus(); 163 | int scrCords[] = new int[2]; 164 | if (w != null) { 165 | w.getLocationOnScreen(scrCords); 166 | float x = event.getRawX() + w.getLeft() - scrCords[0]; 167 | float y = event.getRawY() + w.getTop() - scrCords[1]; 168 | 169 | if (event.getAction() == MotionEvent.ACTION_UP 170 | && (x < w.getLeft() || x >= w.getRight() || y < w.getTop() || y > w.getBottom())) { 171 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 172 | imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); 173 | } 174 | } 175 | } 176 | } 177 | return ret; 178 | } catch (Exception e) { 179 | return ret; 180 | } 181 | } 182 | 183 | public void showPermissionSettingDialog(String message) { 184 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 185 | builder.setTitle(R.string.need_permission); 186 | builder.setMessage(message); 187 | builder.setPositiveButton(R.string.app_settings, new DialogInterface.OnClickListener() { 188 | @Override 189 | public void onClick(DialogInterface dialog, int which) { 190 | Intent intent = new Intent(); 191 | intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 192 | intent.addCategory(Intent.CATEGORY_DEFAULT); 193 | intent.setData(Uri.parse("package:" + getPackageName())); 194 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 195 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); 196 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 197 | startActivity(intent); 198 | dialog.dismiss(); 199 | } 200 | }); 201 | builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 202 | @Override 203 | public void onClick(DialogInterface dialog, int which) { 204 | dialog.dismiss(); 205 | } 206 | }); 207 | builder.create().show(); 208 | } 209 | 210 | public void requestAppPermissions(final String[] requestedPermissions, 211 | final int requestCode, setPermissionListener listener) { 212 | this.permissionListener = listener; 213 | int permissionCheck = PackageManager.PERMISSION_GRANTED; 214 | for (String permission : requestedPermissions) { 215 | permissionCheck = permissionCheck + ContextCompat.checkSelfPermission(this, permission); 216 | } 217 | if (permissionCheck != PackageManager.PERMISSION_GRANTED) { 218 | ActivityCompat.requestPermissions(this, requestedPermissions, requestCode); 219 | } else { 220 | if (permissionListener != null) permissionListener.onPermissionGranted(requestCode); 221 | } 222 | } 223 | 224 | @Override 225 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 226 | for (String permission : permissions) { 227 | if (ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { 228 | if (permissionListener != null) permissionListener.onPermissionGranted(requestCode); 229 | break; 230 | } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { 231 | if (permissionListener != null) permissionListener.onPermissionDenied(requestCode); 232 | break; 233 | } else { 234 | if (permissionListener != null) 235 | permissionListener.onPermissionNeverAsk(requestCode); 236 | break; 237 | } 238 | } 239 | } 240 | 241 | 242 | public interface setPermissionListener { 243 | public void onPermissionGranted(int requestCode); 244 | 245 | public void onPermissionDenied(int requestCode); 246 | 247 | public void onPermissionNeverAsk(int requestCode); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/Constants.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | /** 4 | * Created by Deep Patel 5 | * (Sr. Android Developer) 6 | * on 6/4/2018 7 | */ 8 | public class Constants { 9 | public static final String EXTRA_VIDEO_PATH = "EXTRA_VIDEO_PATH"; 10 | public static String croppedVideoURI; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.databinding.DataBindingUtil; 7 | import android.graphics.Bitmap; 8 | import android.media.ThumbnailUtils; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.provider.MediaStore; 13 | import android.support.annotation.NonNull; 14 | import android.support.annotation.RequiresApi; 15 | import android.support.design.widget.Snackbar; 16 | import android.view.View; 17 | 18 | import com.bumptech.glide.Glide; 19 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 20 | import com.bumptech.glide.request.RequestOptions; 21 | import com.deep.videotrimmer.utils.FileUtils; 22 | import com.deep.videotrimmerexample.databinding.ActivityMainBinding; 23 | 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.File; 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | 29 | import static com.deep.videotrimmerexample.Constants.EXTRA_VIDEO_PATH; 30 | 31 | public class MainActivity extends BaseActivity implements View.OnClickListener { 32 | ActivityMainBinding mBinder; 33 | public static final int PERMISSION_STORAGE = 100; 34 | private final int REQUEST_VIDEO_TRIMMER_RESULT = 342; 35 | 36 | private final int REQUEST_VIDEO_TRIMMER = 0x12; 37 | private File thumbFile; 38 | private String selectedVideoName = null,selectedVideoFile = null; 39 | private RequestOptions simpleOptions; 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | mBinder = DataBindingUtil.setContentView(this, R.layout.activity_main); 44 | setUpToolbar("Video Trimmer Example"); 45 | mBinder.btnSelectVideo.setOnClickListener(this); 46 | simpleOptions = new RequestOptions() 47 | .centerCrop() 48 | .placeholder(R.color.blackOverlay) 49 | .error(R.color.blackOverlay) 50 | .diskCacheStrategy(DiskCacheStrategy.RESOURCE); 51 | } 52 | 53 | @Override 54 | public void onClick(View view) { 55 | switch (view.getId()) { 56 | case R.id.btnSelectVideo: 57 | checkForPermission(); 58 | break; 59 | } 60 | } 61 | 62 | private void checkForPermission() { 63 | requestAppPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 64 | PERMISSION_STORAGE, new BaseActivity.setPermissionListener() { 65 | @Override 66 | public void onPermissionGranted(int requestCode) { 67 | selectVideoDialog(); 68 | } 69 | 70 | @Override 71 | public void onPermissionDenied(int requestCode) { 72 | showSnackbar(mBinder.getRoot(), getString(R.string.critical_permission_denied), 73 | Snackbar.LENGTH_INDEFINITE, getString(R.string.allow), new OnSnackbarActionListener() { 74 | @Override 75 | public void onAction() { 76 | checkForPermission(); 77 | } 78 | }); 79 | } 80 | 81 | @Override 82 | public void onPermissionNeverAsk(int requestCode) { 83 | showPermissionSettingDialog(getString(R.string.permission_gallery_camera)); 84 | } 85 | }); 86 | } 87 | 88 | private void selectVideoDialog() { 89 | new VideoPicker(this) { 90 | @Override 91 | protected void onCameraClicked() { 92 | openVideoCapture(); 93 | } 94 | 95 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 96 | @Override 97 | protected void onGalleryClicked() { 98 | Intent intent = new Intent(); 99 | intent.setTypeAndNormalize("video/*"); 100 | intent.setAction(Intent.ACTION_GET_CONTENT); 101 | intent.addCategory(Intent.CATEGORY_OPENABLE); 102 | startActivityForResult(Intent.createChooser(intent, getString(R.string.select_video)), REQUEST_VIDEO_TRIMMER); 103 | } 104 | }.show(); 105 | } 106 | 107 | private void openVideoCapture() { 108 | Intent videoCapture = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 109 | startActivityForResult(videoCapture, REQUEST_VIDEO_TRIMMER); 110 | } 111 | 112 | private void startTrimActivity(@NonNull Uri uri) { 113 | Intent intent = new Intent(this, VideoTrimmerActivity.class); 114 | intent.putExtra(EXTRA_VIDEO_PATH, FileUtils.getPath(this, uri)); 115 | startActivityForResult(intent, REQUEST_VIDEO_TRIMMER_RESULT); 116 | } 117 | 118 | private File getFileFromBitmap(Bitmap bmp) { 119 | /*//create a file to write bitmap data*/ 120 | thumbFile = new File(this.getCacheDir(), "thumb_" + selectedVideoName + ".png"); 121 | try { 122 | thumbFile.createNewFile(); 123 | } catch (IOException e) { 124 | e.printStackTrace(); 125 | } 126 | /*//Convert bitmap to byte array*/ 127 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 128 | bmp.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos); 129 | byte[] bitmapdata = bos.toByteArray(); 130 | /*//write the bytes in file*/ 131 | try { 132 | FileOutputStream fos = new FileOutputStream(thumbFile); 133 | fos.write(bitmapdata); 134 | fos.flush(); 135 | fos.close(); 136 | } catch (Exception e) { 137 | e.printStackTrace(); 138 | } 139 | return thumbFile; 140 | } 141 | 142 | @Override 143 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 144 | super.onActivityResult(requestCode, resultCode, data); 145 | if (resultCode == Activity.RESULT_OK) { 146 | switch (requestCode) { 147 | case REQUEST_VIDEO_TRIMMER: 148 | final Uri selectedUri = data.getData(); 149 | if (selectedUri != null) { 150 | startTrimActivity(selectedUri); 151 | } else { 152 | showToastShort(getString(R.string.toast_cannot_retrieve_selected_video)); 153 | } 154 | break; 155 | case REQUEST_VIDEO_TRIMMER_RESULT: 156 | final Uri selectedVideoUri = data.getData(); 157 | 158 | if (selectedVideoUri != null) { 159 | selectedVideoFile = data.getData().getPath(); 160 | selectedVideoName = data.getData().getLastPathSegment(); 161 | Bitmap thumb = ThumbnailUtils.createVideoThumbnail(selectedVideoUri.getPath(), 162 | MediaStore.Images.Thumbnails.FULL_SCREEN_KIND); 163 | 164 | Glide.with(this) 165 | .load(getFileFromBitmap(thumb)) 166 | .apply(simpleOptions) 167 | .into(mBinder.selectedVideoThumb); 168 | } else { 169 | showToastShort(getString(R.string.toast_cannot_retrieve_selected_video)); 170 | } 171 | break; 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/OnSnackbarActionListener.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | /** 4 | * Created by Deep Patel 5 | */ 6 | 7 | public interface OnSnackbarActionListener { 8 | void onAction(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/VideoPicker.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | import android.content.Context; 4 | import android.databinding.DataBindingUtil; 5 | import android.os.Bundle; 6 | import android.os.SystemClock; 7 | import android.support.annotation.NonNull; 8 | import android.support.design.widget.BottomSheetDialog; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | 12 | import com.deep.videotrimmerexample.databinding.DialogVideoPickerBinding; 13 | 14 | 15 | public abstract class VideoPicker extends BottomSheetDialog implements View.OnClickListener { 16 | protected long lastClickTime = 0; 17 | DialogVideoPickerBinding mBinder; 18 | 19 | public VideoPicker(@NonNull Context context) { 20 | super(context); 21 | } 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | mBinder = DataBindingUtil.inflate(LayoutInflater.from(getContext()), R.layout.dialog_video_picker, null, false); 27 | setContentView(mBinder.getRoot()); 28 | 29 | mBinder.camera.setOnClickListener(this); 30 | mBinder.gallery.setOnClickListener(this); 31 | } 32 | 33 | @Override 34 | public void onClick(View view) { 35 | preventDoubleClick(view); 36 | dismiss(); 37 | switch (view.getId()) { 38 | case R.id.camera: 39 | onCameraClicked(); 40 | break; 41 | case R.id.gallery: 42 | onGalleryClicked(); 43 | break; 44 | } 45 | } 46 | 47 | private void preventDoubleClick(View view) { 48 | /*// preventing double, using threshold of 1000 ms*/ 49 | if (SystemClock.elapsedRealtime() - lastClickTime < 1000) { 50 | return; 51 | } 52 | lastClickTime = SystemClock.elapsedRealtime(); 53 | } 54 | 55 | protected abstract void onCameraClicked(); 56 | 57 | protected abstract void onGalleryClicked(); 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/deep/videotrimmerexample/VideoTrimmerActivity.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmerexample; 2 | 3 | import android.content.Intent; 4 | import android.databinding.DataBindingUtil; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | import com.deep.videotrimmer.DeepVideoTrimmer; 11 | import com.deep.videotrimmer.interfaces.OnTrimVideoListener; 12 | import com.deep.videotrimmer.view.RangeSeekBarView; 13 | import com.deep.videotrimmerexample.databinding.ActivityVideoTrimmerBinding; 14 | 15 | import static com.deep.videotrimmerexample.Constants.EXTRA_VIDEO_PATH; 16 | 17 | public class VideoTrimmerActivity extends BaseActivity implements OnTrimVideoListener { 18 | ActivityVideoTrimmerBinding mBinder; 19 | private DeepVideoTrimmer mVideoTrimmer; 20 | TextView textSize, tvCroppingMessage; 21 | RangeSeekBarView timeLineBar; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | mBinder = DataBindingUtil.setContentView(this, R.layout.activity_video_trimmer); 27 | 28 | Intent extraIntent = getIntent(); 29 | String path = ""; 30 | 31 | if (extraIntent != null) { 32 | path = extraIntent.getStringExtra(EXTRA_VIDEO_PATH); 33 | } 34 | 35 | mVideoTrimmer = ((DeepVideoTrimmer) findViewById(R.id.timeLine)); 36 | timeLineBar = (RangeSeekBarView) findViewById(R.id.timeLineBar); 37 | textSize = (TextView) findViewById(R.id.textSize); 38 | tvCroppingMessage = (TextView) findViewById(R.id.tvCroppingMessage); 39 | 40 | if (mVideoTrimmer != null && path != null) { 41 | mVideoTrimmer.setMaxDuration(100); 42 | mVideoTrimmer.setOnTrimVideoListener(this); 43 | mVideoTrimmer.setVideoURI(Uri.parse(path)); 44 | } else { 45 | showToastLong(getString(R.string.toast_cannot_retrieve_selected_video)); 46 | } 47 | } 48 | 49 | @Override 50 | public void getResult(final Uri uri) { 51 | runOnUiThread(new Runnable() { 52 | @Override 53 | public void run() { 54 | tvCroppingMessage.setVisibility(View.GONE); 55 | } 56 | }); 57 | Constants.croppedVideoURI = uri.toString(); 58 | Intent intent = new Intent(); 59 | intent.setData(uri); 60 | setResult(RESULT_OK, intent); 61 | finish(); 62 | 63 | } 64 | 65 | @Override 66 | public void cancelAction() { 67 | mVideoTrimmer.destroy(); 68 | runOnUiThread(new Runnable() { 69 | @Override 70 | public void run() { 71 | tvCroppingMessage.setVisibility(View.GONE); 72 | } 73 | }); 74 | finish(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_vector_arrow_back_black.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_camera_bottomsheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_gallery_bottomsheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/v_ic_camera.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 42 | 45 | 48 | 51 | 54 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/v_ic_gallery.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 21 | 24 | 29 | 32 | 37 | 40 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 17 | 18 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_video_trimmer.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_video_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 33 | 34 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #FFF 7 | #80000000 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoTrimmerExample 3 | Select Video 4 | Capture using camera 5 | Gallery 6 | Need permission 7 | Settings 8 | Critical Permission Denied 9 | Allow 10 | We need Gallery and Camera permissions to have video selection 11 | Can not retrieve selected video 12 | Cropping Video Please Wait 13 | Thumb Image for cropped video: 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/path.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.2' 11 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 12 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 04 12:03:53 IST 2018 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-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/device-2018-06-06-170411.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/images/device-2018-06-06-170411.png -------------------------------------------------------------------------------- /images/device-2018-06-06-170642.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/images/device-2018-06-06-170642.png -------------------------------------------------------------------------------- /images/device-2018-06-06-170717.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/images/device-2018-06-06-170717.png -------------------------------------------------------------------------------- /images/device-2018-06-06-170736.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/images/device-2018-06-06-170736.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':videotrimmer' -------------------------------------------------------------------------------- /videotrimmer/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /videotrimmer/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | ext { 3 | bintrayUserOrg = 'deeppatel' 4 | bintrayRepo = 'videotrimmer' 5 | bintrayName = 'videotrimmer' 6 | 7 | publishedGroupId = 'com.deep.videotrimmer' 8 | libraryName = 'videotrimmer' 9 | artifact = 'videotrimmer' 10 | 11 | libraryDescription = 'Whatsapp like video trimmer for android' 12 | 13 | siteUrl = 'https://github.com/deepandroid/videotrimmer' 14 | gitUrl = 'https://github.com/deepandroid/videotrimmer.git' 15 | 16 | libraryVersion = '1.0.1' 17 | 18 | developerId = 'deeppatel' 19 | developerName = 'Deep Patel' 20 | developerEmail = 'deep.1304@yahoo.com' 21 | 22 | licenseName = 'The Apache Software License, Version 2.0' 23 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | allLicenses = ["Apache-2.0"] 25 | } 26 | android { 27 | compileSdkVersion 27 28 | defaultConfig { 29 | minSdkVersion 16 30 | targetSdkVersion 27 31 | versionCode 1 32 | versionName "1.0.1" 33 | vectorDrawables.useSupportLibrary = true 34 | } 35 | buildTypes { 36 | release { 37 | minifyEnabled false 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | } 41 | } 42 | dependencies { 43 | implementation 'com.android.support:appcompat-v7:27.1.1' 44 | implementation 'com.googlecode.mp4parser:isoparser:1.1.20' 45 | } 46 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 47 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 48 | 49 | android.libraryVariants.findAll { variant -> variant.name == 'Release' } each { variant -> 50 | task("generate${variant.name}Javadoc", type: Javadoc) { 51 | description "Generates Javadoc for $variant.name." 52 | source = variant.javaCompile.source 53 | ext.androidJar = "${android.plugin.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" 54 | classpath = files(variant.javaCompile.classpath.files) + files(ext.androidJar) 55 | } 56 | 57 | task("bundle${variant.name}Javadoc", type: Zip) { 58 | description "Bundles Javadoc into zip for $variant.name." 59 | classifier = "javadoc" 60 | from tasks["generate${variant.name}Javadoc"] 61 | } 62 | } -------------------------------------------------------------------------------- /videotrimmer/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /videotrimmer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/videotrimmer/DeepVideoTrimmer.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmer; 2 | 3 | import android.content.Context; 4 | import android.media.MediaMetadataRetriever; 5 | import android.media.MediaPlayer; 6 | import android.net.Uri; 7 | import android.os.Environment; 8 | import android.os.Handler; 9 | import android.os.Message; 10 | import android.support.annotation.NonNull; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.GestureDetector; 14 | import android.view.LayoutInflater; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.FrameLayout; 19 | import android.widget.ImageView; 20 | import android.widget.LinearLayout; 21 | import android.widget.RelativeLayout; 22 | import android.widget.SeekBar; 23 | import android.widget.TextView; 24 | import android.widget.Toast; 25 | import android.widget.VideoView; 26 | 27 | import com.deep.videotrimmer.interfaces.OnProgressVideoListener; 28 | import com.deep.videotrimmer.interfaces.OnRangeSeekBarListener; 29 | import com.deep.videotrimmer.interfaces.OnTrimVideoListener; 30 | import com.deep.videotrimmer.utils.BackgroundExecutor; 31 | import com.deep.videotrimmer.utils.TrimVideoUtils; 32 | import com.deep.videotrimmer.utils.UiThreadExecutor; 33 | import com.deep.videotrimmer.view.ProgressBarView; 34 | import com.deep.videotrimmer.view.RangeSeekBarView; 35 | import com.deep.videotrimmer.view.TimeLineView; 36 | 37 | import java.io.File; 38 | import java.lang.ref.WeakReference; 39 | import java.util.ArrayList; 40 | import java.util.Formatter; 41 | import java.util.List; 42 | 43 | public class DeepVideoTrimmer extends FrameLayout implements MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, 44 | SeekBar.OnSeekBarChangeListener, OnRangeSeekBarListener, OnProgressVideoListener { 45 | 46 | private static final String TAG = DeepVideoTrimmer.class.getSimpleName(); 47 | private static final int MIN_TIME_FRAME = 1000; 48 | 49 | private SeekBar mHolderTopView; 50 | private RangeSeekBarView mRangeSeekBarView; 51 | private RelativeLayout mLinearVideo; 52 | private VideoView mVideoView; 53 | private ImageView mPlayView; 54 | private TextView mTextSize; 55 | private TextView mTextTimeFrame; 56 | private TextView mTextTime; 57 | private TimeLineView mTimeLineView; 58 | 59 | private Uri mSrc; 60 | private String mFinalPath; 61 | 62 | private int mMaxDuration; 63 | private List mListeners; 64 | private OnTrimVideoListener mOnTrimVideoListener; 65 | 66 | private int mDuration = 0; 67 | private int maxFileSize= 25; 68 | private int mTimeVideo = 0; 69 | private int mStartPosition = 0; 70 | private int mEndPosition = 0; 71 | private long mOriginSizeFile; 72 | private boolean mResetSeekBar = true; 73 | @NonNull 74 | private final MessageHandler mMessageHandler = new MessageHandler(this); 75 | private static final int SHOW_PROGRESS = 2; 76 | private boolean letUserProceed; 77 | private GestureDetector mGestureDetector; 78 | private int initialLength; 79 | @NonNull 80 | private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { 81 | @Override 82 | public boolean onSingleTapConfirmed(MotionEvent e) { 83 | if (mVideoView.isPlaying()) { 84 | mPlayView.setVisibility(View.VISIBLE); 85 | mMessageHandler.removeMessages(SHOW_PROGRESS); 86 | mVideoView.pause(); 87 | } else { 88 | mPlayView.setVisibility(View.GONE); 89 | 90 | if (mResetSeekBar) { 91 | mResetSeekBar = false; 92 | mVideoView.seekTo(mStartPosition); 93 | } 94 | 95 | mMessageHandler.sendEmptyMessage(SHOW_PROGRESS); 96 | mVideoView.start(); 97 | } 98 | return true; 99 | } 100 | }; 101 | 102 | @NonNull 103 | private final View.OnTouchListener mTouchListener = new View.OnTouchListener() { 104 | @Override 105 | public boolean onTouch(View v, @NonNull MotionEvent event) { 106 | mGestureDetector.onTouchEvent(event); 107 | return true; 108 | } 109 | }; 110 | 111 | public DeepVideoTrimmer(@NonNull Context context, AttributeSet attrs) { 112 | this(context, attrs, 0); 113 | } 114 | 115 | public DeepVideoTrimmer(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 116 | super(context, attrs, defStyleAttr); 117 | init(context); 118 | } 119 | 120 | private void init(Context context) { 121 | 122 | LayoutInflater.from(context).inflate(R.layout.view_time_line, this, true); 123 | 124 | mHolderTopView = findViewById(R.id.handlerTop); 125 | ProgressBarView progressVideoView = findViewById(R.id.timeVideoView); 126 | mRangeSeekBarView = findViewById(R.id.timeLineBar); 127 | mLinearVideo = findViewById(R.id.layout_surface_view); 128 | mVideoView = findViewById(R.id.video_loader); 129 | mPlayView = findViewById(R.id.icon_video_play); 130 | mTextSize = findViewById(R.id.textSize); 131 | mTextTimeFrame = findViewById(R.id.textTimeSelection); 132 | mTextTime = findViewById(R.id.textTime); 133 | mTimeLineView = findViewById(R.id.timeLineView); 134 | View viewButtonCancel = findViewById(R.id.btCancel); 135 | View viewButtonSave = findViewById(R.id.btSave); 136 | 137 | if (viewButtonCancel != null) { 138 | viewButtonCancel.setOnClickListener(new OnClickListener() { 139 | @Override 140 | public void onClick(View view) { 141 | mOnTrimVideoListener.cancelAction(); 142 | } 143 | } 144 | ); 145 | } 146 | 147 | if (viewButtonSave != null) { 148 | viewButtonSave.setOnClickListener(new OnClickListener() { 149 | @Override 150 | public void onClick(View view) { 151 | 152 | if (letUserProceed) { 153 | if (mStartPosition <= 0 && mEndPosition >= mDuration) { 154 | mOnTrimVideoListener.getResult(mSrc); 155 | } else { 156 | mPlayView.setVisibility(View.VISIBLE); 157 | mVideoView.pause(); 158 | 159 | MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); 160 | mediaMetadataRetriever.setDataSource(getContext(), mSrc); 161 | long METADATA_KEY_DURATION = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); 162 | 163 | File file = new File(mSrc.getPath()); 164 | 165 | if (mTimeVideo < MIN_TIME_FRAME) { 166 | 167 | if ((METADATA_KEY_DURATION - mEndPosition) > (MIN_TIME_FRAME - mTimeVideo)) { 168 | mEndPosition += (MIN_TIME_FRAME - mTimeVideo); 169 | } else if (mStartPosition > (MIN_TIME_FRAME - mTimeVideo)) { 170 | mStartPosition -= (MIN_TIME_FRAME - mTimeVideo); 171 | } 172 | } 173 | startTrimVideo(file, mFinalPath, mStartPosition, mEndPosition, mOnTrimVideoListener); 174 | } 175 | } else { 176 | Toast.makeText(getContext(), "Please trim your video less than 25MB of size", Toast.LENGTH_SHORT).show(); 177 | } 178 | 179 | } 180 | } 181 | ); 182 | } 183 | 184 | mListeners = new ArrayList<>(); 185 | mListeners.add(this); 186 | mListeners.add(progressVideoView); 187 | 188 | mHolderTopView.setMax(1000); 189 | mHolderTopView.setSecondaryProgress(0); 190 | 191 | mRangeSeekBarView.addOnRangeSeekBarListener(this); 192 | mRangeSeekBarView.addOnRangeSeekBarListener(progressVideoView); 193 | 194 | int marge = mRangeSeekBarView.getThumbs().get(0).getWidthBitmap(); 195 | int widthSeek = mHolderTopView.getThumb().getMinimumWidth() / 2; 196 | 197 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mHolderTopView.getLayoutParams(); 198 | lp.setMargins(marge - widthSeek, 0, marge - widthSeek, 0); 199 | mHolderTopView.setLayoutParams(lp); 200 | 201 | lp = (LinearLayout.LayoutParams) mTimeLineView.getLayoutParams(); 202 | lp.setMargins(marge, 0, marge, 0); 203 | mTimeLineView.setLayoutParams(lp); 204 | 205 | lp = (LinearLayout.LayoutParams) progressVideoView.getLayoutParams(); 206 | lp.setMargins(marge, 0, marge, 0); 207 | progressVideoView.setLayoutParams(lp); 208 | 209 | mHolderTopView.setOnSeekBarChangeListener(this); 210 | 211 | mVideoView.setOnPreparedListener(this); 212 | mVideoView.setOnCompletionListener(this); 213 | mVideoView.setOnErrorListener(this); 214 | 215 | mGestureDetector = new GestureDetector(getContext(), mGestureListener); 216 | mVideoView.setOnTouchListener(mTouchListener); 217 | 218 | setDefaultDestinationPath(); 219 | } 220 | 221 | @SuppressWarnings("unused") 222 | public void setVideoURI(final Uri videoURI) { 223 | mSrc = videoURI; 224 | 225 | getSizeFile(false); 226 | 227 | mVideoView.setVideoURI(mSrc); 228 | mVideoView.requestFocus(); 229 | 230 | mTimeLineView.setVideo(mSrc); 231 | } 232 | 233 | @SuppressWarnings("unused") 234 | public void setDestinationPath(final String finalPath) { 235 | mFinalPath = finalPath; 236 | Log.d(TAG, "Setting custom path " + mFinalPath); 237 | } 238 | 239 | private void setDefaultDestinationPath() { 240 | File folder = Environment.getExternalStorageDirectory(); 241 | mFinalPath = folder.getPath() + File.separator; 242 | Log.d(TAG, "Setting default path " + mFinalPath); 243 | } 244 | 245 | @Override 246 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 247 | int duration = (int) ((mDuration * progress) / 1000L); 248 | 249 | if (fromUser) { 250 | if (duration < mStartPosition) { 251 | setProgressBarPosition(mStartPosition); 252 | duration = mStartPosition; 253 | } else if (duration > mEndPosition) { 254 | setProgressBarPosition(mEndPosition); 255 | duration = mEndPosition; 256 | } 257 | setTimeVideo(duration); 258 | } 259 | } 260 | 261 | @Override 262 | public void onStartTrackingTouch(SeekBar seekBar) { 263 | mMessageHandler.removeMessages(SHOW_PROGRESS); 264 | mVideoView.pause(); 265 | mPlayView.setVisibility(View.VISIBLE); 266 | updateProgress(false); 267 | } 268 | 269 | @Override 270 | public void onStopTrackingTouch(@NonNull SeekBar seekBar) { 271 | mMessageHandler.removeMessages(SHOW_PROGRESS); 272 | mVideoView.pause(); 273 | mPlayView.setVisibility(View.VISIBLE); 274 | 275 | int duration = (int) ((mDuration * seekBar.getProgress()) / 1000L); 276 | mVideoView.seekTo(duration); 277 | setTimeVideo(duration); 278 | updateProgress(false); 279 | } 280 | 281 | @Override 282 | public void onPrepared(@NonNull MediaPlayer mp) { 283 | /* Adjust the size of the video 284 | so it fits on the screen*/ 285 | int videoWidth = mp.getVideoWidth(); 286 | int videoHeight = mp.getVideoHeight(); 287 | float videoProportion = (float) videoWidth / (float) videoHeight; 288 | int screenWidth = mLinearVideo.getWidth(); 289 | int screenHeight = mLinearVideo.getHeight(); 290 | float screenProportion = (float) screenWidth / (float) screenHeight; 291 | ViewGroup.LayoutParams lp = mVideoView.getLayoutParams(); 292 | 293 | if (videoProportion > screenProportion) { 294 | lp.width = screenWidth; 295 | lp.height = (int) ((float) screenWidth / videoProportion); 296 | } else { 297 | lp.width = (int) (videoProportion * (float) screenHeight); 298 | lp.height = screenHeight; 299 | } 300 | mVideoView.setLayoutParams(lp); 301 | 302 | mPlayView.setVisibility(View.VISIBLE); 303 | 304 | mDuration = mVideoView.getDuration(); 305 | setSeekBarPosition(); 306 | getSizeFile(false); 307 | setTimeFrames(); 308 | setTimeVideo(0); 309 | letUserProceed = getCroppedFileSize() < maxFileSize; 310 | } 311 | 312 | public int getMaxFileSize() { 313 | return maxFileSize; 314 | } 315 | 316 | public void setMaxFileSize(int maxFileSize) { 317 | this.maxFileSize = maxFileSize; 318 | } 319 | 320 | private void setSeekBarPosition() { 321 | 322 | if (mDuration >= mMaxDuration) { 323 | mStartPosition = mDuration / 2 - mMaxDuration / 2; 324 | mEndPosition = mDuration / 2 + mMaxDuration / 2; 325 | 326 | mRangeSeekBarView.setThumbValue(0, (mStartPosition * 100) / mDuration); 327 | mRangeSeekBarView.setThumbValue(1, (mEndPosition * 100) / mDuration); 328 | 329 | } else { 330 | mStartPosition = 0; 331 | mEndPosition = mDuration; 332 | } 333 | 334 | setProgressBarPosition(mStartPosition); 335 | mVideoView.seekTo(mStartPosition); 336 | 337 | mTimeVideo = mDuration; 338 | mRangeSeekBarView.initMaxWidth(); 339 | 340 | initialLength = ((mEndPosition - mStartPosition) / 1000); 341 | } 342 | 343 | private void startTrimVideo(@NonNull final File file, @NonNull final String dst, final int startVideo, final int endVideo, @NonNull final OnTrimVideoListener callback) { 344 | BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") { 345 | @Override 346 | public void execute() { 347 | try { 348 | TrimVideoUtils.startTrim(file, dst, startVideo, endVideo, callback); 349 | } catch (final Throwable e) { 350 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); 351 | } 352 | } 353 | } 354 | ); 355 | } 356 | 357 | private void setTimeFrames() { 358 | String seconds = getContext().getString(R.string.short_seconds); 359 | mTextTimeFrame.setText(String.format("%s %s - %s %s", stringForTime(mStartPosition), seconds, stringForTime(mEndPosition), seconds)); 360 | } 361 | 362 | 363 | private void setTimeVideo(int position) { 364 | String seconds = getContext().getString(R.string.short_seconds); 365 | mTextTime.setText(String.format("%s %s", stringForTime(position), seconds)); 366 | } 367 | 368 | @Override 369 | public void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 370 | 371 | } 372 | 373 | @Override 374 | public void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 375 | /* 0 is Left selector 376 | 1 is right selector*/ 377 | switch (index) { 378 | case 0: { 379 | mStartPosition = (int) ((mDuration * value) / 100L); 380 | mVideoView.seekTo(mStartPosition); 381 | break; 382 | } 383 | case 1: { 384 | mEndPosition = (int) ((mDuration * value) / 100L); 385 | break; 386 | } 387 | } 388 | setProgressBarPosition(mStartPosition); 389 | 390 | setTimeFrames(); 391 | getSizeFile(true); 392 | mTimeVideo = mEndPosition - mStartPosition; 393 | letUserProceed = getCroppedFileSize() < maxFileSize; 394 | } 395 | 396 | @Override 397 | public void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 398 | 399 | } 400 | 401 | @Override 402 | public void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 403 | mMessageHandler.removeMessages(SHOW_PROGRESS); 404 | mVideoView.pause(); 405 | mPlayView.setVisibility(View.VISIBLE); 406 | } 407 | 408 | private String stringForTime(int timeMs) { 409 | int totalSeconds = timeMs / 1000; 410 | 411 | int seconds = totalSeconds % 60; 412 | int minutes = (totalSeconds / 60) % 60; 413 | int hours = totalSeconds / 3600; 414 | 415 | Formatter mFormatter = new Formatter(); 416 | if (hours > 0) { 417 | return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 418 | } else { 419 | return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 420 | } 421 | } 422 | 423 | private void getSizeFile(boolean isChanged) { 424 | if (isChanged) { 425 | long initSize = getFileSize(); 426 | long newSize; 427 | newSize = ((initSize / initialLength) * (mEndPosition - mStartPosition)); 428 | mTextSize.setText(String.format("%s %s", newSize / 1024, getContext().getString(R.string.megabyte))); 429 | } else { 430 | if (mOriginSizeFile == 0) { 431 | File file = new File(mSrc.getPath()); 432 | 433 | mOriginSizeFile = file.length(); 434 | long fileSizeInKB = mOriginSizeFile / 1024; 435 | 436 | if (fileSizeInKB > 1000) { 437 | long fileSizeInMB = fileSizeInKB / 1024; 438 | mTextSize.setText(String.format("%s %s", fileSizeInMB, getContext().getString(R.string.megabyte))); 439 | } else { 440 | mTextSize.setText(String.format("%s %s", fileSizeInKB, getContext().getString(R.string.kilobyte))); 441 | } 442 | } 443 | } 444 | } 445 | 446 | private long getFileSize() { 447 | File file = new File(mSrc.getPath()); 448 | mOriginSizeFile = file.length(); 449 | long fileSizeInKB = mOriginSizeFile / 1024; 450 | 451 | return fileSizeInKB / 1024; 452 | } 453 | 454 | private long getCroppedFileSize() { 455 | long initSize = getFileSize(); 456 | long newSize; 457 | newSize = ((initSize / initialLength) * (mEndPosition - mStartPosition)); 458 | return newSize / 1024; 459 | } 460 | 461 | @SuppressWarnings("unused") 462 | public void setOnTrimVideoListener(OnTrimVideoListener onTrimVideoListener) { 463 | mOnTrimVideoListener = onTrimVideoListener; 464 | } 465 | 466 | public void setMaxDuration(int maxDuration) { 467 | if (maxDuration == 0) { 468 | mMaxDuration = (mEndPosition - mStartPosition) * 1000; 469 | } else if (maxDuration < 0) { 470 | mMaxDuration = -maxDuration * 1000; 471 | } else { 472 | mMaxDuration = maxDuration * 1000; 473 | } 474 | } 475 | 476 | @Override 477 | public void onCompletion(MediaPlayer mediaPlayer) { 478 | mVideoView.seekTo(0); 479 | } 480 | 481 | @Override 482 | public boolean onError(MediaPlayer mediaPlayer, int i, int i1) { 483 | return false; 484 | } 485 | 486 | private static class MessageHandler extends Handler { 487 | 488 | @NonNull 489 | private final WeakReference mView; 490 | 491 | MessageHandler(DeepVideoTrimmer view) { 492 | mView = new WeakReference<>(view); 493 | } 494 | 495 | @Override 496 | public void handleMessage(Message msg) { 497 | DeepVideoTrimmer view = mView.get(); 498 | if (view == null || view.mVideoView == null) { 499 | return; 500 | } 501 | 502 | view.updateProgress(true); 503 | if (view.mVideoView.isPlaying()) { 504 | sendEmptyMessageDelayed(0, 10); 505 | } 506 | } 507 | } 508 | 509 | private void updateProgress(boolean all) { 510 | if (mDuration == 0) return; 511 | 512 | int position = mVideoView.getCurrentPosition(); 513 | if (all) { 514 | for (OnProgressVideoListener item : mListeners) { 515 | item.updateProgress(position, mDuration, ((position * 100) / mDuration)); 516 | } 517 | } else { 518 | mListeners.get(1).updateProgress(position, mDuration, ((position * 100) / mDuration)); 519 | } 520 | } 521 | 522 | @Override 523 | public void updateProgress(int time, int max, float scale) { 524 | if (mVideoView == null) { 525 | return; 526 | } 527 | 528 | if (time >= mEndPosition) { 529 | mMessageHandler.removeMessages(SHOW_PROGRESS); 530 | mVideoView.pause(); 531 | mPlayView.setVisibility(View.VISIBLE); 532 | mResetSeekBar = true; 533 | return; 534 | } 535 | 536 | if (mHolderTopView != null) { 537 | /*use long to avoid overflow*/ 538 | setProgressBarPosition(time); 539 | } 540 | setTimeVideo(time); 541 | } 542 | 543 | 544 | private void setProgressBarPosition(int position) { 545 | if (mDuration > 0) { 546 | long pos = 1000L * position / mDuration; 547 | mHolderTopView.setProgress((int) pos); 548 | } 549 | } 550 | 551 | public void destroy() { 552 | BackgroundExecutor.cancelAll("", true); 553 | UiThreadExecutor.cancelAll(""); 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/videotrimmer/interfaces/OnProgressVideoListener.java: -------------------------------------------------------------------------------- 1 | 2 | package com.deep.videotrimmer.interfaces; 3 | 4 | /** 5 | * Created by Deep Patel 6 | * (Sr. Android Developer) 7 | * on 6/4/2018 8 | */ 9 | 10 | public interface OnProgressVideoListener { 11 | 12 | void updateProgress(int time, int max, float scale); 13 | } 14 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/videotrimmer/interfaces/OnRangeSeekBarListener.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmer.interfaces; 2 | 3 | import com.deep.videotrimmer.view.RangeSeekBarView; 4 | 5 | /** 6 | * Created by Deep Patel 7 | * (Sr. Android Developer) 8 | * on 6/4/2018 9 | */ 10 | public interface OnRangeSeekBarListener { 11 | void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value); 12 | 13 | void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value); 14 | 15 | void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value); 16 | 17 | void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value); 18 | } 19 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/videotrimmer/interfaces/OnTrimVideoListener.java: -------------------------------------------------------------------------------- 1 | package com.deep.videotrimmer.interfaces; 2 | 3 | import android.net.Uri; 4 | 5 | /** 6 | * Created by Deep Patel 7 | * (Sr. Android Developer) 8 | * on 6/4/2018 9 | */ 10 | public interface OnTrimVideoListener { 11 | 12 | void getResult(final Uri uri); 13 | 14 | void cancelAction(); 15 | } 16 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 | * Created by Deep Patel 31 | * (Sr. Android Developer) 32 | * on 6/4/2018 33 | */ 34 | public final class BackgroundExecutor { 35 | 36 | private static final String TAG = "BackgroundExecutor"; 37 | 38 | public static final Executor DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()); 39 | private static Executor executor = DEFAULT_EXECUTOR; 40 | private static final List TASKS = new ArrayList<>(); 41 | private static final ThreadLocal CURRENT_SERIAL = new ThreadLocal<>(); 42 | 43 | private BackgroundExecutor() { 44 | } 45 | 46 | /** 47 | * Execute a runnable after the given delay. 48 | * 49 | * @param runnable the task to execute 50 | * @param delay the time from now to delay execution, in milliseconds 51 | * if delay is strictly positive and the current 52 | * executor does not support scheduling (if 53 | * Executor has been called with such an 54 | * executor) 55 | * @return Future associated to the running task 56 | * @throws IllegalArgumentException if the current executor set by Executor 57 | * does not support scheduling 58 | */ 59 | private static Future directExecute(Runnable runnable, long delay) { 60 | Future future = null; 61 | if (delay > 0) { 62 | /* no serial, but a delay: schedule the task */ 63 | if (!(executor instanceof ScheduledExecutorService)) { 64 | throw new IllegalArgumentException("The executor set does not support scheduling"); 65 | } 66 | ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor; 67 | future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS); 68 | } else { 69 | if (executor instanceof ExecutorService) { 70 | ExecutorService executorService = (ExecutorService) executor; 71 | future = executorService.submit(runnable); 72 | } else { 73 | /* non-cancellable task */ 74 | executor.execute(runnable); 75 | } 76 | } 77 | return future; 78 | } 79 | 80 | /** 81 | * Execute a task after (at least) its delay and after all 82 | * tasks added with the same non-null serial(if any) have 83 | * completed execution. 84 | * 85 | * @param task the task to execute 86 | * @throws IllegalArgumentException if task.delay is strictly positive and the 87 | * current executor does not support scheduling (if 88 | * Executor has been called with such an 89 | * executor) 90 | */ 91 | public static synchronized void execute(Task task) { 92 | Future future = null; 93 | if (task.serial == null || !hasSerialRunning(task.serial)) { 94 | task.executionAsked = true; 95 | future = directExecute(task, task.remainingDelay); 96 | } 97 | if ((task.id != null || task.serial != null) && !task.managed.get()) { 98 | /* keep task */ 99 | task.future = future; 100 | TASKS.add(task); 101 | } 102 | } 103 | 104 | /** 105 | * Indicates whether a task with the specified serial has been 106 | * submitted to the executor. 107 | * 108 | * @param serial the serial queue 109 | * @return true if such a task has been submitted, 110 | * falseotherwise 111 | */ 112 | private static boolean hasSerialRunning(String serial) { 113 | for (Task task : TASKS) { 114 | if (task.executionAsked && serial.equals(task.serial)) { 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | /** 122 | * Retrieve and remove the first task having the specified 123 | * serial (if any). 124 | * 125 | * @param serial the serial queue 126 | * @return task if found, null otherwise 127 | */ 128 | private static Task take(String serial) { 129 | int len = TASKS.size(); 130 | for (int i = 0; i < len; i++) { 131 | if (serial.equals(TASKS.get(i).serial)) { 132 | return TASKS.remove(i); 133 | } 134 | } 135 | return null; 136 | } 137 | 138 | /** 139 | * Cancel all tasks having the specified id 140 | * 141 | * @param id the cancellation identifier 142 | * @param mayInterruptIfRunning true if the thread executing this task should be 143 | * interrupted; otherwise, in-progress tasks are allowed to 144 | * complete 145 | */ 146 | public static synchronized void cancelAll(String id, boolean mayInterruptIfRunning) { 147 | for (int i = TASKS.size() - 1; i >= 0; i--) { 148 | Task task = TASKS.get(i); 149 | if (id.equals(task.id)) { 150 | if (task.future != null) { 151 | task.future.cancel(mayInterruptIfRunning); 152 | if (!task.managed.getAndSet(true)) { 153 | /* 154 | * the task has been submitted to the executor, but its 155 | * execution has not started yet, so that its run() 156 | * method will never call postExecute() 157 | */ 158 | task.postExecute(); 159 | } 160 | } else if (task.executionAsked) { 161 | Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)"); 162 | } else { 163 | /* this task has not been submitted to the executor */ 164 | TASKS.remove(i); 165 | } 166 | } 167 | } 168 | } 169 | 170 | public static abstract class Task implements Runnable { 171 | 172 | private String id; 173 | private long remainingDelay; 174 | private long targetTimeMillis; /* since epoch */ 175 | private String serial; 176 | private boolean executionAsked; 177 | private Future future; 178 | 179 | /* 180 | * A task can be cancelled after it has been submitted to the executor 181 | * but before its run() method is called. In that case, run() will never 182 | * be called, hence neither will postExecute(): the tasks with the same 183 | * serial identifier (if any) will never be submitted. 184 | * 185 | * Therefore, cancelAll() *must* call postExecute() if run() is not 186 | * started. 187 | * 188 | * This flag guarantees that either cancelAll() or run() manages this 189 | * task post execution, but not both. 190 | */ 191 | private AtomicBoolean managed = new AtomicBoolean(); 192 | 193 | public Task(String id, long delay, String serial) { 194 | if (!"".equals(id)) { 195 | this.id = id; 196 | } 197 | if (delay > 0) { 198 | remainingDelay = delay; 199 | targetTimeMillis = System.currentTimeMillis() + delay; 200 | } 201 | if (!"".equals(serial)) { 202 | this.serial = serial; 203 | } 204 | } 205 | 206 | @Override 207 | public void run() { 208 | if (managed.getAndSet(true)) { 209 | /* cancelled and postExecute() already called */ 210 | return; 211 | } 212 | 213 | try { 214 | CURRENT_SERIAL.set(serial); 215 | execute(); 216 | } finally { 217 | /* handle next tasks */ 218 | postExecute(); 219 | } 220 | } 221 | 222 | public abstract void execute(); 223 | 224 | private void postExecute() { 225 | if (id == null && serial == null) { 226 | /* nothing to do */ 227 | return; 228 | } 229 | CURRENT_SERIAL.set(null); 230 | synchronized (BackgroundExecutor.class) { 231 | /* execution complete */ 232 | TASKS.remove(this); 233 | 234 | if (serial != null) { 235 | Task next = take(serial); 236 | if (next != null) { 237 | if (next.remainingDelay != 0) { 238 | /* the delay may not have elapsed yet */ 239 | next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis()); 240 | } 241 | /* a task having the same serial was queued, execute it */ 242 | BackgroundExecutor.execute(next); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 | * Created by Deep Patel 38 | * (Sr. Android Developer) 39 | * on 6/4/2018 40 | */ 41 | public class FileUtils { 42 | 43 | /** 44 | * Get a file path from a Uri. This will get the the path for Storage Access 45 | * Framework Documents, as well as the _data field for the MediaStore and 46 | * other file-based ContentProviders.
47 | *
48 | * Callers should check whether the path is local before assuming it 49 | * represents a local file. 50 | * 51 | * @param context The context. 52 | * @param uri The Uri to query. 53 | * @author Deep Patel 54 | */ 55 | @SuppressLint("NewApi") 56 | public static String getPath(final Context context, final Uri uri) { 57 | 58 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 59 | 60 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 61 | if (isExternalStorageDocument(uri)) { 62 | final String docId = DocumentsContract.getDocumentId(uri); 63 | final String[] split = docId.split(":"); 64 | final String type = split[0]; 65 | 66 | if ("primary".equalsIgnoreCase(type)) { 67 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 68 | } 69 | 70 | } 71 | else if (isDownloadsDocument(uri)) { 72 | 73 | final String id = DocumentsContract.getDocumentId(uri); 74 | final Uri contentUri = ContentUris.withAppendedId( 75 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 76 | 77 | return getDataColumn(context, contentUri, null, null); 78 | } 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 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 102 | 103 | if (isGooglePhotosUri(uri)) 104 | return uri.getLastPathSegment(); 105 | 106 | return getDataColumn(context, uri, null, null); 107 | } 108 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 109 | return uri.getPath(); 110 | } 111 | 112 | return null; 113 | } 114 | 115 | /** 116 | * Get the value of the data column for this Uri. This is useful for 117 | * MediaStore Uris, and other file-based ContentProviders. 118 | * 119 | * @param context The context. 120 | * @param uri The Uri to query. 121 | * @param selection (Optional) Filter used in the query. 122 | * @param selectionArgs (Optional) Selection arguments used in the query. 123 | * @return The value of the _data column, which is typically a file path. 124 | * @author Deep Patel 125 | */ 126 | private static String getDataColumn(@NonNull Context context, Uri uri, String selection, String[] selectionArgs) { 127 | 128 | Cursor cursor = null; 129 | final String column = "_data"; 130 | final String[] projection = {column}; 131 | 132 | try { 133 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); 134 | if (cursor != null && cursor.moveToFirst()) { 135 | final int column_index = cursor.getColumnIndexOrThrow(column); 136 | return cursor.getString(column_index); 137 | } 138 | } finally { 139 | if (cursor != null) 140 | cursor.close(); 141 | } 142 | return null; 143 | } 144 | 145 | /** 146 | * @param uri The Uri to check. 147 | * @return Whether the Uri authority is Google Photos. 148 | */ 149 | private static boolean isGooglePhotosUri(Uri uri) { 150 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 151 | } 152 | 153 | /** 154 | * @param uri The Uri to check. 155 | * @return Whether the Uri authority is ExternalStorageProvider. 156 | */ 157 | private static boolean isExternalStorageDocument(@NonNull Uri uri) { 158 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 159 | } 160 | 161 | /** 162 | * @param uri The Uri to check. 163 | * @return Whether the Uri authority is DownloadsProvider. 164 | */ 165 | private static boolean isDownloadsDocument(@NonNull Uri uri) { 166 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 167 | } 168 | 169 | /** 170 | * @param uri The Uri to check. 171 | * @return Whether the Uri authority is MediaProvider. 172 | */ 173 | private static boolean isMediaDocument(@NonNull Uri uri) { 174 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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.deep.videotrimmer.interfaces.OnTrimVideoListener; 32 | import com.googlecode.mp4parser.FileDataSourceViaHeapImpl; 33 | import com.googlecode.mp4parser.authoring.Movie; 34 | import com.googlecode.mp4parser.authoring.Track; 35 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; 36 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; 37 | import com.googlecode.mp4parser.authoring.tracks.AppendTrack; 38 | import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; 39 | 40 | import java.io.File; 41 | import java.io.FileOutputStream; 42 | import java.io.IOException; 43 | import java.nio.channels.FileChannel; 44 | import java.text.SimpleDateFormat; 45 | import java.util.Arrays; 46 | import java.util.Date; 47 | import java.util.LinkedList; 48 | import java.util.List; 49 | import java.util.Locale; 50 | /** 51 | * Created by Deep Patel 52 | * (Sr. Android Developer) 53 | * on 6/4/2018 54 | */ 55 | public class TrimVideoUtils { 56 | 57 | private static final String TAG = TrimVideoUtils.class.getSimpleName(); 58 | 59 | @SuppressWarnings("ResultOfMethodCallIgnored") 60 | public static void startTrim(@NonNull File src, @NonNull String dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException { 61 | final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); 62 | final String fileName = "MP4_" + timeStamp + ".mp4"; 63 | final String filePath = dst + fileName; 64 | 65 | File file = new File(filePath); 66 | file.getParentFile().mkdirs(); 67 | Log.d(TAG, "Generated file path " + filePath); 68 | genVideoUsingMp4Parser(src, file, startMs, endMs, callback); 69 | } 70 | 71 | @SuppressWarnings("ResultOfMethodCallIgnored") 72 | private static void genVideoUsingMp4Parser(@NonNull File src, @NonNull File dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException { 73 | 74 | Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(src.getAbsolutePath())); 75 | 76 | List tracks = movie.getTracks(); 77 | movie.setTracks(new LinkedList()); 78 | 79 | double startTime1 = startMs / 1000; 80 | double endTime1 = endMs / 1000; 81 | 82 | boolean timeCorrected = false; 83 | 84 | for (Track track : tracks) { 85 | if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { 86 | if (timeCorrected) { 87 | /* This exception here could be a false positive in case we have multiple tracks 88 | with sync samples at exactly the same positions. E.g. a single movie containing 89 | multiple qualities of the same video (Microsoft Smooth Streaming file)*/ 90 | 91 | throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported."); 92 | } 93 | startTime1 = correctTimeToSyncSample(track, startTime1, false); 94 | endTime1 = correctTimeToSyncSample(track, endTime1, true); 95 | timeCorrected = true; 96 | } 97 | } 98 | 99 | for (Track track : tracks) { 100 | long currentSample = 0; 101 | double currentTime = 0; 102 | double lastTime = -1; 103 | long startSample1 = -1; 104 | long endSample1 = -1; 105 | 106 | for (int i = 0; i < track.getSampleDurations().length; i++) { 107 | long delta = track.getSampleDurations()[i]; 108 | 109 | 110 | if (currentTime > lastTime && currentTime <= startTime1) { 111 | /*current sample is still before the new starttime*/ 112 | startSample1 = currentSample; 113 | } 114 | if (currentTime > lastTime && currentTime <= endTime1) { 115 | /* current sample is after the new start time and still before the new endtime*/ 116 | endSample1 = currentSample; 117 | } 118 | lastTime = currentTime; 119 | currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale(); 120 | currentSample++; 121 | } 122 | movie.addTrack(new AppendTrack(new CroppedTrack(track, startSample1, endSample1))); 123 | } 124 | 125 | dst.getParentFile().mkdirs(); 126 | 127 | if (!dst.exists()) { 128 | dst.createNewFile(); 129 | } 130 | 131 | Container out = new DefaultMp4Builder().build(movie); 132 | 133 | FileOutputStream fos = new FileOutputStream(dst); 134 | FileChannel fc = fos.getChannel(); 135 | out.writeContainer(fc); 136 | 137 | fc.close(); 138 | fos.close(); 139 | callback.getResult(Uri.parse(dst.toString())); 140 | } 141 | 142 | private static double correctTimeToSyncSample(@NonNull Track track, double cutHere, boolean next) { 143 | double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; 144 | long currentSample = 0; 145 | double currentTime = 0; 146 | for (int i = 0; i < track.getSampleDurations().length; i++) { 147 | long delta = track.getSampleDurations()[i]; 148 | 149 | if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { 150 | timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime; 151 | } 152 | currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale(); 153 | currentSample++; 154 | 155 | } 156 | double previous = 0; 157 | for (double timeOfSyncSample : timeOfSyncSamples) { 158 | if (timeOfSyncSample > cutHere) { 159 | if (next) { 160 | return timeOfSyncSample; 161 | } else { 162 | return previous; 163 | } 164 | } 165 | previous = timeOfSyncSample; 166 | } 167 | return timeOfSyncSamples[timeOfSyncSamples.length - 1]; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 | * Created by Deep Patel 27 | * (Sr. Android Developer) 28 | * on 6/4/2018 29 | * 30 | * This class provide operations for 31 | * UiThread tasks. 32 | */ 33 | public final class UiThreadExecutor { 34 | 35 | private static final Handler HANDLER = new Handler(Looper.getMainLooper()) { 36 | @Override 37 | public void handleMessage(Message msg) { 38 | Runnable callback = msg.getCallback(); 39 | if (callback != null) { 40 | callback.run(); 41 | decrementToken((Token) msg.obj); 42 | } else { 43 | super.handleMessage(msg); 44 | } 45 | } 46 | }; 47 | 48 | private static final Map TOKENS = new HashMap<>(); 49 | 50 | private UiThreadExecutor() { 51 | } 52 | 53 | /** 54 | * Store a new task in the map for providing cancellation. This method is 55 | * used by AndroidAnnotations and not intended to be called by clients. 56 | * 57 | * @param id the identifier of the task 58 | * @param task the task itself 59 | * @param delay the delay or zero to run immediately 60 | */ 61 | public static void runTask(String id, Runnable task, long delay) { 62 | if ("".equals(id)) { 63 | HANDLER.postDelayed(task, delay); 64 | return; 65 | } 66 | long time = SystemClock.uptimeMillis() + delay; 67 | HANDLER.postAtTime(task, nextToken(id), time); 68 | } 69 | 70 | private static Token nextToken(String id) { 71 | synchronized (TOKENS) { 72 | Token token = TOKENS.get(id); 73 | if (token == null) { 74 | token = new Token(id); 75 | TOKENS.put(id, token); 76 | } 77 | token.runnablesCount++; 78 | return token; 79 | } 80 | } 81 | 82 | private static void decrementToken(Token token) { 83 | synchronized (TOKENS) { 84 | if (--token.runnablesCount == 0) { 85 | String id = token.id; 86 | Token old = TOKENS.remove(id); 87 | if (old != token) { 88 | /* a runnable finished after cancelling, we just removed a 89 | wrong token, lets put it back*/ 90 | TOKENS.put(id, old); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Cancel all tasks having the specified id. 98 | * 99 | * @param id the cancellation identifier 100 | */ 101 | public static void cancelAll(String id) { 102 | Token token; 103 | synchronized (TOKENS) { 104 | token = TOKENS.remove(id); 105 | } 106 | if (token == null) { 107 | return; 108 | } 109 | HANDLER.removeCallbacksAndMessages(token); 110 | } 111 | 112 | private static final class Token { 113 | int runnablesCount = 0; 114 | final String id; 115 | 116 | private Token(String id) { 117 | this.id = id; 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 com.deep.videotrimmer.R; 36 | import com.deep.videotrimmer.interfaces.OnProgressVideoListener; 37 | import com.deep.videotrimmer.interfaces.OnRangeSeekBarListener; 38 | 39 | /** 40 | * Created by Deep Patel 41 | * (Sr. Android Developer) 42 | * on 6/4/2018 43 | */ 44 | 45 | public class ProgressBarView extends View implements OnRangeSeekBarListener, OnProgressVideoListener { 46 | 47 | private int mProgressHeight; 48 | private int mViewWidth; 49 | 50 | private final Paint mBackgroundColor = new Paint(); 51 | private final Paint mProgressColor = new Paint(); 52 | 53 | private Rect mBackgroundRect; 54 | private Rect mProgressRect; 55 | 56 | public ProgressBarView(@NonNull Context context, AttributeSet attrs) { 57 | this(context, attrs, 0); 58 | } 59 | 60 | public ProgressBarView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 61 | super(context, attrs, defStyleAttr); 62 | init(); 63 | } 64 | 65 | private void init() { 66 | int lineProgress = ContextCompat.getColor(getContext(), R.color.progress_color); 67 | int lineBackground = ContextCompat.getColor(getContext(), R.color.background_progress_color); 68 | 69 | mProgressHeight = getContext().getResources().getDimensionPixelOffset(R.dimen.progress_video_line_height); 70 | 71 | mBackgroundColor.setAntiAlias(true); 72 | mBackgroundColor.setColor(lineBackground); 73 | 74 | mProgressColor.setAntiAlias(true); 75 | mProgressColor.setColor(lineProgress); 76 | } 77 | 78 | @Override 79 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 80 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 81 | 82 | int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 83 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1); 84 | 85 | int minH = getPaddingBottom() + getPaddingTop() + mProgressHeight; 86 | int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1); 87 | 88 | setMeasuredDimension(mViewWidth, viewHeight); 89 | } 90 | 91 | @Override 92 | protected void onDraw(@NonNull Canvas canvas) { 93 | super.onDraw(canvas); 94 | 95 | drawLineBackground(canvas); 96 | drawLineProgress(canvas); 97 | } 98 | 99 | private void drawLineBackground(@NonNull Canvas canvas) { 100 | if (mBackgroundRect != null) { 101 | canvas.drawRect(mBackgroundRect, mBackgroundColor); 102 | } 103 | } 104 | 105 | private void drawLineProgress(@NonNull Canvas canvas) { 106 | if (mProgressRect != null) { 107 | canvas.drawRect(mProgressRect, mProgressColor); 108 | } 109 | } 110 | 111 | @Override 112 | public void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 113 | updateBackgroundRect(index, value); 114 | } 115 | 116 | @Override 117 | public void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 118 | updateBackgroundRect(index, value); 119 | } 120 | 121 | @Override 122 | public void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 123 | updateBackgroundRect(index, value); 124 | } 125 | 126 | @Override 127 | public void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 128 | updateBackgroundRect(index, value); 129 | } 130 | 131 | private void updateBackgroundRect(int index, float value) { 132 | 133 | if (mBackgroundRect == null) { 134 | mBackgroundRect = new Rect(0, 0, mViewWidth, mProgressHeight); 135 | } 136 | 137 | int newValue = (int) ((mViewWidth * value) / 100); 138 | if (index == 0) { 139 | mBackgroundRect = new Rect(newValue, mBackgroundRect.top, mBackgroundRect.right, mBackgroundRect.bottom); 140 | } else { 141 | mBackgroundRect = new Rect(mBackgroundRect.left, mBackgroundRect.top, newValue, mBackgroundRect.bottom); 142 | } 143 | 144 | updateProgress(0, 0, 0.0f); 145 | } 146 | 147 | @Override 148 | public void updateProgress(int time, int max, float scale) { 149 | 150 | if (scale == 0) { 151 | mProgressRect = new Rect(0, mBackgroundRect.top, 0, mBackgroundRect.bottom); 152 | } else { 153 | int newValue = (int) ((mViewWidth * scale) / 100); 154 | mProgressRect = new Rect(mBackgroundRect.left, mBackgroundRect.top, newValue, mBackgroundRect.bottom); 155 | } 156 | 157 | invalidate(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 com.deep.videotrimmer.R; 37 | import com.deep.videotrimmer.interfaces.OnRangeSeekBarListener; 38 | 39 | import java.util.ArrayList; 40 | import java.util.List; 41 | /** 42 | * Created by Deep Patel 43 | * (Sr. Android Developer) 44 | * on 6/4/2018 45 | */ 46 | public class RangeSeekBarView extends View { 47 | 48 | private static final String TAG = RangeSeekBarView.class.getSimpleName(); 49 | 50 | private int mHeightTimeLine; 51 | private List mThumbs; 52 | private List mListeners; 53 | private float mMaxWidth; 54 | private float mThumbWidth; 55 | private float mThumbHeight; 56 | private int mViewWidth; 57 | private float mPixelRangeMin; 58 | private float mPixelRangeMax; 59 | private float mScaleRangeMax; 60 | private boolean mFirstRun; 61 | 62 | private final Paint mShadow = new Paint(); 63 | private final Paint mLine = new Paint(); 64 | 65 | public RangeSeekBarView(@NonNull Context context, AttributeSet attrs) { 66 | this(context, attrs, 0); 67 | } 68 | 69 | public RangeSeekBarView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 70 | super(context, attrs, defStyleAttr); 71 | init(); 72 | } 73 | 74 | private void init() { 75 | mThumbs = Thumb.initThumbs(getResources()); 76 | mThumbWidth = Thumb.getWidthBitmap(mThumbs); 77 | mThumbHeight = Thumb.getHeightBitmap(mThumbs); 78 | 79 | mScaleRangeMax = 100; 80 | mHeightTimeLine = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); 81 | 82 | setFocusable(true); 83 | setFocusableInTouchMode(true); 84 | 85 | mFirstRun = true; 86 | 87 | int shadowColor = ContextCompat.getColor(getContext(), R.color.shadow_color); 88 | mShadow.setAntiAlias(true); 89 | mShadow.setColor(shadowColor); 90 | mShadow.setAlpha(177); 91 | 92 | int lineColor = ContextCompat.getColor(getContext(), R.color.line_color); 93 | mLine.setAntiAlias(true); 94 | mLine.setColor(lineColor); 95 | mLine.setAlpha(200); 96 | } 97 | 98 | public void initMaxWidth() { 99 | mMaxWidth = mThumbs.get(1).getPos() - mThumbs.get(0).getPos(); 100 | 101 | onSeekStop(this, 0, mThumbs.get(0).getVal()); 102 | onSeekStop(this, 1, mThumbs.get(1).getVal()); 103 | } 104 | 105 | @Override 106 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 107 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 108 | 109 | int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 110 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1); 111 | 112 | int minH = getPaddingBottom() + getPaddingTop() + (int) mThumbHeight + mHeightTimeLine; 113 | int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1); 114 | 115 | setMeasuredDimension(mViewWidth, viewHeight); 116 | 117 | mPixelRangeMin = 0; 118 | mPixelRangeMax = mViewWidth - mThumbWidth; 119 | 120 | if (mFirstRun) { 121 | for (int i = 0; i < mThumbs.size(); i++) { 122 | Thumb th = mThumbs.get(i); 123 | th.setVal(mScaleRangeMax * i); 124 | th.setPos(mPixelRangeMax * i); 125 | } 126 | onCreate(this, currentThumb, getThumbValue(currentThumb)); 127 | mFirstRun = false; 128 | } 129 | } 130 | 131 | @Override 132 | protected void onDraw(@NonNull Canvas canvas) { 133 | super.onDraw(canvas); 134 | 135 | drawShadow(canvas); 136 | drawThumbs(canvas); 137 | } 138 | 139 | private int currentThumb = 0; 140 | 141 | @Override 142 | public boolean onTouchEvent(@NonNull MotionEvent ev) { 143 | final Thumb mThumb; 144 | final Thumb mThumb2; 145 | final float coordinate = ev.getX(); 146 | final int action = ev.getAction(); 147 | 148 | switch (action) { 149 | case MotionEvent.ACTION_DOWN: { 150 | /*Remember where we started*/ 151 | currentThumb = getClosestThumb(coordinate); 152 | 153 | if (currentThumb == -1) { 154 | return false; 155 | } 156 | 157 | mThumb = mThumbs.get(currentThumb); 158 | mThumb.setLastTouchX(coordinate); 159 | onSeekStart(this, currentThumb, mThumb.getVal()); 160 | return true; 161 | } 162 | case MotionEvent.ACTION_UP: { 163 | 164 | if (currentThumb == -1) { 165 | return false; 166 | } 167 | 168 | mThumb = mThumbs.get(currentThumb); 169 | onSeekStop(this, currentThumb, mThumb.getVal()); 170 | return true; 171 | } 172 | 173 | case MotionEvent.ACTION_MOVE: { 174 | mThumb = mThumbs.get(currentThumb); 175 | mThumb2 = mThumbs.get(currentThumb == 0 ? 1 : 0); 176 | /* Calculate the distance moved*/ 177 | final float dx = coordinate - mThumb.getLastTouchX(); 178 | final float newX = mThumb.getPos() + dx; 179 | if (currentThumb == 0) { 180 | 181 | if ((newX + mThumb.getWidthBitmap()) >= mThumb2.getPos()) { 182 | mThumb.setPos(mThumb2.getPos() - mThumb.getWidthBitmap()); 183 | } else if (newX <= mPixelRangeMin) { 184 | mThumb.setPos(mPixelRangeMin); 185 | } else { 186 | /*Check if thumb is not out of max width*/ 187 | checkPositionThumb(mThumb, mThumb2, dx, true); 188 | /* Move the object*/ 189 | mThumb.setPos(mThumb.getPos() + dx); 190 | 191 | /* Remember this touch position for the next move event*/ 192 | mThumb.setLastTouchX(coordinate); 193 | } 194 | 195 | } else { 196 | if (newX <= mThumb2.getPos() + mThumb2.getWidthBitmap()) { 197 | mThumb.setPos(mThumb2.getPos() + mThumb.getWidthBitmap()); 198 | } else if (newX >= mPixelRangeMax) { 199 | mThumb.setPos(mPixelRangeMax); 200 | } else { 201 | /*Check if thumb is not out of max width*/ 202 | checkPositionThumb(mThumb2, mThumb, dx, false); 203 | /* Move the object*/ 204 | mThumb.setPos(mThumb.getPos() + dx); 205 | /* Remember this touch position for the next move event*/ 206 | mThumb.setLastTouchX(coordinate); 207 | } 208 | } 209 | 210 | setThumbPos(currentThumb, mThumb.getPos()); 211 | 212 | invalidate(); 213 | return true; 214 | } 215 | } 216 | return false; 217 | } 218 | 219 | private void checkPositionThumb(@NonNull Thumb mThumbLeft, @NonNull Thumb mThumbRight, float dx, boolean isLeftMove) { 220 | if (isLeftMove && dx < 0) { 221 | if ((mThumbRight.getPos() - (mThumbLeft.getPos() + dx)) > mMaxWidth) { 222 | mThumbRight.setPos(mThumbLeft.getPos() + dx + mMaxWidth); 223 | setThumbPos(1, mThumbRight.getPos()); 224 | } 225 | } else if (!isLeftMove && dx > 0) { 226 | if (((mThumbRight.getPos() + dx) - mThumbLeft.getPos()) > mMaxWidth) { 227 | mThumbLeft.setPos(mThumbRight.getPos() + dx - mMaxWidth); 228 | setThumbPos(0, mThumbLeft.getPos()); 229 | } 230 | } 231 | } 232 | 233 | private int getUnstuckFrom(int index) { 234 | int unstuck = 0; 235 | float lastVal = mThumbs.get(index).getVal(); 236 | for (int i = index - 1; i >= 0; i--) { 237 | Thumb th = mThumbs.get(i); 238 | if (th.getVal() != lastVal) 239 | return i + 1; 240 | } 241 | return unstuck; 242 | } 243 | 244 | private float pixelToScale(int index, float pixelValue) { 245 | float scale = (pixelValue * 100) / mPixelRangeMax; 246 | if (index == 0) { 247 | float pxThumb = (scale * mThumbWidth) / 100; 248 | return scale + (pxThumb * 100) / mPixelRangeMax; 249 | } else { 250 | float pxThumb = ((100 - scale) * mThumbWidth) / 100; 251 | return scale - (pxThumb * 100) / mPixelRangeMax; 252 | } 253 | } 254 | 255 | private float scaleToPixel(int index, float scaleValue) { 256 | float px = (scaleValue * mPixelRangeMax) / 100; 257 | if (index == 0) { 258 | float pxThumb = (scaleValue * mThumbWidth) / 100; 259 | return px - pxThumb; 260 | } else { 261 | float pxThumb = ((100 - scaleValue) * mThumbWidth) / 100; 262 | return px + pxThumb; 263 | } 264 | } 265 | 266 | private void calculateThumbValue(int index) { 267 | if (index < mThumbs.size() && !mThumbs.isEmpty()) { 268 | Thumb th = mThumbs.get(index); 269 | th.setVal(pixelToScale(index, th.getPos())); 270 | onSeek(this, index, th.getVal()); 271 | } 272 | } 273 | 274 | private void calculateThumbPos(int index) { 275 | if (index < mThumbs.size() && !mThumbs.isEmpty()) { 276 | Thumb th = mThumbs.get(index); 277 | th.setPos(scaleToPixel(index, th.getVal())); 278 | } 279 | } 280 | 281 | private float getThumbValue(int index) { 282 | return mThumbs.get(index).getVal(); 283 | } 284 | 285 | public void setThumbValue(int index, float value) { 286 | mThumbs.get(index).setVal(value); 287 | calculateThumbPos(index); 288 | invalidate(); 289 | } 290 | 291 | private void setThumbPos(int index, float pos) { 292 | mThumbs.get(index).setPos(pos); 293 | calculateThumbValue(index); 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 | final float tcoordinate = mThumbs.get(i).getPos() + mThumbWidth; 302 | if (coordinate >= mThumbs.get(i).getPos() && coordinate <= tcoordinate) { 303 | closest = mThumbs.get(i).getIndex(); 304 | } 305 | } 306 | } 307 | return closest; 308 | } 309 | 310 | private void drawShadow(@NonNull Canvas canvas) { 311 | if (!mThumbs.isEmpty()) { 312 | 313 | for (Thumb th : mThumbs) { 314 | if (th.getIndex() == 0) { 315 | final float x = th.getPos() + getPaddingLeft(); 316 | if (x > mPixelRangeMin) { 317 | Rect mRect = new Rect((int) mThumbWidth, 0, (int) (x + mThumbWidth), mHeightTimeLine); 318 | canvas.drawRect(mRect, mShadow); 319 | } 320 | } else { 321 | final float x = th.getPos() - getPaddingRight(); 322 | if (x < mPixelRangeMax) { 323 | Rect mRect = new Rect((int) x, 0, (int) (mViewWidth - mThumbWidth), mHeightTimeLine); 324 | canvas.drawRect(mRect, mShadow); 325 | } 326 | } 327 | } 328 | } 329 | } 330 | 331 | private void drawThumbs(@NonNull Canvas canvas) { 332 | 333 | if (!mThumbs.isEmpty()) { 334 | for (Thumb th : mThumbs) { 335 | if (th.getIndex() == 0) { 336 | canvas.drawBitmap(th.getBitmap(), th.getPos() + getPaddingLeft(), getPaddingTop() + mHeightTimeLine, null); 337 | } else { 338 | canvas.drawBitmap(th.getBitmap(), th.getPos() - getPaddingRight(), getPaddingTop() + mHeightTimeLine, null); 339 | } 340 | } 341 | } 342 | } 343 | 344 | public void addOnRangeSeekBarListener(OnRangeSeekBarListener listener) { 345 | 346 | if (mListeners == null) { 347 | mListeners = new ArrayList<>(); 348 | } 349 | 350 | mListeners.add(listener); 351 | } 352 | 353 | private void onCreate(RangeSeekBarView rangeSeekBarView, int index, float value) { 354 | if (mListeners == null) 355 | return; 356 | 357 | for (OnRangeSeekBarListener item : mListeners) { 358 | item.onCreate(rangeSeekBarView, index, value); 359 | } 360 | } 361 | 362 | private void onSeek(RangeSeekBarView rangeSeekBarView, int index, float value) { 363 | if (mListeners == null) 364 | return; 365 | 366 | for (OnRangeSeekBarListener item : mListeners) { 367 | item.onSeek(rangeSeekBarView, index, value); 368 | } 369 | } 370 | 371 | private void onSeekStart(RangeSeekBarView rangeSeekBarView, int index, float value) { 372 | if (mListeners == null) 373 | return; 374 | 375 | for (OnRangeSeekBarListener item : mListeners) { 376 | item.onSeekStart(rangeSeekBarView, index, value); 377 | } 378 | } 379 | 380 | private void onSeekStop(RangeSeekBarView rangeSeekBarView, int index, float value) { 381 | if (mListeners == null) 382 | return; 383 | 384 | for (OnRangeSeekBarListener item : mListeners) { 385 | item.onSeekStop(rangeSeekBarView, index, value); 386 | } 387 | } 388 | 389 | public List getThumbs() { 390 | return mThumbs; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 com.deep.videotrimmer.R; 32 | 33 | import java.util.List; 34 | import java.util.Vector; 35 | /** 36 | * Created by Deep Patel 37 | * (Sr. Android Developer) 38 | * on 6/4/2018 39 | */ 40 | public class Thumb { 41 | 42 | private int mIndex; 43 | private float mVal; 44 | private float mPos; 45 | private Bitmap mBitmap; 46 | private int mWidthBitmap; 47 | private int mHeightBitmap; 48 | 49 | private float mLastTouchX; 50 | 51 | private Thumb() { 52 | mVal = 0; 53 | mPos = 0; 54 | } 55 | 56 | public int getIndex() { 57 | return mIndex; 58 | } 59 | 60 | private void setIndex(int index) { 61 | mIndex = index; 62 | } 63 | 64 | public float getVal() { 65 | return mVal; 66 | } 67 | 68 | public void setVal(float val) { 69 | mVal = val; 70 | } 71 | 72 | public float getPos() { 73 | return mPos; 74 | } 75 | 76 | public void setPos(float pos) { 77 | mPos = pos; 78 | } 79 | 80 | public Bitmap getBitmap() { 81 | return mBitmap; 82 | } 83 | 84 | private void setBitmap(@NonNull Bitmap bitmap) { 85 | mBitmap = bitmap; 86 | mWidthBitmap = bitmap.getWidth(); 87 | mHeightBitmap = bitmap.getHeight(); 88 | } 89 | 90 | @NonNull 91 | public static List initThumbs(Resources resources) { 92 | 93 | List thumbs = new Vector<>(); 94 | 95 | for (int i = 0; i < 2; i++) { 96 | Thumb th = new Thumb(); 97 | th.setIndex(i); 98 | if (i == 0) { 99 | int resImageLeft = R.drawable.text_select_handle_left; 100 | th.setBitmap(BitmapFactory.decodeResource(resources, resImageLeft)); 101 | } else { 102 | int resImageRight = R.drawable.select_handle_right; 103 | th.setBitmap(BitmapFactory.decodeResource(resources, resImageRight)); 104 | } 105 | 106 | thumbs.add(th); 107 | } 108 | 109 | return thumbs; 110 | } 111 | 112 | public static int getWidthBitmap(@NonNull List thumbs) { 113 | return thumbs.get(0).getWidthBitmap(); 114 | } 115 | 116 | public static int getHeightBitmap(@NonNull List thumbs) { 117 | return thumbs.get(0).getHeightBitmap(); 118 | } 119 | 120 | public float getLastTouchX() { 121 | return mLastTouchX; 122 | } 123 | 124 | public void setLastTouchX(float lastTouchX) { 125 | mLastTouchX = lastTouchX; 126 | } 127 | 128 | public int getWidthBitmap() { 129 | return mWidthBitmap; 130 | } 131 | 132 | private int getHeightBitmap() { 133 | return mHeightBitmap; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /videotrimmer/src/main/java/com/deep/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 com.deep.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 com.deep.videotrimmer.R; 37 | import com.deep.videotrimmer.utils.BackgroundExecutor; 38 | import com.deep.videotrimmer.utils.UiThreadExecutor; 39 | 40 | import java.util.Set; 41 | 42 | /** 43 | * Created by Deep Patel 44 | * (Sr. Android Developer) 45 | * on 6/4/2018 46 | */ 47 | 48 | public class TimeLineView extends View { 49 | 50 | private Uri mVideoUri; 51 | private int mHeightView; 52 | private LongSparseArray mBitmapList = null; 53 | 54 | public TimeLineView(@NonNull Context context, AttributeSet attrs) { 55 | this(context, attrs, 0); 56 | } 57 | 58 | public TimeLineView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { 59 | super(context, attrs, defStyleAttr); 60 | init(); 61 | } 62 | 63 | private void init() { 64 | mHeightView = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); 65 | } 66 | 67 | @Override 68 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 69 | final int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 70 | int w = resolveSizeAndState(minW, widthMeasureSpec, 1); 71 | 72 | final int minH = getPaddingBottom() + getPaddingTop() + mHeightView; 73 | int h = resolveSizeAndState(minH, heightMeasureSpec, 1); 74 | 75 | setMeasuredDimension(w, h); 76 | } 77 | 78 | @Override 79 | protected void onSizeChanged(final int w, int h, final int oldW, int oldH) { 80 | super.onSizeChanged(w, h, oldW, oldH); 81 | 82 | if (w != oldW) { 83 | getBitmap(w); 84 | } 85 | } 86 | 87 | private void getBitmap(final int viewWidth) { 88 | BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") { 89 | @Override 90 | public void execute() { 91 | try { 92 | LongSparseArray thumbnailList = new LongSparseArray<>(); 93 | 94 | MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); 95 | mediaMetadataRetriever.setDataSource(getContext(), mVideoUri); 96 | 97 | /* Retrieve media data*/ 98 | long videoLengthInMs = Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000; 99 | 100 | /*Set thumbnail properties (Thumbs are squares)*/ 101 | final int thumbWidth = mHeightView; 102 | final int thumbHeight = mHeightView; 103 | 104 | int numThumbs = (int) Math.ceil(((float) viewWidth) / thumbWidth); 105 | 106 | final long interval = videoLengthInMs / numThumbs; 107 | 108 | for (int i = 0; i < numThumbs; ++i) { 109 | Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); 110 | try { 111 | bitmap = Bitmap.createScaledBitmap(bitmap, thumbWidth, thumbHeight, false); 112 | } catch (Exception e) { 113 | e.printStackTrace(); 114 | } 115 | thumbnailList.put(i, bitmap); 116 | } 117 | 118 | mediaMetadataRetriever.release(); 119 | returnBitmaps(thumbnailList); 120 | } catch (final Throwable e) { 121 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); 122 | } 123 | } 124 | } 125 | ); 126 | } 127 | 128 | private void returnBitmaps(final LongSparseArray thumbnailList) { 129 | UiThreadExecutor.runTask("", new Runnable() { 130 | @Override 131 | public void run() { 132 | mBitmapList = thumbnailList; 133 | invalidate(); 134 | } 135 | } 136 | , 0L); 137 | } 138 | 139 | @Override 140 | protected void onDraw(@NonNull Canvas canvas) { 141 | super.onDraw(canvas); 142 | 143 | if (mBitmapList != null) { 144 | canvas.save(); 145 | int x = 0; 146 | 147 | for (int i = 0; i < mBitmapList.size(); i++) { 148 | Bitmap bitmap = mBitmapList.get(i); 149 | 150 | if (bitmap != null) { 151 | canvas.drawBitmap(bitmap, x, 0, null); 152 | x = x + bitmap.getWidth(); 153 | } 154 | } 155 | } 156 | } 157 | 158 | public void setVideo(@NonNull Uri data) { 159 | mVideoUri = data; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable-hdpi/select_handle_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/videotrimmer/src/main/res/drawable-hdpi/select_handle_right.png -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable-hdpi/text_select_handle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/videotrimmer/src/main/res/drawable-hdpi/text_select_handle_left.png -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable-hdpi/text_select_handle_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepandroid/video-trimmer/8dbbd6d45e9c7a5825371dc005727b8e7de72d12/videotrimmer/src/main/res/drawable-hdpi/text_select_handle_middle.png -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable-v21/shape_black_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable/ic_vector_play_gray.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/drawable/shape_black_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /videotrimmer/src/main/res/layout/view_time_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 41 | 42 | 48 | 49 | 54 | 55 | 61 | 62 | 67 | 68 | 69 | 70 | 77 | 78 | 83 | 84 | 89 | 90 | 95 | 96 | 102 | 103 | 108 | 109 |