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/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java
new file mode 100644
index 0000000..ed0fab9
--- /dev/null
+++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java
@@ -0,0 +1,113 @@
+package com.steelkiwi.videotrimming.view.customVideoViews;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import androidx.annotation.NonNull;
+
+import com.steelkiwi.videotrimming.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/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java
new file mode 100644
index 0000000..b715015
--- /dev/null
+++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java
@@ -0,0 +1,380 @@
+package com.steelkiwi.videotrimming.view.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.steelkiwi.videotrimming.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/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java
new file mode 100644
index 0000000..b0659f7
--- /dev/null
+++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java
@@ -0,0 +1,10 @@
+package com.steelkiwi.videotrimming.view.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/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java
new file mode 100644
index 0000000..683c106
--- /dev/null
+++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java
@@ -0,0 +1,14 @@
+package com.steelkiwi.videotrimming.view.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/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java
new file mode 100644
index 0000000..b449970
--- /dev/null
+++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java
@@ -0,0 +1,239 @@
+package com.steelkiwi.videotrimming.view.customVideoViews;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Build;
+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 androidx.annotation.RequiresApi;
+import com.steelkiwi.videotrimming.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);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ @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, "") {
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @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);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @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 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/android/src/main/res/drawable-mdpi/circle_thumb.xml b/android/src/main/res/drawable-mdpi/circle_thumb.xml
new file mode 100644
index 0000000..39d0f8e
--- /dev/null
+++ b/android/src/main/res/drawable-mdpi/circle_thumb.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/drawable-mdpi/ic_white_pause.png b/android/src/main/res/drawable-mdpi/ic_white_pause.png
new file mode 100644
index 0000000..15e08b5
Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_white_pause.png differ
diff --git a/android/src/main/res/drawable-mdpi/ic_white_play.png b/android/src/main/res/drawable-mdpi/ic_white_play.png
new file mode 100644
index 0000000..33169d5
Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_white_play.png differ
diff --git a/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml b/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml
new file mode 100644
index 0000000..46ca651
--- /dev/null
+++ b/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml
@@ -0,0 +1,19 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/drawable-v24/time_line_a.png b/android/src/main/res/drawable-v24/time_line_a.png
new file mode 100644
index 0000000..6050902
Binary files /dev/null and b/android/src/main/res/drawable-v24/time_line_a.png differ
diff --git a/android/src/main/res/drawable/background_button.xml b/android/src/main/res/drawable/background_button.xml
deleted file mode 100644
index 551b010..0000000
--- a/android/src/main/res/drawable/background_button.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/src/main/res/drawable/ic_baseline_clear_24.xml b/android/src/main/res/drawable/ic_baseline_clear_24.xml
deleted file mode 100644
index af9bff5..0000000
--- a/android/src/main/res/drawable/ic_baseline_clear_24.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/android/src/main/res/drawable/ic_baseline_done_24.xml b/android/src/main/res/drawable/ic_baseline_done_24.xml
deleted file mode 100644
index dc9b7e7..0000000
--- a/android/src/main/res/drawable/ic_baseline_done_24.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml b/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml
deleted file mode 100644
index 9474032..0000000
--- a/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/android/src/main/res/drawable/ic_videocam_black_24dp.xml b/android/src/main/res/drawable/ic_videocam_black_24dp.xml
deleted file mode 100644
index 923a07f..0000000
--- a/android/src/main/res/drawable/ic_videocam_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/android/src/main/res/drawable/icon_video_play.png b/android/src/main/res/drawable/icon_video_play.png
deleted file mode 100644
index 0498cad..0000000
Binary files a/android/src/main/res/drawable/icon_video_play.png and /dev/null differ
diff --git a/android/src/main/res/drawable/launch_background.xml b/android/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f..0000000
--- a/android/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/android/src/main/res/layout/activity_main.xml b/android/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 4f22107..0000000
--- a/android/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/src/main/res/layout/activity_trimmer.xml b/android/src/main/res/layout/activity_trimmer.xml
index 020816a..ae2f109 100644
--- a/android/src/main/res/layout/activity_trimmer.xml
+++ b/android/src/main/res/layout/activity_trimmer.xml
@@ -2,10 +2,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/layout/video_trimmer.xml b/android/src/main/res/layout/video_trimmer.xml
deleted file mode 100644
index 7e61ae1..0000000
--- a/android/src/main/res/layout/video_trimmer.xml
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml
index 3ab3e9c..e6431ba 100644
--- a/android/src/main/res/values/colors.xml
+++ b/android/src/main/res/values/colors.xml
@@ -3,4 +3,12 @@
#3F51B5
#303F9F
#FF4081
+
+ #65b7b7b7
+ #0e1f2f
+ #000000
+ #3be3e3
+ #FF15FF00
+ #00000000
+ #ffffff
diff --git a/android/src/main/res/values/dimen.xml b/android/src/main/res/values/dimen.xml
new file mode 100644
index 0000000..a6dfba3
--- /dev/null
+++ b/android/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/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 6dc2c0e..7a501ee 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -6,5 +6,10 @@
Video saves at : $%1s
Select or record a a video below to try it out:
sec
-
+ Save
+ Cancel
+ Done
+ Select 1 min video
+ Video should be of minimum 3 seconds
+ Should allow permission to work app correct
diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml
index e917519..949fbe6 100644
--- a/android/src/main/res/values/styles.xml
+++ b/android/src/main/res/values/styles.xml
@@ -1,12 +1,5 @@
-
-
-
+
+
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index bc73f07..9d54427 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -40,8 +40,10 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.steelkiwi.videotrimming_example"
- minSdkVersion 21
- targetSdkVersion 29
+ minSdkVersion 26
+ targetSdkVersion 30
+ multiDexEnabled true
+
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -61,4 +63,6 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'com.android.support:multidex:1.0.3'
+
}
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 82cbb58..a6b142b 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 296b146..59dd2e1 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jun 23 08:50:38 CEST 2017
+#Fri Mar 12 19:02:36 EET 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/example/android/settings_aar.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/example/lib/main.dart b/example/lib/main.dart
index fd693f4..f257260 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -63,10 +63,12 @@ class _MyHomePageState extends State {
}
_pickVideo() async {
if (await Permission.storage.request().isGranted) {
- File selectedFile = await FilePicker.getFile(type: FileType.video);
- selectedPath = selectedFile.path;
+
+ FilePickerResult selectedFile = await FilePicker.platform.pickFiles(type: FileType.video);
+
+ selectedPath = selectedFile.files.single.path;
var trimmedFile =
- await VideoTrimming.trimVideo(sourcePath: selectedFile.path);
+ await VideoTrimming.trimVideo(sourcePath: selectedPath);
trimmedPath = trimmedFile.path;
_controller = VideoPlayerController.file(trimmedFile);
@@ -80,5 +82,5 @@ class _MyHomePageState extends State {
].request();
print(statuses[Permission.location]);
- }
+ }
}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 559321a..bf66916 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -63,14 +63,7 @@ packages:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
- version: "1.11.0+3"
- file_picker_platform_interface:
- dependency: transitive
- description:
- name: file_picker_platform_interface
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.2.0"
+ version: "2.1.7"
flutter:
dependency: "direct main"
description: flutter
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 134bb10..816bf08 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -23,7 +23,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
- file_picker: ^1.10.0
+ file_picker: ^2.1.4
permission_handler: ^5.0.1
video_player: ^0.10.11+1
dev_dependencies: