├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── VideoTrimmer.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── es │ │ └── anthorlop │ │ └── videotrimmer │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── es │ │ └── anthorlop │ │ └── videotrimmer │ │ ├── MainActivity.java │ │ ├── VideoRecorderedPlayerActivity.java │ │ ├── custom │ │ ├── RangeSeekBar.java │ │ └── VideoControllerView.java │ │ └── utils │ │ └── Screens.java │ └── res │ ├── drawable │ ├── ic_camara_menu_switch.png │ ├── ic_flash_off_white.png │ ├── ic_flash_on_white.png │ ├── ic_media_fullscreen_shrink.png │ ├── ic_media_fullscreen_stretch.png │ ├── ic_media_pause.png │ ├── ic_media_play.png │ ├── ic_player_big_pause.png │ ├── ic_player_big_play.png │ └── seek_thumb.png │ ├── layout │ ├── activity_main.xml │ ├── activity_video_player.xml │ └── media_controller.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screen_landscape.png ├── screen_portrait.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | VideoTrimmer -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoTrimmer 2 | 3 |

Select, preview and edit a video file

4 |

5 | Apache License 2.0

6 |

Native Android app for edit video files 7 |

    8 |
  1. IDE: Latest version of Android Studio 1.2.2 for MAC OS X
  2. 9 |
  3. Other tools:
      10 |
    1. Adobe Illustrator
    2. 11 |
    3. Charles web debugging proxy
    4. 12 |
    13 |
  4. 14 |
  5. Devices tested:
      15 |
    1. Sony Xperia Z2
    2. 16 |
    3. Samsung Galaxy S4
    4. 17 |
    5. Samsung Galaxy S3
    6. 18 |
    7. Nexus 10
    8. 19 |
    9. Nexus 7
    10. 20 |
    21 |
  6. 22 |
  7. Warnings: 0
  8. 23 |
  9. Fragmentation: minSDKversion = 14 (Android 4.0 ICE_CREAM_SANDWICH), so can run in around 99.5 % of existing Android devices, like the Android documentation show (https://developer.android.com/about/dashboards/index.html)
  10. 24 |
25 | 26 |
27 |
28 |

Screenshots

29 |

Mobile portrait

30 | 31 | 32 | 33 |

Mobile landscape

34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /VideoTrimmer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "es.anthorlop.videotrimmer" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | compile 'com.android.support:appcompat-v7:22.2.0' 26 | 27 | compile 'com.jakewharton:butterknife:6.1.0' 28 | 29 | compile 'com.googlecode.mp4parser:isoparser:1.+' 30 | 31 | compile 'org.apache.commons:commons-io:1.3.2' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/antoniohormigo/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/es/anthorlop/videotrimmer/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/es/anthorlop/videotrimmer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.database.Cursor; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.net.Uri; 10 | import android.os.Environment; 11 | import android.os.Bundle; 12 | import android.provider.MediaStore; 13 | import android.util.Log; 14 | import android.view.Menu; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.widget.Button; 18 | import android.widget.Toast; 19 | 20 | import org.apache.commons.io.FileUtils; 21 | 22 | import java.io.File; 23 | import java.io.FileNotFoundException; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.util.Date; 28 | 29 | import butterknife.ButterKnife; 30 | import butterknife.InjectView; 31 | 32 | 33 | public class MainActivity extends Activity { 34 | 35 | @InjectView(R.id.buttonSelect) 36 | Button buttonSelect; 37 | 38 | private static final int SELECT_PHOTO = 567843; 39 | 40 | private static final int VIDEO_RECORDERED = 452684; 41 | 42 | private String url_file; 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | setContentView(R.layout.activity_main); 48 | 49 | ButterKnife.inject(this); 50 | 51 | buttonSelect.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | openGalleryToSendFiles(v); 55 | } 56 | }); 57 | 58 | } 59 | 60 | private void openGalleryToSendFiles(View view) { 61 | // create example file to save to OneDrive 62 | Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); 63 | photoPickerIntent.setType("video/*"); 64 | startActivityForResult(photoPickerIntent, SELECT_PHOTO); 65 | } 66 | 67 | @Override 68 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 69 | 70 | if (requestCode == SELECT_PHOTO) { 71 | if (resultCode == RESULT_OK) { 72 | 73 | Uri selectedImage = data.getData(); 74 | 75 | String filepath = getRealPathFromURI(selectedImage, this); 76 | 77 | if (filepath != null) { 78 | 79 | File file = new File(Environment.getExternalStorageDirectory().getPath() + "/anthorlopCamera"); 80 | if (!file.exists()) { 81 | file.mkdirs(); 82 | } 83 | 84 | Date d = new Date(); 85 | String timestamp = String.valueOf(d.getTime()); 86 | 87 | url_file = Environment.getExternalStorageDirectory().getPath() + "/anthorlopCamera/preview_" + timestamp + ".mp4"; 88 | 89 | File original = new File(filepath); 90 | File destino = new File(url_file); 91 | 92 | try { 93 | FileUtils.copyFile(original, destino); 94 | } catch (IOException e) { 95 | e.printStackTrace(); 96 | } 97 | 98 | goToPreviewVideo(); 99 | } else { 100 | Toast.makeText(MainActivity.this, "Se ha producido en error", Toast.LENGTH_LONG).show(); 101 | } 102 | } 103 | 104 | } else if (requestCode == VIDEO_RECORDERED) { 105 | if (resultCode == RESULT_OK) { 106 | 107 | String path = data.getExtras().getString(VideoRecorderedPlayerActivity.VIDEO_PATH); 108 | 109 | 110 | Toast.makeText(MainActivity.this, "El vídeo se ha editado correctamente '" + path + "'", Toast.LENGTH_LONG).show(); 111 | 112 | } else if (resultCode == RESULT_CANCELED && data != null && data.getExtras() != null 113 | && data.getExtras().containsKey(VideoRecorderedPlayerActivity.VIDEO_PATH)) { 114 | 115 | // el video ha sido editado y se vuelve a mostrar 116 | 117 | String path = data.getExtras().getString(VideoRecorderedPlayerActivity.VIDEO_PATH); 118 | 119 | url_file = path.replace("_edited.mp4", ".mp4"); 120 | 121 | // eliminamos el video inicial 122 | File orig = new File(url_file); 123 | if (orig.exists() && orig.isFile()) { 124 | orig.delete(); 125 | } 126 | 127 | // cambiamos el nombre del video editado por el inicial 128 | File nuevo = new File(path); 129 | nuevo.renameTo(new File(path.replace("_edited.mp4", ".mp4"))); 130 | 131 | goToPreviewVideo(); 132 | } else if (resultCode == RESULT_CANCELED) { 133 | 134 | Toast.makeText(MainActivity.this, "Se ha cancelado la edición del video", Toast.LENGTH_LONG).show(); 135 | 136 | } 137 | } 138 | super.onActivityResult(requestCode, resultCode, data); 139 | } 140 | 141 | private void goToPreviewVideo() { 142 | Intent intent = new Intent(this, VideoRecorderedPlayerActivity.class); 143 | Bundle extras = new Bundle(); 144 | extras.putString(VideoRecorderedPlayerActivity.VIDEO_PATH, url_file); 145 | intent.putExtras(extras); 146 | startActivityForResult(intent, VIDEO_RECORDERED); 147 | //finish(); 148 | } 149 | 150 | @Override 151 | public boolean onCreateOptionsMenu(Menu menu) { 152 | // Inflate the menu; this adds items to the action bar if it is present. 153 | getMenuInflater().inflate(R.menu.menu_main, menu); 154 | return true; 155 | } 156 | 157 | @Override 158 | public boolean onOptionsItemSelected(MenuItem item) { 159 | // Handle action bar item clicks here. The action bar will 160 | // automatically handle clicks on the Home/Up button, so long 161 | // as you specify a parent activity in AndroidManifest.xml. 162 | int id = item.getItemId(); 163 | 164 | //noinspection SimplifiableIfStatement 165 | if (id == R.id.action_settings) { 166 | return true; 167 | } 168 | 169 | return super.onOptionsItemSelected(item); 170 | } 171 | 172 | @Override 173 | protected void onDestroy() { 174 | super.onDestroy(); 175 | ButterKnife.reset(this); 176 | } 177 | 178 | public String getRealPathFromURI(Uri contentUri, Context context) { 179 | String videoPath = contentUri.getPath(); 180 | try{ 181 | String[] proj = { MediaStore.Video.Media.DATA }; 182 | //Cursor cursor = activity.managedQuery(contentUri, proj, null, null, 183 | //null); 184 | Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null); 185 | Log.i("cursor", "upload-->" + cursor); 186 | Log.i("contentUri", "upload-->" + contentUri); 187 | Log.i("proj", "upload-->" + proj); 188 | int position=0; 189 | 190 | if (cursor !=null && cursor.moveToPosition(position)) { 191 | int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 192 | 193 | Log.i("column_index", "Upload-->" + column_index); 194 | videoPath = cursor.getString(column_index); //I got a null pointer exception here.(But cursor hreturns saome value) 195 | Log.i("videoPath", "Upload-->" + videoPath); 196 | cursor.close(); 197 | 198 | } 199 | 200 | } catch(Exception e){ 201 | return contentUri.getPath(); 202 | } 203 | 204 | return videoPath; 205 | 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/es/anthorlop/videotrimmer/VideoRecorderedPlayerActivity.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer; 2 | 3 | import android.app.Activity; 4 | import android.app.ProgressDialog; 5 | import android.content.Intent; 6 | import android.content.pm.ActivityInfo; 7 | import android.media.AudioManager; 8 | import android.media.MediaPlayer; 9 | import android.net.Uri; 10 | import android.os.AsyncTask; 11 | import android.os.Bundle; 12 | import android.util.DisplayMetrics; 13 | import android.util.Log; 14 | import android.view.SurfaceHolder; 15 | import android.view.SurfaceView; 16 | import android.view.View; 17 | import android.view.Window; 18 | import android.view.WindowManager; 19 | import android.widget.Button; 20 | import android.widget.FrameLayout; 21 | import android.widget.RelativeLayout; 22 | import android.widget.Toast; 23 | 24 | import com.coremedia.iso.boxes.Container; 25 | import com.googlecode.mp4parser.FileDataSourceImpl; 26 | import com.googlecode.mp4parser.authoring.Movie; 27 | import com.googlecode.mp4parser.authoring.Track; 28 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; 29 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; 30 | import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; 31 | 32 | import java.io.File; 33 | import java.io.FileNotFoundException; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.nio.channels.WritableByteChannel; 37 | import java.util.LinkedList; 38 | import java.util.List; 39 | 40 | import butterknife.ButterKnife; 41 | import butterknife.InjectView; 42 | import es.anthorlop.videotrimmer.custom.RangeSeekBar; 43 | import es.anthorlop.videotrimmer.custom.VideoControllerView; 44 | 45 | public class VideoRecorderedPlayerActivity extends Activity 46 | implements SurfaceHolder.Callback, MediaPlayer.OnPreparedListener, VideoControllerView.MediaPlayerControl { 47 | 48 | public static String TAG = "VideoRecorderedPlayerActivity"; 49 | 50 | public static String VIDEO_PATH = "VideoPath"; 51 | 52 | private MediaPlayer player; 53 | private VideoControllerView controller; 54 | 55 | RangeSeekBar seekBar; 56 | 57 | @InjectView(R.id.videoSurfaceContainer) 58 | FrameLayout videoSurfaceContainer; 59 | 60 | @InjectView(R.id.videoSurface) 61 | SurfaceView videoSurface; 62 | 63 | @InjectView(R.id.cancelButton) 64 | Button cancelButton; 65 | 66 | @InjectView(R.id.continueButton) 67 | Button continueButton; 68 | 69 | @InjectView(R.id.rangeLayout) 70 | RelativeLayout rangeLayout; 71 | 72 | private ProgressDialog mProgressDialog; 73 | 74 | private String path; 75 | private String finalPath; 76 | 77 | public void onCreate(Bundle icicle) { 78 | super.onCreate(icicle); 79 | 80 | requestWindowFeature(Window.FEATURE_NO_TITLE); 81 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 82 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 83 | 84 | setContentView(R.layout.activity_video_player); 85 | 86 | ButterKnife.inject(this); 87 | 88 | if (getIntent().getExtras() != null) { 89 | path = getIntent().getExtras().getString(VIDEO_PATH); 90 | init(); 91 | } 92 | 93 | 94 | 95 | cancelButton.setOnClickListener(new View.OnClickListener() { 96 | @Override 97 | public void onClick(View v) { 98 | deleteVideo(); 99 | finish(); 100 | } 101 | }); 102 | 103 | continueButton.setOnClickListener(new View.OnClickListener() { 104 | @Override 105 | public void onClick(View v) { 106 | 107 | // comprobamos si se debe editar el video 108 | if (controller.getMinPosition() > 0 || controller.getMaxPosition() < getDuration()) { 109 | 110 | mProgressDialog = new ProgressDialog(VideoRecorderedPlayerActivity.this); 111 | mProgressDialog.setMessage("Preparando vídeo ..."); 112 | mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); 113 | mProgressDialog.setCancelable(false); 114 | mProgressDialog.show(); 115 | 116 | new PostTrimAsynkTask(path).execute(""); 117 | 118 | } else { 119 | 120 | goBackAndSendVideo(); 121 | 122 | } 123 | } 124 | }); 125 | 126 | videoSurfaceContainer.setOnClickListener(new View.OnClickListener() { 127 | @Override 128 | public void onClick(View v) { 129 | controller.onclick(); 130 | } 131 | }); 132 | } 133 | 134 | private void goBackAndReopenVideo() { 135 | 136 | Intent resultIntent = new Intent(); 137 | Bundle bundle = new Bundle(); 138 | bundle.putString(VIDEO_PATH, finalPath); 139 | resultIntent.putExtras(bundle); 140 | setResult(RESULT_CANCELED, resultIntent); 141 | VideoRecorderedPlayerActivity.this.finish(); 142 | 143 | } 144 | 145 | private void goBackAndSendVideo() { 146 | 147 | Intent resultIntent = new Intent(); 148 | Bundle bundle = new Bundle(); 149 | bundle.putString(VIDEO_PATH, path); 150 | resultIntent.putExtras(bundle); 151 | setResult(RESULT_OK, resultIntent); 152 | VideoRecorderedPlayerActivity.this.finish(); 153 | 154 | } 155 | 156 | public void onPause() { 157 | super.onPause(); 158 | } 159 | 160 | private void init() { 161 | SurfaceHolder videoHolder = videoSurface.getHolder(); 162 | videoHolder.addCallback(this); 163 | 164 | player = new MediaPlayer(); 165 | controller = new VideoControllerView(this); 166 | 167 | try { 168 | player.setAudioStreamType(AudioManager.STREAM_MUSIC); 169 | player.setDataSource(this, Uri.parse(path)); 170 | player.setOnPreparedListener(this); 171 | } catch (IllegalArgumentException e) { 172 | e.printStackTrace(); 173 | } catch (SecurityException e) { 174 | e.printStackTrace(); 175 | } catch (IllegalStateException e) { 176 | e.printStackTrace(); 177 | } catch (IOException e) { 178 | e.printStackTrace(); 179 | } 180 | 181 | } 182 | 183 | @Override 184 | public void surfaceCreated(SurfaceHolder holder) { 185 | 186 | try { 187 | player.setDisplay(holder); 188 | player.prepareAsync(); 189 | } catch (IllegalStateException e) { 190 | e.printStackTrace(); 191 | } 192 | } 193 | 194 | @Override 195 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 196 | 197 | } 198 | 199 | @Override 200 | public void surfaceDestroyed(SurfaceHolder holder) { 201 | /*player.reset(); 202 | player.release();*/ 203 | } 204 | 205 | // Implement MediaPlayer.OnPreparedListener 206 | @Override 207 | public void onPrepared(MediaPlayer mp) { 208 | controller.setMediaPlayer(this); 209 | 210 | controller.setMinPosition(0); 211 | controller.setMaxPosition(getDuration()); 212 | 213 | seekTo(1); 214 | 215 | seekBar = new RangeSeekBar(0, getDuration(), this); 216 | seekBar.setOnRangeSeekBarChangeListener(new RangeSeekBar.OnRangeSeekBarChangeListener() { 217 | @Override 218 | public void onRangeSeekBarValuesChanged(RangeSeekBar bar, Integer minValue, Integer maxValue) { 219 | controller.setMinPosition(minValue); 220 | controller.setMaxPosition(maxValue); 221 | 222 | if (getCurrentPosition() < minValue) { 223 | seekTo(minValue); 224 | } 225 | 226 | if (getCurrentPosition() > maxValue) { 227 | seekTo(maxValue); 228 | } 229 | 230 | controller.show(); 231 | } 232 | }); 233 | 234 | rangeLayout.addView(seekBar); 235 | 236 | if (player.getVideoWidth() > player.getVideoHeight()) { 237 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); 238 | } else { 239 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); 240 | } 241 | 242 | controller.setAnchorView(videoSurfaceContainer, player.getVideoWidth(), player.getVideoHeight()); 243 | 244 | controller.show(); 245 | 246 | //player.start(); 247 | } 248 | // End MediaPlayer.OnPreparedListener 249 | 250 | // Implement VideoMediaController.MediaPlayerControl 251 | @Override 252 | public boolean canPause() { 253 | return true; 254 | } 255 | 256 | @Override 257 | public boolean canSeekBackward() { 258 | return true; 259 | } 260 | 261 | @Override 262 | public boolean canSeekForward() { 263 | return true; 264 | } 265 | 266 | @Override 267 | public int getBufferPercentage() { 268 | return 0; 269 | } 270 | 271 | @Override 272 | public int getCurrentPosition() { 273 | return player.getCurrentPosition(); 274 | } 275 | 276 | @Override 277 | public int getDuration() { 278 | return player.getDuration(); 279 | } 280 | 281 | @Override 282 | public boolean isPlaying() { 283 | return player.isPlaying(); 284 | } 285 | 286 | @Override 287 | public void pause() { 288 | player.pause(); 289 | } 290 | 291 | @Override 292 | public void seekTo(int i) { 293 | player.seekTo(i); 294 | } 295 | 296 | @Override 297 | public void start() { 298 | player.start(); 299 | } 300 | 301 | @Override 302 | public boolean isFullScreen() { 303 | return false; 304 | } 305 | 306 | @Override 307 | public void toggleFullScreen() { 308 | //обрабатываем нажатие на кнопку увеличения видео в весь экран 309 | DisplayMetrics metrics = new DisplayMetrics(); 310 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 311 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) videoSurfaceContainer .getLayoutParams(); 312 | params.width = metrics.widthPixels; 313 | params.height = metrics.heightPixels; 314 | params.leftMargin = 0; 315 | videoSurfaceContainer.setLayoutParams(params); 316 | } 317 | // End VideoMediaController.MediaPlayerControl 318 | 319 | @Override 320 | protected void onDestroy() { 321 | super.onDestroy(); 322 | ButterKnife.reset(this); 323 | 324 | if (mProgressDialog != null && mProgressDialog.isShowing()) { 325 | mProgressDialog.dismiss(); 326 | } 327 | 328 | } 329 | 330 | private FileDataSourceImpl file = null; 331 | private FileOutputStream fos = null; 332 | private WritableByteChannel fc = null; 333 | 334 | private void trimVideo(String path, String finalPath, double startTime, double endTime) { 335 | 336 | Movie movie = null; 337 | try { 338 | file = new FileDataSourceImpl(path); 339 | 340 | 341 | movie = new MovieCreator() 342 | .build(file); 343 | 344 | } catch (FileNotFoundException e) { 345 | e.printStackTrace(); 346 | } catch (IOException e) { 347 | e.printStackTrace(); 348 | } 349 | 350 | 351 | 352 | List tracks = movie.getTracks(); 353 | movie.setTracks(new LinkedList()); 354 | // remove all tracks we will create new tracks from the old 355 | 356 | boolean timeCorrected = false; 357 | 358 | // Here we try to find a track that has sync samples. Since we can only 359 | // start decoding 360 | // at such a sample we SHOULD make sure that the start of the new 361 | // fragment is exactly 362 | // such a frame 363 | for (Track track : tracks) { 364 | if (track.getSyncSamples() != null 365 | && track.getSyncSamples().length > 0) { 366 | if (timeCorrected) { 367 | // This exception here could be a false positive in case we 368 | // have multiple tracks 369 | // with sync samples at exactly the same positions. E.g. a 370 | // single movie containing 371 | // multiple qualities of the same video (Microsoft Smooth 372 | // Streaming file) 373 | 374 | throw new RuntimeException( 375 | "The startTime has already been corrected by another track with SyncSample. Not Supported."); 376 | 377 | } 378 | } 379 | } 380 | 381 | for (Track track : tracks) { 382 | long currentSample = 0; 383 | double currentTime = 0; 384 | long startSample = -1; 385 | long endSample = -1; 386 | 387 | for (int i = 0; i < track.getSampleDurations().length; i++) { 388 | if (currentTime <= startTime) { 389 | 390 | // current sample is still before the new starttime 391 | startSample = currentSample; 392 | } 393 | if (currentTime <= endTime) { 394 | // current sample is after the new start time and still before the new endtime 395 | endSample = currentSample; 396 | } else { 397 | // current sample is after the end of the cropped video 398 | break; 399 | } 400 | currentTime += (double) track.getSampleDurations()[i] / (double) track.getTrackMetaData().getTimescale(); 401 | currentSample++; 402 | } 403 | 404 | movie.addTrack(new CroppedTrack(track, startSample, endSample)); 405 | 406 | Container out = new DefaultMp4Builder().build(movie); 407 | //MovieHeaderBox mvhd = Path.getPath(out, "moov/mvhd"); 408 | //mvhd.setMatrix(Matrix.ROTATE_180); 409 | 410 | File dst = new File(finalPath); 411 | 412 | try { 413 | fos = new FileOutputStream(dst); 414 | 415 | fc = fos.getChannel(); 416 | 417 | try { 418 | out.writeContainer(fc); 419 | 420 | } catch (IOException e) { 421 | e.printStackTrace(); 422 | } 423 | 424 | } catch (FileNotFoundException e) { 425 | e.printStackTrace(); 426 | } 427 | } 428 | } 429 | 430 | @Override 431 | public void onBackPressed() { 432 | super.onBackPressed(); 433 | 434 | deleteVideo(); 435 | 436 | } 437 | 438 | private void deleteVideo() { 439 | File mp4 = new File(path); 440 | if (mp4.exists() && mp4.isFile()) { 441 | mp4.delete(); 442 | } 443 | } 444 | 445 | 446 | private class PostTrimAsynkTask extends AsyncTask { 447 | 448 | private String mPath; 449 | 450 | public PostTrimAsynkTask(String path) { 451 | this.mPath = path; 452 | } 453 | 454 | @Override 455 | protected Boolean doInBackground(String... params) { 456 | 457 | try { 458 | 459 | finalPath = mPath.replace(".mp4", "_edited.mp4"); 460 | 461 | trimVideo(mPath, finalPath, controller.getMinPosition()/1000L, controller.getMaxPosition()/1000L); 462 | 463 | return true; 464 | 465 | } catch (Exception e) { 466 | Log.e(TAG, e.toString()); 467 | } 468 | 469 | return false; 470 | } 471 | 472 | @Override 473 | protected void onPostExecute(Boolean result){ 474 | 475 | try { 476 | fc.close(); 477 | fos.close(); 478 | file.close(); 479 | } catch (IOException e2) { 480 | e2.printStackTrace(); 481 | } 482 | 483 | if (result) { 484 | goBackAndReopenVideo(); 485 | Toast.makeText(VideoRecorderedPlayerActivity.this, "Video editado correctamente", 486 | Toast.LENGTH_LONG).show(); 487 | } else { 488 | Toast.makeText(VideoRecorderedPlayerActivity.this, "Error al editar el vídeo", 489 | Toast.LENGTH_LONG).show(); 490 | } 491 | 492 | mProgressDialog.dismiss(); 493 | 494 | } 495 | } 496 | 497 | 498 | 499 | } -------------------------------------------------------------------------------- /app/src/main/java/es/anthorlop/videotrimmer/custom/RangeSeekBar.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer.custom; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.Paint.Style; 10 | import android.graphics.RectF; 11 | import android.os.Bundle; 12 | import android.os.Parcelable; 13 | import android.view.MotionEvent; 14 | import android.view.ViewConfiguration; 15 | import android.widget.ImageView; 16 | 17 | import java.math.BigDecimal; 18 | 19 | import es.anthorlop.videotrimmer.R; 20 | 21 | /** 22 | * @author anthorlop 23 | * 24 | */ 25 | public class RangeSeekBar extends ImageView { 26 | private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 27 | private final Bitmap thumbImage = BitmapFactory.decodeResource(getResources(), R.drawable.seek_thumb); 28 | private final Bitmap thumbPressedImage = BitmapFactory.decodeResource(getResources(), R.drawable.seek_thumb); 29 | private final float thumbWidth = thumbImage.getWidth(); 30 | private final float thumbHalfWidth = 0.5f * thumbWidth; 31 | private final float thumbHalfHeight = 0.5f * thumbImage.getHeight(); 32 | private final float lineHeight = 0.3f * thumbHalfHeight; 33 | private final float padding = thumbHalfWidth; 34 | private final T absoluteMinValue, absoluteMaxValue; 35 | private final NumberType numberType; 36 | private final double absoluteMinValuePrim, absoluteMaxValuePrim; 37 | private double normalizedMinValue = 0d; 38 | private double normalizedMaxValue = 1d; 39 | private Thumb pressedThumb = null; 40 | private boolean notifyWhileDragging = false; 41 | private OnRangeSeekBarChangeListener listener; 42 | 43 | /** 44 | * An invalid pointer id. 45 | */ 46 | public static final int INVALID_POINTER_ID = 255; 47 | 48 | // Localized constants from MotionEvent for compatibility 49 | // with API < 8 "Froyo". 50 | public static final int ACTION_POINTER_UP = 0x6, ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8; 51 | 52 | private float mDownMotionX; 53 | private int mActivePointerId = INVALID_POINTER_ID; 54 | 55 | /** 56 | * On touch, this offset plus the scaled value from the position of the touch will form the progress value. Usually 0. 57 | */ 58 | float mTouchProgressOffset; 59 | 60 | private int mScaledTouchSlop; 61 | private boolean mIsDragging; 62 | 63 | /** 64 | * Creates a new RangeSeekBar. 65 | * 66 | * @param absoluteMinValue 67 | * The minimum value of the selectable range. 68 | * @param absoluteMaxValue 69 | * The maximum value of the selectable range. 70 | * @param context 71 | * @throws IllegalArgumentException 72 | * Will be thrown if min/max value type is not one of Long, Double, Integer, Float, Short, Byte or BigDecimal. 73 | */ 74 | public RangeSeekBar(T absoluteMinValue, T absoluteMaxValue, Context context) throws IllegalArgumentException { 75 | super(context); 76 | this.absoluteMinValue = absoluteMinValue; 77 | this.absoluteMaxValue = absoluteMaxValue; 78 | absoluteMinValuePrim = absoluteMinValue.doubleValue(); 79 | absoluteMaxValuePrim = absoluteMaxValue.doubleValue(); 80 | numberType = NumberType.fromNumber(absoluteMinValue); 81 | 82 | // make RangeSeekBar focusable. This solves focus handling issues in case EditText widgets are being used along with the RangeSeekBar within ScollViews. 83 | setFocusable(true); 84 | setFocusableInTouchMode(true); 85 | init(); 86 | } 87 | 88 | private final void init() { 89 | mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 90 | } 91 | 92 | public boolean isNotifyWhileDragging() { 93 | return notifyWhileDragging; 94 | } 95 | 96 | /** 97 | * Should the widget notify the listener callback while the user is still dragging a thumb? Default is false. 98 | * 99 | * @param flag 100 | */ 101 | public void setNotifyWhileDragging(boolean flag) { 102 | this.notifyWhileDragging = flag; 103 | } 104 | 105 | /** 106 | * Returns the absolute minimum value of the range that has been set at construction time. 107 | * 108 | * @return The absolute minimum value of the range. 109 | */ 110 | public T getAbsoluteMinValue() { 111 | return absoluteMinValue; 112 | } 113 | 114 | /** 115 | * Returns the absolute maximum value of the range that has been set at construction time. 116 | * 117 | * @return The absolute maximum value of the range. 118 | */ 119 | public T getAbsoluteMaxValue() { 120 | return absoluteMaxValue; 121 | } 122 | 123 | /** 124 | * Returns the currently selected min value. 125 | * 126 | * @return The currently selected min value. 127 | */ 128 | public T getSelectedMinValue() { 129 | return normalizedToValue(normalizedMinValue); 130 | } 131 | 132 | /** 133 | * Sets the currently selected minimum value. The widget will be invalidated and redrawn. 134 | * 135 | * @param value 136 | * The Number value to set the minimum value to. Will be clamped to given absolute minimum/maximum range. 137 | */ 138 | public void setSelectedMinValue(T value) { 139 | // in case absoluteMinValue == absoluteMaxValue, avoid division by zero when normalizing. 140 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 141 | setNormalizedMinValue(0d); 142 | } 143 | else { 144 | setNormalizedMinValue(valueToNormalized(value)); 145 | } 146 | } 147 | 148 | /** 149 | * Returns the currently selected max value. 150 | * 151 | * @return The currently selected max value. 152 | */ 153 | public T getSelectedMaxValue() { 154 | return normalizedToValue(normalizedMaxValue); 155 | } 156 | 157 | /** 158 | * Sets the currently selected maximum value. The widget will be invalidated and redrawn. 159 | * 160 | * @param value 161 | * The Number value to set the maximum value to. Will be clamped to given absolute minimum/maximum range. 162 | */ 163 | public void setSelectedMaxValue(T value) { 164 | // in case absoluteMinValue == absoluteMaxValue, avoid division by zero when normalizing. 165 | if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) { 166 | setNormalizedMaxValue(1d); 167 | } 168 | else { 169 | setNormalizedMaxValue(valueToNormalized(value)); 170 | } 171 | } 172 | 173 | /** 174 | * Registers given listener callback to notify about changed selected values. 175 | * 176 | * @param listener 177 | * The listener to notify about changed selected values. 178 | */ 179 | public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener listener) { 180 | this.listener = listener; 181 | } 182 | 183 | /** 184 | * Handles thumb selection and movement. Notifies listener callback on certain events. 185 | */ 186 | @Override 187 | public boolean onTouchEvent(MotionEvent event) { 188 | 189 | if (!isEnabled()) 190 | return false; 191 | 192 | int pointerIndex; 193 | 194 | final int action = event.getAction(); 195 | switch (action & MotionEvent.ACTION_MASK) { 196 | 197 | case MotionEvent.ACTION_DOWN: 198 | // Remember where the motion event started 199 | mActivePointerId = event.getPointerId(event.getPointerCount() - 1); 200 | pointerIndex = event.findPointerIndex(mActivePointerId); 201 | mDownMotionX = event.getX(pointerIndex); 202 | 203 | pressedThumb = evalPressedThumb(mDownMotionX); 204 | 205 | // Only handle thumb presses. 206 | if (pressedThumb == null) 207 | return super.onTouchEvent(event); 208 | 209 | setPressed(true); 210 | invalidate(); 211 | onStartTrackingTouch(); 212 | trackTouchEvent(event); 213 | attemptClaimDrag(); 214 | 215 | break; 216 | case MotionEvent.ACTION_MOVE: 217 | if (pressedThumb != null) { 218 | 219 | if (mIsDragging) { 220 | trackTouchEvent(event); 221 | } 222 | else { 223 | // Scroll to follow the motion event 224 | pointerIndex = event.findPointerIndex(mActivePointerId); 225 | final float x = event.getX(pointerIndex); 226 | 227 | if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) { 228 | setPressed(true); 229 | invalidate(); 230 | onStartTrackingTouch(); 231 | trackTouchEvent(event); 232 | attemptClaimDrag(); 233 | } 234 | } 235 | 236 | if (notifyWhileDragging && listener != null) { 237 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue()); 238 | } 239 | } 240 | break; 241 | case MotionEvent.ACTION_UP: 242 | if (mIsDragging) { 243 | trackTouchEvent(event); 244 | onStopTrackingTouch(); 245 | setPressed(false); 246 | } 247 | else { 248 | // Touch up when we never crossed the touch slop threshold 249 | // should be interpreted as a tap-seek to that location. 250 | onStartTrackingTouch(); 251 | trackTouchEvent(event); 252 | onStopTrackingTouch(); 253 | } 254 | 255 | pressedThumb = null; 256 | invalidate(); 257 | if (listener != null) { 258 | listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue()); 259 | } 260 | break; 261 | case MotionEvent.ACTION_POINTER_DOWN: { 262 | final int index = event.getPointerCount() - 1; 263 | // final int index = ev.getActionIndex(); 264 | mDownMotionX = event.getX(index); 265 | mActivePointerId = event.getPointerId(index); 266 | invalidate(); 267 | break; 268 | } 269 | case MotionEvent.ACTION_POINTER_UP: 270 | onSecondaryPointerUp(event); 271 | invalidate(); 272 | break; 273 | case MotionEvent.ACTION_CANCEL: 274 | if (mIsDragging) { 275 | onStopTrackingTouch(); 276 | setPressed(false); 277 | } 278 | invalidate(); // see above explanation 279 | break; 280 | } 281 | return true; 282 | } 283 | 284 | private final void onSecondaryPointerUp(MotionEvent ev) { 285 | final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; 286 | 287 | final int pointerId = ev.getPointerId(pointerIndex); 288 | if (pointerId == mActivePointerId) { 289 | // This was our active pointer going up. Choose 290 | // a new active pointer and adjust accordingly. 291 | // TODO: Make this decision more intelligent. 292 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 293 | mDownMotionX = ev.getX(newPointerIndex); 294 | mActivePointerId = ev.getPointerId(newPointerIndex); 295 | } 296 | } 297 | 298 | private final void trackTouchEvent(MotionEvent event) { 299 | final int pointerIndex = event.findPointerIndex(mActivePointerId); 300 | final float x = event.getX(pointerIndex); 301 | 302 | if (Thumb.MIN.equals(pressedThumb)) { 303 | setNormalizedMinValue(screenToNormalized(x)); 304 | } 305 | else if (Thumb.MAX.equals(pressedThumb)) { 306 | setNormalizedMaxValue(screenToNormalized(x)); 307 | } 308 | } 309 | 310 | /** 311 | * Tries to claim the user's drag motion, and requests disallowing any ancestors from stealing events in the drag. 312 | */ 313 | private void attemptClaimDrag() { 314 | if (getParent() != null) { 315 | getParent().requestDisallowInterceptTouchEvent(true); 316 | } 317 | } 318 | 319 | /** 320 | * This is called when the user has started touching this widget. 321 | */ 322 | void onStartTrackingTouch() { 323 | mIsDragging = true; 324 | } 325 | 326 | /** 327 | * This is called when the user either releases his touch or the touch is canceled. 328 | */ 329 | void onStopTrackingTouch() { 330 | mIsDragging = false; 331 | } 332 | 333 | /** 334 | * Ensures correct size of the widget. 335 | */ 336 | @Override 337 | protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 338 | int width = 200; 339 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) { 340 | width = MeasureSpec.getSize(widthMeasureSpec); 341 | } 342 | int height = thumbImage.getHeight(); 343 | if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) { 344 | height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec)); 345 | } 346 | setMeasuredDimension(width, height); 347 | } 348 | 349 | /** 350 | * Draws the widget on the given canvas. 351 | */ 352 | @Override 353 | protected synchronized void onDraw(Canvas canvas) { 354 | super.onDraw(canvas); 355 | 356 | // draw seek bar background line 357 | final RectF rect = new RectF(padding, 0.5f * (getHeight() - lineHeight), getWidth() - padding, 0.5f * (getHeight() + lineHeight)); 358 | paint.setStyle(Style.FILL); 359 | paint.setColor(Color.GRAY); 360 | paint.setAntiAlias(true); 361 | canvas.drawRect(rect, paint); 362 | 363 | // draw seek bar active range line 364 | rect.left = normalizedToScreen(normalizedMinValue); 365 | rect.right = normalizedToScreen(normalizedMaxValue); 366 | 367 | // orange color 368 | paint.setColor(getResources().getColor(android.R.color.holo_blue_light)); 369 | canvas.drawRect(rect, paint); 370 | 371 | // draw minimum thumb 372 | drawThumb(normalizedToScreen(normalizedMinValue), Thumb.MIN.equals(pressedThumb), canvas); 373 | 374 | // draw maximum thumb 375 | drawThumb(normalizedToScreen(normalizedMaxValue), Thumb.MAX.equals(pressedThumb), canvas); 376 | } 377 | 378 | /** 379 | * Overridden to save instance state when device orientation changes. This method is called automatically if you assign an id to the RangeSeekBar widget using the {@link #setId(int)} method. Other members of this class than the normalized min and max values don't need to be saved. 380 | */ 381 | @Override 382 | protected Parcelable onSaveInstanceState() { 383 | final Bundle bundle = new Bundle(); 384 | bundle.putParcelable("SUPER", super.onSaveInstanceState()); 385 | bundle.putDouble("MIN", normalizedMinValue); 386 | bundle.putDouble("MAX", normalizedMaxValue); 387 | return bundle; 388 | } 389 | 390 | /** 391 | * Overridden to restore instance state when device orientation changes. This method is called automatically if you assign an id to the RangeSeekBar widget using the {@link #setId(int)} method. 392 | */ 393 | @Override 394 | protected void onRestoreInstanceState(Parcelable parcel) { 395 | final Bundle bundle = (Bundle) parcel; 396 | super.onRestoreInstanceState(bundle.getParcelable("SUPER")); 397 | normalizedMinValue = bundle.getDouble("MIN"); 398 | normalizedMaxValue = bundle.getDouble("MAX"); 399 | } 400 | 401 | /** 402 | * Draws the "normal" resp. "pressed" thumb image on specified x-coordinate. 403 | * 404 | * @param screenCoord 405 | * The x-coordinate in screen space where to draw the image. 406 | * @param pressed 407 | * Is the thumb currently in "pressed" state? 408 | * @param canvas 409 | * The canvas to draw upon. 410 | */ 411 | private void drawThumb(float screenCoord, boolean pressed, Canvas canvas) { 412 | canvas.drawBitmap(pressed ? thumbPressedImage : thumbImage, screenCoord - thumbHalfWidth, (float) ((0.5f * getHeight()) - thumbHalfHeight), paint); 413 | } 414 | 415 | /** 416 | * Decides which (if any) thumb is touched by the given x-coordinate. 417 | * 418 | * @param touchX 419 | * The x-coordinate of a touch event in screen space. 420 | * @return The pressed thumb or null if none has been touched. 421 | */ 422 | private Thumb evalPressedThumb(float touchX) { 423 | Thumb result = null; 424 | boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue); 425 | boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue); 426 | if (minThumbPressed && maxThumbPressed) { 427 | // if both thumbs are pressed (they lie on top of each other), choose the one with more room to drag. this avoids "stalling" the thumbs in a corner, not being able to drag them apart anymore. 428 | result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX; 429 | } 430 | else if (minThumbPressed) { 431 | result = Thumb.MIN; 432 | } 433 | else if (maxThumbPressed) { 434 | result = Thumb.MAX; 435 | } 436 | return result; 437 | } 438 | 439 | /** 440 | * Decides if given x-coordinate in screen space needs to be interpreted as "within" the normalized thumb x-coordinate. 441 | * 442 | * @param touchX 443 | * The x-coordinate in screen space to check. 444 | * @param normalizedThumbValue 445 | * The normalized x-coordinate of the thumb to check. 446 | * @return true if x-coordinate is in thumb range, false otherwise. 447 | */ 448 | private boolean isInThumbRange(float touchX, double normalizedThumbValue) { 449 | return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth; 450 | } 451 | 452 | /** 453 | * Sets normalized min value to value so that 0 <= value <= normalized max value <= 1. The View will get invalidated when calling this method. 454 | * 455 | * @param value 456 | * The new normalized min value to set. 457 | */ 458 | public void setNormalizedMinValue(double value) { 459 | normalizedMinValue = Math.max(0d, Math.min(1d, Math.min(value, normalizedMaxValue))); 460 | invalidate(); 461 | } 462 | 463 | /** 464 | * Sets normalized max value to value so that 0 <= normalized min value <= value <= 1. The View will get invalidated when calling this method. 465 | * 466 | * @param value 467 | * The new normalized max value to set. 468 | */ 469 | public void setNormalizedMaxValue(double value) { 470 | normalizedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, normalizedMinValue))); 471 | invalidate(); 472 | } 473 | 474 | /** 475 | * Converts a normalized value to a Number object in the value space between absolute minimum and maximum. 476 | * 477 | * @param normalized 478 | * @return 479 | */ 480 | @SuppressWarnings("unchecked") 481 | private T normalizedToValue(double normalized) { 482 | return (T) numberType.toNumber(absoluteMinValuePrim + normalized * (absoluteMaxValuePrim - absoluteMinValuePrim)); 483 | } 484 | 485 | /** 486 | * Converts the given Number value to a normalized double. 487 | * 488 | * @param value 489 | * The Number value to normalize. 490 | * @return The normalized double. 491 | */ 492 | private double valueToNormalized(T value) { 493 | if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) { 494 | // prevent division by zero, simply return 0. 495 | return 0d; 496 | } 497 | return (value.doubleValue() - absoluteMinValuePrim) / (absoluteMaxValuePrim - absoluteMinValuePrim); 498 | } 499 | 500 | /** 501 | * Converts a normalized value into screen space. 502 | * 503 | * @param normalizedCoord 504 | * The normalized value to convert. 505 | * @return The converted value in screen space. 506 | */ 507 | private float normalizedToScreen(double normalizedCoord) { 508 | return (float) (padding + normalizedCoord * (getWidth() - 2 * padding)); 509 | } 510 | 511 | /** 512 | * Converts screen space x-coordinates into normalized values. 513 | * 514 | * @param screenCoord 515 | * The x-coordinate in screen space to convert. 516 | * @return The normalized value. 517 | */ 518 | private double screenToNormalized(float screenCoord) { 519 | int width = getWidth(); 520 | if (width <= 2 * padding) { 521 | // prevent division by zero, simply return 0. 522 | return 0d; 523 | } 524 | else { 525 | double result = (screenCoord - padding) / (width - 2 * padding); 526 | return Math.min(1d, Math.max(0d, result)); 527 | } 528 | } 529 | 530 | /** 531 | * Callback listener interface to notify about changed range values. 532 | * 533 | * @param 534 | * The Number type the RangeSeekBar has been declared with. 535 | */ 536 | public interface OnRangeSeekBarChangeListener { 537 | public void onRangeSeekBarValuesChanged(RangeSeekBar bar, T minValue, T maxValue); 538 | } 539 | 540 | /** 541 | * Thumb constants (min and max). 542 | */ 543 | private static enum Thumb { 544 | MIN, MAX 545 | }; 546 | 547 | /** 548 | * Utility enumaration used to convert between Numbers and doubles. 549 | * 550 | * 551 | */ 552 | private static enum NumberType { 553 | LONG, DOUBLE, INTEGER, FLOAT, SHORT, BYTE, BIG_DECIMAL; 554 | 555 | public static NumberType fromNumber(E value) throws IllegalArgumentException { 556 | if (value instanceof Long) { 557 | return LONG; 558 | } 559 | if (value instanceof Double) { 560 | return DOUBLE; 561 | } 562 | if (value instanceof Integer) { 563 | return INTEGER; 564 | } 565 | if (value instanceof Float) { 566 | return FLOAT; 567 | } 568 | if (value instanceof Short) { 569 | return SHORT; 570 | } 571 | if (value instanceof Byte) { 572 | return BYTE; 573 | } 574 | if (value instanceof BigDecimal) { 575 | return BIG_DECIMAL; 576 | } 577 | throw new IllegalArgumentException("Number class '" + value.getClass().getName() + "' is not supported"); 578 | } 579 | 580 | public Number toNumber(double value) { 581 | switch (this) { 582 | case LONG: 583 | return new Long((long) value); 584 | case DOUBLE: 585 | return value; 586 | case INTEGER: 587 | return new Integer((int) value); 588 | case FLOAT: 589 | return new Float(value); 590 | case SHORT: 591 | return new Short((short) value); 592 | case BYTE: 593 | return new Byte((byte) value); 594 | case BIG_DECIMAL: 595 | return new BigDecimal(value); 596 | } 597 | throw new InstantiationError("can't convert " + this + " to a Number object"); 598 | } 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /app/src/main/java/es/anthorlop/videotrimmer/custom/VideoControllerView.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer.custom; 2 | 3 | import android.content.Context; 4 | import android.content.res.Configuration; 5 | import android.graphics.PorterDuff; 6 | import android.graphics.PorterDuffColorFilter; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.Gravity; 12 | import android.view.KeyEvent; 13 | import android.view.LayoutInflater; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.widget.FrameLayout; 18 | import android.widget.ImageButton; 19 | import android.widget.ImageView; 20 | import android.widget.ProgressBar; 21 | import android.widget.SeekBar; 22 | import android.widget.SeekBar.OnSeekBarChangeListener; 23 | import android.widget.TextView; 24 | 25 | import java.lang.ref.WeakReference; 26 | import java.util.Formatter; 27 | import java.util.Locale; 28 | 29 | import es.anthorlop.videotrimmer.R; 30 | import es.anthorlop.videotrimmer.utils.Screens; 31 | 32 | public class VideoControllerView extends FrameLayout { 33 | 34 | private static final String TAG = "VideoControllerView"; 35 | 36 | private MediaPlayerControl mPlayer; 37 | private Context mContext; 38 | private ViewGroup mAnchor; 39 | private View mRoot; 40 | private ProgressBar mProgress; 41 | private TextView mEndTime, mCurrentTime; 42 | private boolean mShowing; 43 | private boolean mDragging; 44 | private static final int sDefaultTimeout = 3000; 45 | private static final int FADE_OUT = 1; 46 | private static final int SHOW_PROGRESS = 2; 47 | private boolean mUseFastForward; 48 | private boolean mFromXml; 49 | private boolean mListenersSet; 50 | private OnClickListener mNextListener, mPrevListener; 51 | StringBuilder mFormatBuilder; 52 | Formatter mFormatter; 53 | 54 | private ImageButton mPauseButton; 55 | 56 | private ImageView mPauseImg; 57 | 58 | private ImageButton mFfwdButton; 59 | private ImageButton mRewButton; 60 | private ImageButton mNextButton; 61 | private ImageButton mPrevButton; 62 | private ImageButton mFullscreenButton; 63 | private Handler mHandler = new MessageHandler(this); 64 | 65 | private int minPosition = 0; 66 | private int maxPosition = 0; 67 | 68 | public VideoControllerView(Context context, AttributeSet attrs) { 69 | super(context, attrs); 70 | mRoot = null; 71 | mContext = context; 72 | mUseFastForward = true; 73 | mFromXml = true; 74 | 75 | Log.i(TAG, TAG); 76 | } 77 | 78 | public VideoControllerView(Context context, boolean useFastForward) { 79 | super(context); 80 | mContext = context; 81 | mUseFastForward = useFastForward; 82 | 83 | Log.i(TAG, TAG); 84 | } 85 | 86 | public VideoControllerView(Context context) { 87 | this(context, true); 88 | 89 | Log.i(TAG, TAG); 90 | } 91 | 92 | @Override 93 | public void onFinishInflate() { 94 | if (mRoot != null) 95 | initControllerView(mRoot); 96 | } 97 | 98 | public void setMediaPlayer(MediaPlayerControl player) { 99 | mPlayer = player; 100 | updatePausePlay(); 101 | updateFullScreen(); 102 | } 103 | 104 | public void setAnchorView(ViewGroup view, int ratioWidth, int ratioHeight) { 105 | mAnchor = view; 106 | 107 | int h = view.getHeight(); 108 | int w = Screens.getScreenWidth(mContext); 109 | 110 | if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { 111 | h = w*ratioHeight/ratioWidth; 112 | } else { 113 | w = h*ratioWidth/ratioHeight; 114 | } 115 | 116 | LayoutParams frameParams = new LayoutParams( 117 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT 118 | ); 119 | 120 | view.getLayoutParams().width = w; 121 | view.getLayoutParams().height = h; 122 | 123 | removeAllViews(); 124 | View v = makeControllerView(); 125 | addView(v, frameParams); 126 | } 127 | 128 | protected View makeControllerView() { 129 | LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 130 | mRoot = inflate.inflate(R.layout.media_controller, null); 131 | 132 | initControllerView(mRoot); 133 | 134 | return mRoot; 135 | } 136 | 137 | private void initControllerView(View v) { 138 | mPauseButton = (ImageButton) v.findViewById(R.id.pause); 139 | if (mPauseButton != null) { 140 | mPauseButton.requestFocus(); 141 | mPauseButton.setOnClickListener(mPauseListener); 142 | } 143 | 144 | mPauseImg = (ImageView) v.findViewById(R.id.pausePlay); 145 | if (mPauseImg != null) { 146 | mPauseImg.requestFocus(); 147 | mPauseImg.setOnClickListener(mPauseListener); 148 | } 149 | 150 | mFullscreenButton = (ImageButton) v.findViewById(R.id.fullscreen); 151 | if (mFullscreenButton != null) { 152 | mFullscreenButton.requestFocus(); 153 | mFullscreenButton.setOnClickListener(mFullscreenListener); 154 | } 155 | 156 | mFfwdButton = (ImageButton) v.findViewById(R.id.ffwd); 157 | if (mFfwdButton != null) { 158 | mFfwdButton.setOnClickListener(mFfwdListener); 159 | if (!mFromXml) { 160 | mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 161 | } 162 | } 163 | 164 | mRewButton = (ImageButton) v.findViewById(R.id.rew); 165 | if (mRewButton != null) { 166 | mRewButton.setOnClickListener(mRewListener); 167 | if (!mFromXml) { 168 | mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 169 | } 170 | } 171 | 172 | // По дефолту вьюха будет спрятана, и показываться будет после события onTouch() 173 | mNextButton = (ImageButton) v.findViewById(R.id.next); 174 | if (mNextButton != null && !mFromXml && !mListenersSet) { 175 | mNextButton.setVisibility(View.GONE); 176 | } 177 | mPrevButton = (ImageButton) v.findViewById(R.id.prev); 178 | if (mPrevButton != null && !mFromXml && !mListenersSet) { 179 | mPrevButton.setVisibility(View.GONE); 180 | } 181 | 182 | mProgress = (SeekBar) v.findViewById(R.id.mediacontroller_progress); 183 | if (mProgress != null) { 184 | if (mProgress instanceof SeekBar) { 185 | 186 | mProgress.getProgressDrawable().setColorFilter(new PorterDuffColorFilter(getResources().getColor(android.R.color.holo_blue_light), PorterDuff.Mode.SRC_IN)); 187 | 188 | SeekBar seeker = (SeekBar) mProgress; 189 | seeker.setOnSeekBarChangeListener(mSeekListener); 190 | } 191 | mProgress.setMax(1000); 192 | } 193 | 194 | mEndTime = (TextView) v.findViewById(R.id.time); 195 | mCurrentTime = (TextView) v.findViewById(R.id.time_current); 196 | mFormatBuilder = new StringBuilder(); 197 | mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); 198 | 199 | installPrevNextListeners(); 200 | } 201 | 202 | 203 | public void show() { 204 | show(sDefaultTimeout); 205 | } 206 | 207 | public void onclick() { 208 | if (mPauseImg.getVisibility() == INVISIBLE) { 209 | show(); 210 | } else { 211 | mPauseImg.setVisibility(INVISIBLE); 212 | } 213 | } 214 | 215 | private void disableUnsupportedButtons() { 216 | if (mPlayer == null) { 217 | return; 218 | } 219 | 220 | try { 221 | if (mPauseButton != null && !mPlayer.canPause()) { 222 | mPauseButton.setEnabled(false); 223 | } 224 | if (mRewButton != null && !mPlayer.canSeekBackward()) { 225 | mRewButton.setEnabled(false); 226 | } 227 | if (mFfwdButton != null && !mPlayer.canSeekForward()) { 228 | mFfwdButton.setEnabled(false); 229 | } 230 | } catch (IncompatibleClassChangeError ex) { 231 | //выводите в лог что хотите из ex 232 | } 233 | } 234 | 235 | public void show(int timeout) { 236 | 237 | mPauseImg.setVisibility(VISIBLE); 238 | 239 | if (!mShowing && mAnchor != null) { 240 | 241 | setProgress(); 242 | if (mPauseButton != null) { 243 | mPauseButton.requestFocus(); 244 | } 245 | disableUnsupportedButtons(); 246 | 247 | LayoutParams tlp = new LayoutParams( 248 | ViewGroup.LayoutParams.MATCH_PARENT, 249 | ViewGroup.LayoutParams.WRAP_CONTENT, 250 | Gravity.BOTTOM 251 | ); 252 | 253 | mAnchor.addView(this, tlp); 254 | mShowing = true; 255 | } 256 | updatePausePlay(); 257 | updateFullScreen(); 258 | 259 | mHandler.sendEmptyMessage(SHOW_PROGRESS); 260 | 261 | Message msg = mHandler.obtainMessage(FADE_OUT); 262 | if (timeout != 0) { 263 | mHandler.removeMessages(FADE_OUT); 264 | mHandler.sendMessageDelayed(msg, timeout); 265 | } 266 | } 267 | 268 | public boolean isShowing() { 269 | return mShowing; 270 | } 271 | 272 | public void hide() { 273 | if (mAnchor == null) { 274 | return; 275 | } 276 | 277 | try { 278 | mAnchor.removeView(this); 279 | mHandler.removeMessages(SHOW_PROGRESS); 280 | } catch (IllegalArgumentException ex) { 281 | Log.w("MediaController", "already removed"); 282 | } 283 | mShowing = false; 284 | } 285 | 286 | private String stringForTime(int timeMs) { 287 | int totalSeconds = timeMs / 1000; 288 | 289 | int seconds = totalSeconds % 60; 290 | int minutes = (totalSeconds / 60) % 60; 291 | int hours = totalSeconds / 3600; 292 | 293 | mFormatBuilder.setLength(0); 294 | if (hours > 0) { 295 | return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 296 | } else { 297 | return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 298 | } 299 | } 300 | 301 | private int setProgress() { 302 | if (mPlayer == null || mDragging) { 303 | return 0; 304 | } 305 | 306 | int position = mPlayer.getCurrentPosition(); 307 | int duration = mPlayer.getDuration(); 308 | if (mProgress != null) { 309 | if (duration > 0) { 310 | // use long to avoid overflow 311 | long pos = 1000L * position / duration; 312 | mProgress.setProgress( (int) pos); 313 | } 314 | int percent = mPlayer.getBufferPercentage(); 315 | mProgress.setSecondaryProgress(percent * 10); 316 | } 317 | 318 | if (mEndTime != null) 319 | mEndTime.setText(stringForTime(duration)); 320 | if (mCurrentTime != null) 321 | mCurrentTime.setText(stringForTime(position)); 322 | 323 | /*if (position > maxPosition) { 324 | mPlayer.pause(); 325 | updatePausePlay(); 326 | } 327 | 328 | if (position < minPosition) { 329 | mPlayer.seekTo(minPosition); 330 | }*/ 331 | 332 | return position; 333 | } 334 | 335 | @Override 336 | public boolean onTouchEvent(MotionEvent event) { 337 | show(sDefaultTimeout); 338 | return true; 339 | } 340 | 341 | @Override 342 | public boolean onTrackballEvent(MotionEvent ev) { 343 | show(sDefaultTimeout); 344 | return false; 345 | } 346 | 347 | @Override 348 | public boolean dispatchKeyEvent(KeyEvent event) { 349 | if (mPlayer == null) { 350 | return true; 351 | } 352 | 353 | int keyCode = event.getKeyCode(); 354 | final boolean uniqueDown = event.getRepeatCount() == 0 355 | && event.getAction() == KeyEvent.ACTION_DOWN; 356 | if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK 357 | || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 358 | || keyCode == KeyEvent.KEYCODE_SPACE) { 359 | if (uniqueDown) { 360 | doPauseResume(); 361 | show(sDefaultTimeout); 362 | if (mPauseButton != null) { 363 | mPauseButton.requestFocus(); 364 | } 365 | } 366 | return true; 367 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 368 | if (uniqueDown && !mPlayer.isPlaying()) { 369 | mPlayer.start(); 370 | updatePausePlay(); 371 | show(sDefaultTimeout); 372 | } 373 | return true; 374 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 375 | || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 376 | if (uniqueDown && mPlayer.isPlaying()) { 377 | mPlayer.pause(); 378 | updatePausePlay(); 379 | show(sDefaultTimeout); 380 | } 381 | return true; 382 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 383 | || keyCode == KeyEvent.KEYCODE_VOLUME_UP 384 | || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) { 385 | return super.dispatchKeyEvent(event); 386 | } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { 387 | if (uniqueDown) { 388 | hide(); 389 | } 390 | return true; 391 | } 392 | 393 | show(sDefaultTimeout); 394 | return super.dispatchKeyEvent(event); 395 | } 396 | 397 | private OnClickListener mPauseListener = new OnClickListener() { 398 | public void onClick(View v) { 399 | doPauseResume(); 400 | show(sDefaultTimeout); 401 | } 402 | }; 403 | 404 | private OnClickListener mFullscreenListener = new OnClickListener() { 405 | public void onClick(View v) { 406 | doToggleFullscreen(); 407 | show(sDefaultTimeout); 408 | } 409 | }; 410 | 411 | public void updatePausePlay() { 412 | if (mRoot == null || mPauseButton == null || mPlayer == null) { 413 | return; 414 | } 415 | 416 | if (mPlayer.isPlaying()) { 417 | mPauseButton.setImageResource(R.drawable.ic_media_pause); 418 | mPauseImg.setImageResource(R.drawable.ic_player_big_pause); 419 | } else { 420 | mPauseButton.setImageResource(R.drawable.ic_media_play); 421 | mPauseImg.setImageResource(R.drawable.ic_player_big_play); 422 | } 423 | } 424 | 425 | public void updateFullScreen() { 426 | if (mRoot == null || mFullscreenButton == null || mPlayer == null) { 427 | return; 428 | } 429 | 430 | if (mPlayer.isFullScreen()) { 431 | mFullscreenButton.setImageResource(R.drawable.ic_media_fullscreen_shrink); 432 | } 433 | else { 434 | mFullscreenButton.setImageResource(R.drawable.ic_media_fullscreen_stretch); 435 | } 436 | } 437 | 438 | private void doPauseResume() { 439 | if (mPlayer == null) { 440 | return; 441 | } 442 | 443 | if (mPlayer.isPlaying()) { 444 | mPlayer.pause(); 445 | } else { 446 | mPlayer.start(); 447 | 448 | if (mPlayer.getCurrentPosition() > maxPosition) { 449 | mPlayer.seekTo(minPosition); 450 | } 451 | 452 | } 453 | updatePausePlay(); 454 | } 455 | 456 | private void doToggleFullscreen() { 457 | if (mPlayer == null) { 458 | return; 459 | } 460 | 461 | mPlayer.toggleFullScreen(); 462 | } 463 | 464 | private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 465 | public void onStartTrackingTouch(SeekBar bar) { 466 | show(3600000); 467 | 468 | mDragging = true; 469 | mHandler.removeMessages(SHOW_PROGRESS); 470 | } 471 | 472 | public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 473 | if (mPlayer == null) { 474 | return; 475 | } 476 | 477 | if (!fromuser) { 478 | 479 | long duration = mPlayer.getDuration(); 480 | long newposition = (duration * progress) / 1000L; 481 | 482 | if (newposition > maxPosition) { 483 | if (mPlayer.isPlaying()) { 484 | mPauseButton.performClick(); 485 | } 486 | } 487 | 488 | return; 489 | } 490 | 491 | long duration = mPlayer.getDuration(); 492 | long newposition = (duration * progress) / 1000L; 493 | 494 | if (newposition > maxPosition) { 495 | newposition = maxPosition; 496 | if (mPlayer.isPlaying()) { 497 | mPauseButton.performClick(); 498 | } 499 | } 500 | 501 | if (newposition < minPosition) { 502 | newposition = minPosition; 503 | } 504 | 505 | mPlayer.seekTo( (int) newposition); 506 | 507 | if (mCurrentTime != null) 508 | mCurrentTime.setText(stringForTime( (int) newposition)); 509 | 510 | } 511 | 512 | public void onStopTrackingTouch(SeekBar bar) { 513 | mDragging = false; 514 | setProgress(); 515 | updatePausePlay(); 516 | show(sDefaultTimeout); 517 | 518 | mHandler.sendEmptyMessage(SHOW_PROGRESS); 519 | } 520 | }; 521 | 522 | @Override 523 | public void setEnabled(boolean enabled) { 524 | if (mPauseButton != null) { 525 | mPauseButton.setEnabled(enabled); 526 | } 527 | if (mFfwdButton != null) { 528 | mFfwdButton.setEnabled(enabled); 529 | } 530 | if (mRewButton != null) { 531 | mRewButton.setEnabled(enabled); 532 | } 533 | if (mNextButton != null) { 534 | mNextButton.setEnabled(enabled && mNextListener != null); 535 | } 536 | if (mPrevButton != null) { 537 | mPrevButton.setEnabled(enabled && mPrevListener != null); 538 | } 539 | if (mProgress != null) { 540 | mProgress.setEnabled(enabled); 541 | } 542 | disableUnsupportedButtons(); 543 | super.setEnabled(enabled); 544 | } 545 | 546 | private OnClickListener mRewListener = new OnClickListener() { 547 | public void onClick(View v) { 548 | if (mPlayer == null) { 549 | return; 550 | } 551 | 552 | int pos = mPlayer.getCurrentPosition(); 553 | pos -= 5000; // милисекунд 554 | mPlayer.seekTo(pos); 555 | setProgress(); 556 | 557 | show(sDefaultTimeout); 558 | } 559 | }; 560 | 561 | private OnClickListener mFfwdListener = new OnClickListener() { 562 | public void onClick(View v) { 563 | if (mPlayer == null) { 564 | return; 565 | } 566 | 567 | int pos = mPlayer.getCurrentPosition(); 568 | pos += 15000; // милисекунд 569 | mPlayer.seekTo(pos); 570 | setProgress(); 571 | 572 | show(sDefaultTimeout); 573 | } 574 | }; 575 | 576 | private void installPrevNextListeners() { 577 | if (mNextButton != null) { 578 | mNextButton.setOnClickListener(mNextListener); 579 | mNextButton.setEnabled(mNextListener != null); 580 | } 581 | 582 | if (mPrevButton != null) { 583 | mPrevButton.setOnClickListener(mPrevListener); 584 | mPrevButton.setEnabled(mPrevListener != null); 585 | } 586 | } 587 | 588 | public void setPrevNextListeners(OnClickListener next, OnClickListener prev) { 589 | mNextListener = next; 590 | mPrevListener = prev; 591 | mListenersSet = true; 592 | 593 | if (mRoot != null) { 594 | installPrevNextListeners(); 595 | 596 | if (mNextButton != null && !mFromXml) { 597 | mNextButton.setVisibility(View.VISIBLE); 598 | } 599 | if (mPrevButton != null && !mFromXml) { 600 | mPrevButton.setVisibility(View.VISIBLE); 601 | } 602 | } 603 | } 604 | 605 | public interface MediaPlayerControl { 606 | void start(); 607 | void pause(); 608 | int getDuration(); 609 | int getCurrentPosition(); 610 | void seekTo(int pos); 611 | boolean isPlaying(); 612 | int getBufferPercentage(); 613 | boolean canPause(); 614 | boolean canSeekBackward(); 615 | boolean canSeekForward(); 616 | boolean isFullScreen(); 617 | void toggleFullScreen(); 618 | } 619 | 620 | private static class MessageHandler extends Handler { 621 | private final WeakReference mView; 622 | 623 | MessageHandler(VideoControllerView view) { 624 | mView = new WeakReference(view); 625 | } 626 | @Override 627 | public void handleMessage(Message msg) { 628 | VideoControllerView view = mView.get(); 629 | if (view == null || view.mPlayer == null) { 630 | return; 631 | } 632 | 633 | int pos; 634 | switch (msg.what) { 635 | case FADE_OUT: 636 | //view.hide(); 637 | view.mPauseImg.setVisibility(INVISIBLE); 638 | 639 | break; 640 | case SHOW_PROGRESS: 641 | pos = view.setProgress(); 642 | if (!view.mDragging && view.mShowing && view.mPlayer.isPlaying()) { 643 | msg = obtainMessage(SHOW_PROGRESS); 644 | sendMessageDelayed(msg, 1000 - (pos % 1000)); 645 | } 646 | break; 647 | } 648 | } 649 | } 650 | 651 | public int getMinPosition() { 652 | return minPosition; 653 | } 654 | 655 | public void setMinPosition(int minPosition) { 656 | this.minPosition = minPosition; 657 | } 658 | 659 | public int getMaxPosition() { 660 | return maxPosition; 661 | } 662 | 663 | public void setMaxPosition(int maxPosition) { 664 | this.maxPosition = maxPosition; 665 | } 666 | } -------------------------------------------------------------------------------- /app/src/main/java/es/anthorlop/videotrimmer/utils/Screens.java: -------------------------------------------------------------------------------- 1 | package es.anthorlop.videotrimmer.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.Resources; 7 | import android.graphics.Point; 8 | import android.util.DisplayMetrics; 9 | import android.view.Display; 10 | import android.view.WindowManager; 11 | 12 | 13 | public class Screens { 14 | 15 | @SuppressWarnings("deprecation") 16 | /** 17 | * M�todo que devuelve el alto de la pantalla 18 | * 19 | * @param context Context de la aplicaci�n 20 | * 21 | * @return int con la altura 22 | */ 23 | public static int getScreenHeight(Context context) { 24 | 25 | WindowManager wm = (WindowManager) context 26 | .getSystemService(Context.WINDOW_SERVICE); 27 | Display display = wm.getDefaultDisplay(); 28 | return display.getHeight(); 29 | 30 | } 31 | 32 | @SuppressWarnings("deprecation") 33 | /** 34 | * M�todo que devuelve el ancho de la pantalla 35 | * 36 | * @param context Context de la aplicaci�n 37 | * 38 | * @return int con la ancho 39 | */ 40 | public static int getScreenWidth(Context context) { 41 | 42 | WindowManager wm = (WindowManager) context 43 | .getSystemService(Context.WINDOW_SERVICE); 44 | Display display = wm.getDefaultDisplay(); 45 | return display.getWidth(); 46 | 47 | } 48 | 49 | /** 50 | * This method converts dp unit to equivalent pixels, depending on device 51 | * density. 52 | * 53 | * @param dp 54 | * A value in dp (density independent pixels) unit. Which we need 55 | * to convert into pixels 56 | * @param context 57 | * Context to get resources and device specific display metrics 58 | * @return A float value to represent px equivalent to dp depending on 59 | * device density 60 | */ 61 | public static float convertDpToPixel(float dp, Context context) { 62 | Resources resources = context.getResources(); 63 | DisplayMetrics metrics = resources.getDisplayMetrics(); 64 | float px = dp * (metrics.densityDpi / 160f); 65 | return px; 66 | } 67 | 68 | /** 69 | * This method converts device specific pixels to density independent 70 | * pixels. 71 | * 72 | * @param px 73 | * A value in px (pixels) unit. Which we need to convert into db 74 | * @param context 75 | * Context to get resources and device specific display metrics 76 | * @return A float value to represent dp equivalent to px value 77 | */ 78 | public static float convertPixelsToDp(float px, Context context) { 79 | Resources resources = context.getResources(); 80 | DisplayMetrics metrics = resources.getDisplayMetrics(); 81 | float dp = px / (metrics.densityDpi / 160f); 82 | return dp; 83 | } 84 | 85 | /** 86 | * Valida si el dispositivo es una tableta 87 | * 88 | * @param context Context de la aplicaci�n 89 | * @return true si es una tablet 90 | * false si es un m�vil 91 | */ 92 | public static Boolean isTableta(Context context) { 93 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE 94 | || ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE)) { 95 | return true; 96 | } else { 97 | return false; 98 | } 99 | } 100 | 101 | /** 102 | * Valida si el dispositivo es una tableta de tama�o large (is at least approximately 480x640 dp units) 103 | * 104 | * @param context Context de la aplicaci�n 105 | * @return true si es una tablet de tama�o Large 106 | * false si es no lo es 107 | */ 108 | public static Boolean isTabletaLarge(Context context) { 109 | 110 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE) { 111 | return true; 112 | } else { 113 | return false; 114 | } 115 | } 116 | 117 | /** 118 | * Valida si el dispositivo es una tableta de tama�o large (is at least approximately 720x960 dp units) 119 | * 120 | * @param context Context de la aplicaci�n 121 | * @return true si es una tablet de tama�o Large 122 | * false si es no lo es 123 | */ 124 | public static Boolean isTabletXLarge(Context context) { 125 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE) { 126 | return true; 127 | } else { 128 | return false; 129 | } 130 | } 131 | 132 | /** 133 | * Valida si el dispositivo tiene un tama�o de pantalla standard (is at least approximately 320x470 dp units) 134 | * 135 | * @param context Context de la aplicaci�n 136 | * @return true si el tama�o de pantalla es standard 137 | * false si es no lo es 138 | */ 139 | 140 | public static Boolean isNormal(Context context) { 141 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_NORMAL) { 142 | return true; 143 | } else { 144 | return false; 145 | } 146 | } 147 | 148 | /** 149 | * Valida si el dispositivo es una tableta de tama�o peque�o (is at least approximately 320x426 dp units) 150 | * 151 | * @param context Context de la aplicaci�n 152 | * @return true si es m�vil peque�o 153 | * false si es no lo es 154 | */ 155 | public static Boolean isSmall(Context context) { 156 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_SMALL) { 157 | return true; 158 | } else { 159 | return false; 160 | } 161 | } 162 | 163 | /** 164 | * Valida la orientaci�n del dispositivo 165 | * 166 | * @param ctx Context de la aplicaci�n 167 | * 168 | * @return true si la pantalla est� en vertical 169 | * false si est� en horizontal 170 | */ 171 | public static boolean isVertical(Context ctx) { 172 | if (ctx.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) 173 | return true; 174 | else 175 | return false; 176 | } 177 | 178 | /** 179 | * Tama�o de la pantalla 180 | * 181 | * @param display display 182 | * 183 | * @return Point tama�o de la pantalla 184 | */ 185 | public static Point getDisplaySize(final Display display) { 186 | final Point point = new Point(); 187 | try { 188 | display.getSize(point); 189 | } catch (NoSuchMethodError ignore) { // Older device 190 | point.x = display.getWidth(); 191 | point.y = display.getHeight(); 192 | } 193 | return point; 194 | } 195 | 196 | // navigation bar (at the bottom of the screen on a Nexus device) 197 | public static int getNavigationBarHeight(Context ctx) { 198 | Resources resources = ctx.getResources(); 199 | int resourceId = resources.getIdentifier("navigation_bar_height", 200 | "dimen", "android"); 201 | if (resourceId > 0) { 202 | return resources.getDimensionPixelSize(resourceId); 203 | } 204 | return 0; 205 | } 206 | 207 | // navigation bar (at the bottom of the screen on a Nexus device) 208 | public static int getStatusBarHeight(Context ctx) { 209 | Resources resources = ctx.getResources(); 210 | int resourceId = resources.getIdentifier("status_bar_height", "dimen", 211 | "android"); 212 | if (resourceId > 0) { 213 | return resources.getDimensionPixelSize(resourceId); 214 | } 215 | return 0; 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camara_menu_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_camara_menu_switch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flash_off_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_flash_off_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flash_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_flash_on_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_media_fullscreen_shrink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_media_fullscreen_shrink.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_media_fullscreen_stretch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_media_fullscreen_stretch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_media_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_media_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_player_big_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_player_big_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_player_big_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/ic_player_big_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/seek_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthorlop/VideoTrimmer/c41e16ea31ff7df4d696c64be6b5b682ed650bcd/app/src/main/res/drawable/seek_thumb.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 |