CURRENT_SERIAL = new ThreadLocal<>();
+
+ private BackgroundTask() {
+ }
+
+ /**
+ * Execute a runnable after the given delay.
+ *
+ * @param runnable the task to execute
+ * @param delay the time from now to delay execution, in milliseconds
+ *
+ * if delay
is strictly positive and the current
+ * executor does not support scheduling (if
+ * Executor has been called with such an
+ * executor)
+ * @return Future associated to the running task
+ * @throws IllegalArgumentException if the current executor set by Executor
+ * does not support scheduling
+ */
+ private static Future> directExecute(Runnable runnable, long delay) {
+ Future> future = null;
+ if (delay > 0) {
+ /* no serial, but a delay: schedule the task */
+ if (!(executor instanceof ScheduledExecutorService)) {
+ throw new IllegalArgumentException("The executor set does not support scheduling");
+ }
+ ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor;
+ future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS);
+ } else {
+ if (executor instanceof ExecutorService) {
+ ExecutorService executorService = (ExecutorService) executor;
+ future = executorService.submit(runnable);
+ } else {
+ /* non-cancellable task */
+ executor.execute(runnable);
+ }
+ }
+ return future;
+ }
+
+ /**
+ * Execute a task after (at least) its delay and after all
+ * tasks added with the same non-null serial
(if any) have
+ * completed execution.
+ *
+ * @param task the task to execute
+ * @throws IllegalArgumentException if task.delay
is strictly positive and the
+ * current executor does not support scheduling (if
+ * Executor has been called with such an
+ * executor)
+ */
+ public static synchronized void execute(Task task) {
+ Future> future = null;
+ if (task.serial == null || !hasRunning(task.serial)) {
+ task.executionAsked = true;
+ future = directExecute(task, task.remainingDelay);
+ }
+ if ((task.id != null || task.serial != null) && !task.managed.get()) {
+ /* keep task */
+ task.future = future;
+ TASKS.add(task);
+ }
+ }
+
+ /**
+ * Indicates whether a task with the specified serial
has been
+ * submitted to the executor.
+ *
+ * @param serial the serial queue
+ * @return true
if such a task has been submitted,
+ * false
otherwise
+ */
+ private static boolean hasRunning(String serial) {
+ for (Task task : TASKS) {
+ if (task.executionAsked && serial.equals(task.serial)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve and remove the first task having the specified
+ * serial
(if any).
+ *
+ * @param serial the serial queue
+ * @return task if found, null
otherwise
+ */
+ private static Task take(String serial) {
+ int len = TASKS.size();
+ for (int i = 0; i < len; i++) {
+ if (serial.equals(TASKS.get(i).serial)) {
+ return TASKS.remove(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Cancel all tasks having the specified id
.
+ *
+ * @param id the cancellation identifier
+ * @param mayInterruptIfRunning true
if the thread executing this task should be
+ * interrupted; otherwise, in-progress tasks are allowed to
+ * complete
+ */
+ public static synchronized void cancelAllTask(String id, boolean mayInterruptIfRunning) {
+ for (int i = TASKS.size() - 1; i >= 0; i--) {
+ Task task = TASKS.get(i);
+ if (id.equals(task.id)) {
+ if (task.future != null) {
+ task.future.cancel(mayInterruptIfRunning);
+ if (!task.managed.getAndSet(true)) {
+ /*
+ * the task has been submitted to the executor, but its
+ * execution has not started yet, so that its run()
+ * method will never call postExecute()
+ */
+ task.postExecute();
+ }
+ } else if (task.executionAsked) {
+ Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)");
+ } else {
+ /* this task has not been submitted to the executor */
+ TASKS.remove(i);
+ }
+ }
+ }
+ }
+
+ public static abstract class Task implements Runnable {
+
+ private String id;
+ private long remainingDelay;
+ private long targetTimeMillis; /* since epoch */
+ private String serial;
+ private boolean executionAsked;
+ private Future> future;
+
+ /*
+ * A task can be cancelled after it has been submitted to the executor
+ * but before its run() method is called. In that case, run() will never
+ * be called, hence neither will postExecute(): the tasks with the same
+ * serial identifier (if any) will never be submitted.
+ *
+ * Therefore, cancelAllTask() *must* call postExecute() if run() is not
+ * started.
+ *
+ * This flag guarantees that either cancelAllTask() or run() manages this
+ * task post execution, but not both.
+ */
+ private AtomicBoolean managed = new AtomicBoolean();
+
+ protected Task(String id, long delay, String serial) {
+ if (!"".equals(id)) {
+ this.id = id;
+ }
+ if (delay > 0) {
+ remainingDelay = delay;
+ targetTimeMillis = System.currentTimeMillis() + delay;
+ }
+ if (!"".equals(serial)) {
+ this.serial = serial;
+ }
+ }
+
+ @Override
+ public void run() {
+ if (managed.getAndSet(true)) {
+ /* cancelled and postExecute() already called */
+ return;
+ }
+
+ try {
+ CURRENT_SERIAL.set(serial);
+ execute();
+ } finally {
+ /* handle next tasks */
+ postExecute();
+ }
+ }
+
+ public abstract void execute();
+
+ private void postExecute() {
+ if (id == null && serial == null) {
+ /* nothing to do */
+ return;
+ }
+ CURRENT_SERIAL.set(null);
+ synchronized (BackgroundTask.class) {
+ /* execution complete */
+ TASKS.remove(this);
+
+ if (serial != null) {
+ Task next = take(serial);
+ if (next != null) {
+ if (next.remainingDelay != 0) {
+ /* the delay may not have elapsed yet */
+ next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis());
+ }
+ /* a task having the same serial was queued, execute it */
+ BackgroundTask.execute(next);
+ }
+ }
+ }
+ }
+ }
+
+
+}
+
diff --git a/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/BarThumb.java b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/BarThumb.java
new file mode 100644
index 0000000..048a77c
--- /dev/null
+++ b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/BarThumb.java
@@ -0,0 +1,127 @@
+// The MIT License (MIT)
+
+// Copyright (c) 2018 Intuz Solutions Pvt Ltd.
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
+// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
+// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package com.ahmedbadereldin.videotrimmer.customVideoViews;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import androidx.annotation.NonNull;
+
+import com.ahmedbadereldin.videotrimmerapplication.R;
+
+import java.util.List;
+import java.util.Vector;
+
+
+public class BarThumb {
+
+ public static final int LEFT = 0;
+ public static final int RIGHT = 1;
+
+ private int mIndex;
+ private float mVal;
+ private float mPos;
+ private Bitmap mBitmap;
+ private int mWidthBitmap;
+ private int mHeightBitmap;
+
+ private float mLastTouchX;
+
+ private BarThumb() {
+ mVal = 0;
+ mPos = 0;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ private void setIndex(int index) {
+ mIndex = index;
+ }
+
+ public float getVal() {
+ return mVal;
+ }
+
+ public void setVal(float val) {
+ mVal = val;
+ }
+
+ public float getPos() {
+ return mPos;
+ }
+
+ public void setPos(float pos) {
+ mPos = pos;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ private void setBitmap(@NonNull Bitmap bitmap) {
+ mBitmap = bitmap;
+ mWidthBitmap = bitmap.getWidth();
+ mHeightBitmap = bitmap.getHeight();
+ }
+
+ @NonNull
+ public static List initThumbs(Resources resources) {
+
+ List barThumbs = new Vector<>();
+
+ for (int i = 0; i < 2; i++) {
+ BarThumb th = new BarThumb();
+ th.setIndex(i);
+ if (i == 0) {
+ int resImageLeft = R.drawable.time_line_a;
+ th.setBitmap(BitmapFactory.decodeResource(resources, resImageLeft));
+ } else {
+ int resImageRight = R.drawable.time_line_a;
+ th.setBitmap(BitmapFactory.decodeResource(resources, resImageRight));
+ }
+
+ barThumbs.add(th);
+ }
+
+ return barThumbs;
+ }
+
+ public static int getWidthBitmap(@NonNull List barThumbs) {
+ return barThumbs.get(0).getWidthBitmap();
+ }
+
+ public static int getHeightBitmap(@NonNull List barThumbs) {
+ return barThumbs.get(0).getHeightBitmap();
+ }
+
+ public float getLastTouchX() {
+ return mLastTouchX;
+ }
+
+ public void setLastTouchX(float lastTouchX) {
+ mLastTouchX = lastTouchX;
+ }
+
+ public int getWidthBitmap() {
+ return mWidthBitmap;
+ }
+
+ private int getHeightBitmap() {
+ return mHeightBitmap;
+ }
+}
diff --git a/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/CustomRangeSeekBar.java b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/CustomRangeSeekBar.java
new file mode 100644
index 0000000..7abe929
--- /dev/null
+++ b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/CustomRangeSeekBar.java
@@ -0,0 +1,380 @@
+package com.ahmedbadereldin.videotrimmer.customVideoViews;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import com.ahmedbadereldin.videotrimmerapplication.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CustomRangeSeekBar extends View {
+
+ private int mHeightTimeLine;
+ private List mBarThumbs;
+ private List mListeners;
+ private float mMaxWidth;
+ private float mThumbWidth;
+ private float mThumbHeight;
+ private int mViewWidth;
+ private float mPixelRangeMin;
+ private float mPixelRangeMax;
+ private float mScaleRangeMax;
+ private boolean mFirstRun;
+
+ private final Paint mShadow = new Paint();
+ private final Paint mLine = new Paint();
+
+ public CustomRangeSeekBar(@NonNull Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomRangeSeekBar(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ mBarThumbs = BarThumb.initThumbs(getResources());
+ mThumbWidth = BarThumb.getWidthBitmap(mBarThumbs);
+ mThumbHeight = BarThumb.getHeightBitmap(mBarThumbs);
+
+ mScaleRangeMax = 100;
+ mHeightTimeLine = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ mFirstRun = true;
+
+ int shadowColor = ContextCompat.getColor(getContext(), R.color.shadow_color);
+ mShadow.setAntiAlias(true);
+ mShadow.setColor(shadowColor);
+ mShadow.setAlpha(177);
+
+ int lineColor = ContextCompat.getColor(getContext(), R.color.line_color);
+ mLine.setAntiAlias(true);
+ mLine.setColor(lineColor);
+ mLine.setAlpha(200);
+ }
+
+ public void initMaxWidth() {
+ mMaxWidth = mBarThumbs.get(1).getPos() - mBarThumbs.get(0).getPos();
+
+ onSeekStop(this, 0, mBarThumbs.get(0).getVal());
+ onSeekStop(this, 1, mBarThumbs.get(1).getVal());
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
+ mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1);
+
+ int minH = getPaddingBottom() + getPaddingTop() + (int) mThumbHeight;
+ int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1);
+
+ setMeasuredDimension(mViewWidth, viewHeight);
+
+ mPixelRangeMin = 0;
+ mPixelRangeMax = mViewWidth - mThumbWidth;
+
+ if (mFirstRun) {
+ for (int i = 0; i < mBarThumbs.size(); i++) {
+ BarThumb th = mBarThumbs.get(i);
+ th.setVal(mScaleRangeMax * i);
+ th.setPos(mPixelRangeMax * i);
+ }
+ // Fire listener callback
+ onCreate(this, currentThumb, getThumbValue(currentThumb));
+ mFirstRun = false;
+ }
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawShadow(canvas);
+ drawThumbs(canvas);
+ }
+
+ private int currentThumb = 0;
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent ev) {
+ final BarThumb mBarThumb;
+ final BarThumb mBarThumb2;
+ final float coordinate = ev.getX();
+ final int action = ev.getAction();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ // Remember where we started
+ currentThumb = getClosestThumb(coordinate);
+
+ if (currentThumb == -1) {
+ return false;
+ }
+
+ mBarThumb = mBarThumbs.get(currentThumb);
+ mBarThumb.setLastTouchX(coordinate);
+ onSeekStart(this, currentThumb, mBarThumb.getVal());
+ return true;
+ }
+ case MotionEvent.ACTION_UP: {
+
+ if (currentThumb == -1) {
+ return false;
+ }
+
+ mBarThumb = mBarThumbs.get(currentThumb);
+ onSeekStop(this, currentThumb, mBarThumb.getVal());
+ return true;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ mBarThumb = mBarThumbs.get(currentThumb);
+ mBarThumb2 = mBarThumbs.get(currentThumb == 0 ? 1 : 0);
+ // Calculate the distance moved
+ final float dx = coordinate - mBarThumb.getLastTouchX();
+ final float newX = mBarThumb.getPos() + dx;
+
+ if (currentThumb == 0) {
+
+ if ((newX + mBarThumb.getWidthBitmap()) >= mBarThumb2.getPos()) {
+ mBarThumb.setPos(mBarThumb2.getPos() - mBarThumb.getWidthBitmap());
+ } else if (newX <= mPixelRangeMin) {
+ mBarThumb.setPos(mPixelRangeMin);
+ if ((mBarThumb2.getPos() - (mBarThumb.getPos() + dx)) > mMaxWidth) {
+ mBarThumb2.setPos(mBarThumb.getPos() + dx + mMaxWidth);
+ setThumbPos(1, mBarThumb2.getPos());
+ }
+ } else {
+ //Check if thumb is not out of max width
+// checkPositionThumb(mBarThumb, mBarThumb2, dx, true, coordinate);
+ if ((mBarThumb2.getPos() - (mBarThumb.getPos() + dx)) > mMaxWidth) {
+ mBarThumb2.setPos(mBarThumb.getPos() + dx + mMaxWidth);
+ setThumbPos(1, mBarThumb2.getPos());
+ }
+ // Move the object
+ mBarThumb.setPos(mBarThumb.getPos() + dx);
+
+ // Remember this touch position for the next move event
+ mBarThumb.setLastTouchX(coordinate);
+ }
+
+ } else {
+ if (newX <= mBarThumb2.getPos() + mBarThumb2.getWidthBitmap()) {
+ mBarThumb.setPos(mBarThumb2.getPos() + mBarThumb.getWidthBitmap());
+ } else if (newX >= mPixelRangeMax) {
+ mBarThumb.setPos(mPixelRangeMax);
+ if (((mBarThumb.getPos() + dx) - mBarThumb2.getPos()) > mMaxWidth) {
+ mBarThumb2.setPos(mBarThumb.getPos() + dx - mMaxWidth);
+ setThumbPos(0, mBarThumb2.getPos());
+ }
+ } else {
+ //Check if thumb is not out of max width
+// checkPositionThumb(mBarThumb2, mBarThumb, dx, false, coordinate);
+ if (((mBarThumb.getPos() + dx) - mBarThumb2.getPos()) > mMaxWidth) {
+ mBarThumb2.setPos(mBarThumb.getPos() + dx - mMaxWidth);
+ setThumbPos(0, mBarThumb2.getPos());
+ }
+ // Move the object
+ mBarThumb.setPos(mBarThumb.getPos() + dx);
+ // Remember this touch position for the next move event
+ mBarThumb.setLastTouchX(coordinate);
+ }
+ }
+
+ setThumbPos(currentThumb, mBarThumb.getPos());
+
+ // Invalidate to request a redraw
+ invalidate();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void checkPositionThumb(@NonNull BarThumb mBarThumbLeft, @NonNull BarThumb mBarThumbRight, float dx, boolean isLeftMove, float coordinate) {
+
+ if (isLeftMove && dx < 0) {
+ if ((mBarThumbRight.getPos() - (mBarThumbLeft.getPos() + dx)) > mMaxWidth) {
+ mBarThumbRight.setPos(mBarThumbLeft.getPos() + dx + mMaxWidth);
+ setThumbPos(1, mBarThumbRight.getPos());
+ }
+ } else if (!isLeftMove && dx > 0) {
+ if (((mBarThumbRight.getPos() + dx) - mBarThumbLeft.getPos()) > mMaxWidth) {
+ mBarThumbLeft.setPos(mBarThumbRight.getPos() + dx - mMaxWidth);
+ setThumbPos(0, mBarThumbLeft.getPos());
+ }
+ }
+
+ }
+
+
+ private float pixelToScale(int index, float pixelValue) {
+ float scale = (pixelValue * 100) / mPixelRangeMax;
+ if (index == 0) {
+ float pxThumb = (scale * mThumbWidth) / 100;
+ return scale + (pxThumb * 100) / mPixelRangeMax;
+ } else {
+ float pxThumb = ((100 - scale) * mThumbWidth) / 100;
+ return scale - (pxThumb * 100) / mPixelRangeMax;
+ }
+ }
+
+ private float scaleToPixel(int index, float scaleValue) {
+ float px = (scaleValue * mPixelRangeMax) / 100;
+ if (index == 0) {
+ float pxThumb = (scaleValue * mThumbWidth) / 100;
+ return px - pxThumb;
+ } else {
+ float pxThumb = ((100 - scaleValue) * mThumbWidth) / 100;
+ return px + pxThumb;
+ }
+ }
+
+ private void calculateThumbValue(int index) {
+ if (index < mBarThumbs.size() && !mBarThumbs.isEmpty()) {
+ BarThumb th = mBarThumbs.get(index);
+ th.setVal(pixelToScale(index, th.getPos()));
+ onSeek(this, index, th.getVal());
+ }
+ }
+
+ private void calculateThumbPos(int index) {
+ if (index < mBarThumbs.size() && !mBarThumbs.isEmpty()) {
+ BarThumb th = mBarThumbs.get(index);
+ th.setPos(scaleToPixel(index, th.getVal()));
+ }
+ }
+
+ private float getThumbValue(int index) {
+ return mBarThumbs.get(index).getVal();
+ }
+
+ public void setThumbValue(int index, float value) {
+ mBarThumbs.get(index).setVal(value);
+ calculateThumbPos(index);
+ // Tell the view we want a complete redraw
+ invalidate();
+ }
+
+ private void setThumbPos(int index, float pos) {
+ mBarThumbs.get(index).setPos(pos);
+ calculateThumbValue(index);
+ // Tell the view we want a complete redraw
+ invalidate();
+ }
+
+ private int getClosestThumb(float coordinate) {
+ int closest = -1;
+ if (!mBarThumbs.isEmpty()) {
+ for (int i = 0; i < mBarThumbs.size(); i++) {
+ // Find thumb closest to x coordinate
+ final float tcoordinate = mBarThumbs.get(i).getPos() + mThumbWidth;
+ if (coordinate >= mBarThumbs.get(i).getPos() && coordinate <= tcoordinate) {
+ closest = mBarThumbs.get(i).getIndex();
+ }
+ }
+ }
+ return closest;
+ }
+
+ private void drawShadow(@NonNull Canvas canvas) {
+ if (!mBarThumbs.isEmpty()) {
+
+ for (BarThumb th : mBarThumbs) {
+ if (th.getIndex() == 0) {
+ final float x = th.getPos();
+ if (x > mPixelRangeMin) {
+ Rect mRect = new Rect(0, (int) (mThumbHeight - mHeightTimeLine) / 2,
+ (int) (x + (mThumbWidth / 2)), mHeightTimeLine + (int) (mThumbHeight - mHeightTimeLine) / 2);
+ canvas.drawRect(mRect, mShadow);
+ }
+ } else {
+ final float x = th.getPos();
+ if (x < mPixelRangeMax) {
+ Rect mRect = new Rect((int) (x + (mThumbWidth / 2)), (int) (mThumbHeight - mHeightTimeLine) / 2,
+ (mViewWidth), mHeightTimeLine + (int) (mThumbHeight - mHeightTimeLine) / 2);
+ canvas.drawRect(mRect, mShadow);
+ }
+ }
+ }
+ }
+ }
+
+ private void drawThumbs(@NonNull Canvas canvas) {
+
+ if (!mBarThumbs.isEmpty()) {
+ for (BarThumb th : mBarThumbs) {
+ if (th.getIndex() == 0) {
+ canvas.drawBitmap(th.getBitmap(), th.getPos() + getPaddingLeft(), getPaddingTop(), null);
+ } else {
+ canvas.drawBitmap(th.getBitmap(), th.getPos() - getPaddingRight(), getPaddingTop(), null);
+ }
+ }
+ }
+ }
+
+ public void addOnRangeSeekBarListener(OnRangeSeekBarChangeListener listener) {
+
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ }
+
+ mListeners.add(listener);
+ }
+
+ private void onCreate(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) {
+ if (mListeners == null)
+ return;
+
+ for (OnRangeSeekBarChangeListener item : mListeners) {
+ item.onCreate(CustomRangeSeekBar, index, value);
+ }
+ }
+
+ private void onSeek(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) {
+ if (mListeners == null)
+ return;
+
+ for (OnRangeSeekBarChangeListener item : mListeners) {
+ item.onSeek(CustomRangeSeekBar, index, value);
+ }
+ }
+
+ private void onSeekStart(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) {
+ if (mListeners == null)
+ return;
+
+ for (OnRangeSeekBarChangeListener item : mListeners) {
+ item.onSeekStart(CustomRangeSeekBar, index, value);
+ }
+ }
+
+ private void onSeekStop(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) {
+ if (mListeners == null)
+ return;
+
+ for (OnRangeSeekBarChangeListener item : mListeners) {
+ item.onSeekStop(CustomRangeSeekBar, index, value);
+ }
+ }
+
+ public List getThumbs() {
+ return mBarThumbs;
+ }
+}
diff --git a/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnRangeSeekBarChangeListener.java b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnRangeSeekBarChangeListener.java
new file mode 100644
index 0000000..36c3ecd
--- /dev/null
+++ b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnRangeSeekBarChangeListener.java
@@ -0,0 +1,10 @@
+package com.ahmedbadereldin.videotrimmer.customVideoViews;
+public interface OnRangeSeekBarChangeListener {
+ void onCreate(CustomRangeSeekBar CustomRangeSeekBar, int index, float value);
+
+ void onSeek(CustomRangeSeekBar CustomRangeSeekBar, int index, float value);
+
+ void onSeekStart(CustomRangeSeekBar CustomRangeSeekBar, int index, float value);
+
+ void onSeekStop(CustomRangeSeekBar CustomRangeSeekBar, int index, float value);
+}
diff --git a/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnVideoTrimListener.java b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnVideoTrimListener.java
new file mode 100644
index 0000000..1528c5e
--- /dev/null
+++ b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/OnVideoTrimListener.java
@@ -0,0 +1,14 @@
+package com.ahmedbadereldin.videotrimmer.customVideoViews;
+
+import android.net.Uri;
+
+public interface OnVideoTrimListener {
+
+ void onTrimStarted();
+
+ void getResult(final Uri uri);
+
+ void cancelAction();
+
+ void onError(final String message);
+}
diff --git a/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/TileView.java b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/TileView.java
new file mode 100644
index 0000000..65ee53e
--- /dev/null
+++ b/videotrimmer/src/main/java/com/ahmedbadereldin/videotrimmer/customVideoViews/TileView.java
@@ -0,0 +1,237 @@
+package com.ahmedbadereldin.videotrimmer.customVideoViews;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.LongSparseArray;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+
+import com.ahmedbadereldin.videotrimmer.R;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TileView extends View {
+
+ private Uri mVideoUri;
+ private int mHeightView;
+ private LongSparseArray mBitmapList = null;
+ private int viewWidth = 0;
+ private int viewHeight = 0;
+
+ public TileView(@NonNull Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TileView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ mHeightView = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
+ int w = resolveSizeAndState(minW, widthMeasureSpec, 1);
+
+ final int minH = getPaddingBottom() + getPaddingTop() + mHeightView;
+ int h = resolveSizeAndState(minH, heightMeasureSpec, 1);
+
+ setMeasuredDimension(w, h);
+ }
+
+ @Override
+ protected void onSizeChanged(final int w, int h, final int oldW, int oldH) {
+ super.onSizeChanged(w, h, oldW, oldH);
+ viewWidth = w;
+ viewHeight = h;
+ if (w != oldW) {
+ if (mVideoUri != null)
+ getBitmap();
+ }
+ }
+
+ private void getBitmap() {
+ BackgroundTask
+ .execute(new BackgroundTask.Task("", 0L, "") {
+ @Override
+ public void execute() {
+ try {
+ LongSparseArray thumbnailList = new LongSparseArray<>();
+
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ mediaMetadataRetriever.setDataSource(getContext(), mVideoUri);
+
+ // Retrieve media data
+ long videoLengthInMs = Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000;
+
+ // Set thumbnail properties (Thumbs are squares)
+ final int thumbWidth = mHeightView;
+ final int thumbHeight = mHeightView;
+
+ int numThumbs = (int) Math.ceil(((float) viewWidth) / thumbWidth);
+
+ final long interval = videoLengthInMs / numThumbs;
+
+ for (int i = 0; i < numThumbs; ++i) {
+ Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
+ // TODO: bitmap might be null here, hence throwing NullPointerException. You were right
+ try {
+ bitmap = Bitmap.createScaledBitmap(bitmap, thumbWidth, thumbHeight, false);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ thumbnailList.put(i, bitmap);
+ }
+
+ mediaMetadataRetriever.release();
+ returnBitmaps(thumbnailList);
+ } catch (final Throwable e) {
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
+ }
+ }
+ }
+ );
+ }
+
+ private void returnBitmaps(final LongSparseArray thumbnailList) {
+ new MainThreadExecutor().runTask("", new Runnable() {
+ @Override
+ public void run() {
+ mBitmapList = thumbnailList;
+ invalidate();
+ }
+ }
+ , 0L);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mBitmapList != null) {
+ canvas.save();
+ int x = 0;
+
+ for (int i = 0; i < mBitmapList.size(); i++) {
+ Bitmap bitmap = mBitmapList.get(i);
+
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, x, 0, null);
+ x = x + bitmap.getWidth();
+ }
+ }
+ }
+ }
+
+ public void setVideo(@NonNull Uri data) {
+ mVideoUri = data;
+ getBitmap();
+ }
+
+ public final class MainThreadExecutor {
+
+ private final Handler HANDLER = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ Runnable callback = msg.getCallback();
+ if (callback != null) {
+ callback.run();
+ decrementToken((Token) msg.obj);
+ } else {
+ super.handleMessage(msg);
+ }
+ }
+ };
+
+ private final Map TOKENS = new HashMap<>();
+
+ private MainThreadExecutor() {
+ // should not be instantiated
+ }
+
+ /**
+ * Store a new task in the map for providing cancellation. This method is
+ * used by AndroidAnnotations and not intended to be called by clients.
+ *
+ * @param id the identifier of the task
+ * @param task the task itself
+ * @param delay the delay or zero to run immediately
+ */
+ public void runTask(String id, Runnable task, long delay) {
+ if ("".equals(id)) {
+ HANDLER.postDelayed(task, delay);
+ return;
+ }
+ long time = SystemClock.uptimeMillis() + delay;
+ HANDLER.postAtTime(task, nextToken(id), time);
+ }
+
+ private Token nextToken(String id) {
+ synchronized (TOKENS) {
+ Token token = TOKENS.get(id);
+ if (token == null) {
+ token = new MainThreadExecutor.Token(id);
+ TOKENS.put(id, token);
+ }
+ token.runnablesCount++;
+ return token;
+ }
+ }
+
+ private void decrementToken(Token token) {
+ synchronized (TOKENS) {
+ if (--token.runnablesCount == 0) {
+ String id = token.id;
+ Token old = TOKENS.remove(id);
+ if (old != token) {
+ // a runnable finished after cancelling, we just removed a
+ // wrong token, lets put it back
+ TOKENS.put(id, old);
+ }
+ }
+ }
+ }
+
+ /**
+ * Cancel all tasks having the specified id
.
+ *
+ * @param id the cancellation identifier
+ */
+ public void cancelAll(String id) {
+ Token token;
+ synchronized (TOKENS) {
+ token = TOKENS.remove(id);
+ }
+ if (token == null) {
+ // nothing to cancel
+ return;
+ }
+ HANDLER.removeCallbacksAndMessages(token);
+ }
+
+ private final class Token {
+ int runnablesCount = 0;
+ final String id;
+
+ private Token(String id) {
+ this.id = id;
+ }
+ }
+
+ }
+
+}
diff --git a/videotrimmer/src/main/res/drawable/circle_thumb.xml b/videotrimmer/src/main/res/drawable/circle_thumb.xml
new file mode 100644
index 0000000..39d0f8e
--- /dev/null
+++ b/videotrimmer/src/main/res/drawable/circle_thumb.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/drawable/ic_white_pause.png b/videotrimmer/src/main/res/drawable/ic_white_pause.png
new file mode 100644
index 0000000..15e08b5
Binary files /dev/null and b/videotrimmer/src/main/res/drawable/ic_white_pause.png differ
diff --git a/videotrimmer/src/main/res/drawable/ic_white_play.png b/videotrimmer/src/main/res/drawable/ic_white_play.png
new file mode 100644
index 0000000..33169d5
Binary files /dev/null and b/videotrimmer/src/main/res/drawable/ic_white_play.png differ
diff --git a/videotrimmer/src/main/res/drawable/round_corner_white_fill3.xml b/videotrimmer/src/main/res/drawable/round_corner_white_fill3.xml
new file mode 100644
index 0000000..704ac04
--- /dev/null
+++ b/videotrimmer/src/main/res/drawable/round_corner_white_fill3.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/drawable/seekbar_drawable_video.xml b/videotrimmer/src/main/res/drawable/seekbar_drawable_video.xml
new file mode 100644
index 0000000..46ca651
--- /dev/null
+++ b/videotrimmer/src/main/res/drawable/seekbar_drawable_video.xml
@@ -0,0 +1,19 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/drawable/time_line_a.png b/videotrimmer/src/main/res/drawable/time_line_a.png
new file mode 100644
index 0000000..6050902
Binary files /dev/null and b/videotrimmer/src/main/res/drawable/time_line_a.png differ
diff --git a/videotrimmer/src/main/res/layout/activity_video_trim.xml b/videotrimmer/src/main/res/layout/activity_video_trim.xml
new file mode 100644
index 0000000..42a9d5b
--- /dev/null
+++ b/videotrimmer/src/main/res/layout/activity_video_trim.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/values/colors.xml b/videotrimmer/src/main/res/values/colors.xml
new file mode 100644
index 0000000..703bd70
--- /dev/null
+++ b/videotrimmer/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
+ #65b7b7b7
+ #0e1f2f
+ #000000
+ #3be3e3
+ #FF15FF00
+ #00000000
+ #ffffff
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/values/dimen.xml b/videotrimmer/src/main/res/values/dimen.xml
new file mode 100644
index 0000000..a6dfba3
--- /dev/null
+++ b/videotrimmer/src/main/res/values/dimen.xml
@@ -0,0 +1,14 @@
+
+
+
+ @dimen/_8sdp
+ @dimen/_5sdp
+ @dimen/_165sdp
+ @dimen/_28sdp
+
+ @dimen/_19ssp
+ @dimen/_12sdp
+
+ 62dp
+ 150dp
+
diff --git a/videotrimmer/src/main/res/values/strings.xml b/videotrimmer/src/main/res/values/strings.xml
new file mode 100644
index 0000000..77e42c3
--- /dev/null
+++ b/videotrimmer/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+ VideoTrimmerApplication
+ Save
+ Cancel
+ Done
+ Select 1 min video
+ Video should be of minimum 3 seconds
+ Should allow permission to work app correct
+ Some Permissions were Denied
+ File format is not supported
+ com.demo.videotrimmer.provider
+
\ No newline at end of file
diff --git a/videotrimmer/src/main/res/values/styles.xml b/videotrimmer/src/main/res/values/styles.xml
new file mode 100644
index 0000000..0fe5728
--- /dev/null
+++ b/videotrimmer/src/main/res/values/styles.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/videotrimmer/src/test/java/com/ahmedbadereldin/videotrimmer/ExampleUnitTest.kt b/videotrimmer/src/test/java/com/ahmedbadereldin/videotrimmer/ExampleUnitTest.kt
new file mode 100644
index 0000000..75198b1
--- /dev/null
+++ b/videotrimmer/src/test/java/com/ahmedbadereldin/videotrimmer/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.ahmedbadereldin.videotrimmer
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file