react-native-video-trim 6.0.2 → 6.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,6 +99,13 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
99
99
  private boolean isZoomedIn = false;
100
100
  private final Handler zoomWaitTimer = new Handler();
101
101
  private Runnable zoomRunnable;
102
+ private long zoomedInRangeStart = 0;
103
+ private long zoomedInRangeDuration = 0;
104
+ private boolean isTrimmingLeading = false;
105
+
106
+ // thumbnail caching for zoom functionality
107
+ private final java.util.List<ImageView> cachedFullViewThumbnails = new java.util.ArrayList<>();
108
+ private volatile boolean isGeneratingThumbnails = false;
102
109
 
103
110
  private MediaMetadataRetriever mediaMetadataRetriever;
104
111
  private ProgressBar loadingIndicator;
@@ -244,6 +251,8 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
244
251
 
245
252
  private void startShootVideoThumbs(final Context context, int totalThumbsCount, long startPosition, long endPosition) {
246
253
  mThumbnailContainer.removeAllViews();
254
+ cachedFullViewThumbnails.clear(); // Clear previous cache
255
+
247
256
  VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever, totalThumbsCount, startPosition, endPosition,
248
257
  (bitmap, interval) -> {
249
258
  if (bitmap != null) {
@@ -255,6 +264,13 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
255
264
  layoutParams.width = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
256
265
  thumbImageView.setLayoutParams(layoutParams);
257
266
  mThumbnailContainer.addView(thumbImageView);
267
+
268
+ // Cache the thumbnail for zoom functionality
269
+ ImageView cachedView = new ImageView(context);
270
+ cachedView.setImageBitmap(bitmap);
271
+ cachedView.setScaleType(ImageView.ScaleType.CENTER_CROP);
272
+ cachedView.setLayoutParams(layoutParams);
273
+ cachedFullViewThumbnails.add(cachedView);
258
274
  });
259
275
  }
260
276
  });
@@ -327,12 +343,9 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
327
343
  }
328
344
 
329
345
  private void updateHandlePositions() {
330
- float startPercent = (float) startTime / mDuration;
331
- float endPercent = (float) endTime / mDuration;
332
-
333
- float containerWidth = trimmerContainerBg.getWidth();
334
- float leadingHandleX = startPercent * containerWidth;
335
- float trailingHandleX = endPercent * containerWidth;
346
+ // Use zoom-aware position calculation
347
+ float leadingHandleX = positionForTime(startTime);
348
+ float trailingHandleX = positionForTime(endTime);
336
349
 
337
350
  leadingHandle.setX(leadingHandleX);
338
351
  trailingHandle.setX(trailingHandleX + trailingHandle.getWidth());
@@ -429,12 +442,17 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
429
442
 
430
443
  @Override
431
444
  public void onDestroy() {
445
+ // Stop any ongoing operations
446
+ isGeneratingThumbnails = false;
432
447
  BackgroundExecutor.cancelAll("", true);
433
448
  UiThreadExecutor.cancelAll("");
434
449
  mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
435
450
  mTimingHandler.removeCallbacks(mTimingRunnable);
436
451
  zoomWaitTimer.removeCallbacks(zoomRunnable);
437
452
 
453
+ // Clear cached thumbnails to prevent memory leaks
454
+ cachedFullViewThumbnails.clear();
455
+
438
456
  try {
439
457
  if (mediaMetadataRetriever != null) {
440
458
  mediaMetadataRetriever.release();
@@ -590,15 +608,41 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
590
608
  endTimeText.setText(endTime);
591
609
 
592
610
  if (needUpdateProgress) {
593
- // Update progressIndicator position
594
- float indicatorPosition = (float) currentPosition / duration * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth();
611
+ // Update progressIndicator position using zoom-aware calculation
612
+ float indicatorPosition;
613
+
614
+ if (isZoomedIn) {
615
+ // Calculate position relative to zoomed range
616
+ long visibleRangeStart = getVisibleRangeStart();
617
+ long visibleRangeDuration = getVisibleRangeDuration();
618
+
619
+ // Ensure current position is within visible range for proper calculation
620
+ if (currentPosition < visibleRangeStart || currentPosition > visibleRangeStart + visibleRangeDuration) {
621
+ // If current position is outside visible range, clamp it
622
+ currentPosition = (int) Math.max(visibleRangeStart, Math.min(visibleRangeStart + visibleRangeDuration, currentPosition));
623
+ }
624
+
625
+ float ratio = visibleRangeDuration > 0 ? (float) (currentPosition - visibleRangeStart) / visibleRangeDuration : 0;
626
+ indicatorPosition = ratio * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth();
627
+ } else {
628
+ // Calculate position relative to full duration (original logic)
629
+ indicatorPosition = mDuration > 0 ? (float) currentPosition / mDuration * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth() : leadingHandle.getWidth();
630
+ }
631
+
632
+ // Ensure indicator stays within handle bounds using actual handle positions
633
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
634
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
635
+
636
+ // Apply bounds checking based on actual handle positions
637
+ indicatorPosition = Math.max(leftBoundary, Math.min(rightBoundary, indicatorPosition));
595
638
 
596
639
  if (currentSelectedhandle == leadingHandle) {
597
- float leftBoundary = trimmerContainer.getX();
598
640
  progressIndicator.setX(Math.max(leftBoundary, indicatorPosition));
599
- } else {
600
- float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
641
+ } else if (currentSelectedhandle == trailingHandle) {
601
642
  progressIndicator.setX(Math.min(rightBoundary, indicatorPosition));
643
+ } else {
644
+ // Normal playback - use calculated position with handle bounds
645
+ progressIndicator.setX(indicatorPosition);
602
646
  }
603
647
  }
604
648
  }
@@ -636,9 +680,11 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
636
680
  private void onTrimmerContainerPanned(MotionEvent event) {
637
681
  float newX = event.getRawX();
638
682
  boolean didClamp = false;
639
- // Ensure newX is within valid range
640
- float leftBoundary = trimmerContainer.getX();
641
- float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
683
+
684
+ // Use handle positions for boundaries instead of trimmer container
685
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
686
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
687
+
642
688
  newX = Math.max(leftBoundary, newX);
643
689
  newX = Math.min(rightBoundary, newX);
644
690
 
@@ -658,9 +704,21 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
658
704
 
659
705
  float indicatorPosition = newX - (trimmerContainerBg.getX());
660
706
 
661
- // TODO: check this
662
- float indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
663
- long newVideoPosition = (long) (indicatorPositionPercent * mDuration);
707
+ // Calculate video position based on zoom state
708
+ float indicatorPositionPercent;
709
+ long newVideoPosition;
710
+
711
+ if (isZoomedIn) {
712
+ // Calculate relative to zoomed range
713
+ indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
714
+ long visibleStart = getVisibleRangeStart();
715
+ long visibleDuration = getVisibleRangeDuration();
716
+ newVideoPosition = visibleStart + (long) (indicatorPositionPercent * visibleDuration);
717
+ } else {
718
+ // Calculate relative to full duration
719
+ indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
720
+ newVideoPosition = (long) (indicatorPositionPercent * mDuration);
721
+ }
664
722
 
665
723
  seekTo(newVideoPosition, false);
666
724
  }
@@ -676,6 +734,7 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
676
734
  fadeOutProgressIndicator();
677
735
  seekTo(isLeading ? startTime : endTime, true);
678
736
  playHapticFeedback(true);
737
+ isTrimmingLeading = isLeading;
679
738
  break;
680
739
  case MotionEvent.ACTION_MOVE:
681
740
  if (draggingDisabled) {
@@ -684,6 +743,8 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
684
743
 
685
744
  boolean didClamp = false;
686
745
  float newX = event.getRawX() - ((float) view.getWidth() / 2);
746
+
747
+ // Handle constraints need to consider zoom state
687
748
  if (isLeading) {
688
749
  newX = Math.max(0, Math.min(newX, trailingHandle.getX() - view.getWidth()));
689
750
  } else {
@@ -692,54 +753,114 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
692
753
 
693
754
  view.setX(newX);
694
755
 
695
- // Calculate new startTime or endTime
756
+ // Calculate new startTime or endTime based on zoom state
696
757
  if (isLeading) {
697
758
  // Calculate the new startTime based on the handle's new position
698
- long newStartTime = (long) ((newX / trimmerContainerBg.getWidth()) * mDuration);
759
+ long newStartTime = timeForPosition(newX);
699
760
  // Calculate the duration between the new startTime and the current endTime
700
761
  long duration = endTime - newStartTime;
701
762
  if (duration >= mMinDuration && duration <= mMaxDuration) {
702
763
  // If the duration is within the allowed range, update startTime and move the progress indicator
703
764
  startTime = newStartTime;
704
- progressIndicator.setX(newX + view.getWidth());
765
+ float indicatorX = newX + view.getWidth();
766
+ // Ensure progress indicator stays within handle bounds
767
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
768
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
769
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
705
770
  } else if (duration < mMinDuration) {
706
771
  didClamp = true;
707
- // If the duration is less than the minimum, set startTime to the maximum possible to maintain the minimum duration
772
+ // If the duration is less than the minimum, calculate maximum startTime to maintain minimum duration
708
773
  startTime = endTime - mMinDuration;
709
- // Adjust the handle position accordingly
710
- view.setX((float) startTime / mDuration * trimmerContainerBg.getWidth());
711
- progressIndicator.setX(view.getX() + view.getWidth());
774
+
775
+ // In zoom mode, don't recalculate position - keep handle where it is but update times
776
+ if (isZoomedIn) {
777
+ // Keep handle at current position but clamp times
778
+ float indicatorX = newX + view.getWidth();
779
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
780
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
781
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
782
+ } else {
783
+ // Normal mode - adjust handle position
784
+ view.setX(positionForTime(startTime));
785
+ float indicatorX = view.getX() + view.getWidth();
786
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
787
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
788
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
789
+ }
712
790
  } else {
713
791
  didClamp = true;
714
- // If the duration is greater than the maximum, set startTime to the minimum possible to maintain the maximum duration
792
+ // If the duration is greater than the maximum, calculate minimum startTime to maintain maximum duration
715
793
  startTime = endTime - mMaxDuration;
716
- // Adjust the handle position accordingly
717
- view.setX((float) startTime / mDuration * trimmerContainerBg.getWidth());
718
- progressIndicator.setX(view.getX() + view.getWidth());
794
+
795
+ // In zoom mode, don't recalculate position - keep handle where it is but update times
796
+ if (isZoomedIn) {
797
+ // Keep handle at current position but clamp times
798
+ float indicatorX = newX + view.getWidth();
799
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
800
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
801
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
802
+ } else {
803
+ // Normal mode - adjust handle position
804
+ view.setX(positionForTime(startTime));
805
+ float indicatorX = view.getX() + view.getWidth();
806
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
807
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
808
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
809
+ }
719
810
  }
720
811
  } else {
721
812
  // Calculate the new endTime based on the handle's new position
722
- long newEndTime = (long) (((newX - view.getWidth()) / trimmerContainerBg.getWidth()) * mDuration);
813
+ long newEndTime = timeForPosition(newX - view.getWidth());
723
814
  // Calculate the duration between the new endTime and the current startTime
724
815
  long duration = newEndTime - startTime;
725
816
  if (duration >= mMinDuration && duration <= mMaxDuration) {
726
817
  // If the duration is within the allowed range, update endTime and move the progress indicator
727
818
  endTime = newEndTime;
728
- progressIndicator.setX(newX - progressIndicator.getWidth());
819
+ float indicatorX = newX - progressIndicator.getWidth();
820
+ // Ensure progress indicator stays within handle bounds
821
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
822
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
823
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
729
824
  } else if (duration < mMinDuration) {
730
825
  didClamp = true;
731
- // If the duration is less than the minimum, set endTime to the minimum possible to maintain the minimum duration
826
+ // If the duration is less than the minimum, calculate minimum endTime to maintain minimum duration
732
827
  endTime = startTime + mMinDuration;
733
- // Adjust the handle position accordingly
734
- view.setX((float) endTime / mDuration * trimmerContainerBg.getWidth() + view.getWidth());
735
- progressIndicator.setX(view.getX() - progressIndicator.getWidth());
828
+
829
+ // In zoom mode, don't recalculate position - keep handle where it is but update times
830
+ if (isZoomedIn) {
831
+ // Keep handle at current position but clamp times
832
+ float indicatorX = newX - progressIndicator.getWidth();
833
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
834
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
835
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
836
+ } else {
837
+ // Normal mode - adjust handle position
838
+ view.setX(positionForTime(endTime) + view.getWidth());
839
+ float indicatorX = view.getX() - progressIndicator.getWidth();
840
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
841
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
842
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
843
+ }
736
844
  } else {
737
845
  didClamp = true;
738
- // If the duration is greater than the maximum, set endTime to the maximum possible to maintain the maximum duration
846
+ // If the duration is greater than the maximum, calculate maximum endTime to maintain maximum duration
739
847
  endTime = startTime + mMaxDuration;
740
- // Adjust the handle position accordingly
741
- view.setX((float) endTime / mDuration * trimmerContainerBg.getWidth() + view.getWidth());
742
- progressIndicator.setX(view.getX() - progressIndicator.getWidth());
848
+
849
+ // In zoom mode, don't recalculate position - keep handle where it is but update times
850
+ if (isZoomedIn) {
851
+ // Keep handle at current position but clamp times
852
+ float indicatorX = newX - progressIndicator.getWidth();
853
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
854
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
855
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
856
+ } else {
857
+ // Normal mode - adjust handle position
858
+ view.setX(positionForTime(endTime) + view.getWidth());
859
+ float indicatorX = view.getX() - progressIndicator.getWidth();
860
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
861
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
862
+ progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
863
+ }
743
864
  }
744
865
  }
745
866
 
@@ -751,11 +872,11 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
751
872
  updateTrimmerContainerWidth();
752
873
  seekTo(isLeading ? startTime : endTime, false);
753
874
 
754
- // TODO: create zoom feature like iOS
755
- // startZoomWaitTimer();
875
+ // Start zoom wait timer when dragging handles
876
+ startZoomWaitTimer();
756
877
  break;
757
878
  case MotionEvent.ACTION_UP:
758
- // stopZoomIfNeeded();
879
+ stopZoomIfNeeded();
759
880
  fadeInProgressIndicator();
760
881
  view.performClick();
761
882
  break;
@@ -805,7 +926,6 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
805
926
  }
806
927
 
807
928
  zoomRunnable = () -> {
808
- Log.i("tag", "A Kiss after 500ms");
809
929
  stopZoomWaitTimer();
810
930
  zoomIfNeeded();
811
931
  };
@@ -814,12 +934,32 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
814
934
  }
815
935
 
816
936
  private void stopZoomWaitTimer() {
817
- zoomWaitTimer.removeCallbacks(zoomRunnable);
937
+ if (zoomRunnable != null) {
938
+ zoomWaitTimer.removeCallbacks(zoomRunnable);
939
+ }
818
940
  }
819
941
 
820
942
  private void stopZoomIfNeeded() {
821
943
  stopZoomWaitTimer();
822
- isZoomedIn = false;
944
+ if (isZoomedIn) {
945
+ // Stop any ongoing thumbnail generation immediately
946
+ isGeneratingThumbnails = false;
947
+
948
+ // Cancel any ongoing background tasks for thumbnail generation
949
+ BackgroundExecutor.cancelAll("progressive_thumbs", true);
950
+
951
+ isZoomedIn = false;
952
+
953
+ // Immediately restore cached thumbnails without waiting for animation
954
+ restoreCachedThumbnails();
955
+
956
+ // Then apply smooth transition animation
957
+ animateZoomTransition(() -> {
958
+ updateHandlePositions();
959
+ // Force update progress indicator position after exiting zoom
960
+ updateCurrentTime(true);
961
+ });
962
+ }
823
963
  }
824
964
 
825
965
  private void zoomIfNeeded() {
@@ -827,9 +967,200 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
827
967
  return;
828
968
  }
829
969
 
830
- startShootVideoThumbs(mContext, 10, 5000, 10000);
970
+ // Store current handle positions to maintain visual continuity
971
+ float currentLeadingX = leadingHandle.getX();
972
+ float currentTrailingX = trailingHandle.getX();
973
+
974
+ // Calculate zoom range similar to iOS implementation
975
+ long newDuration = mDuration > 4000 ? 2000 : Math.max(1000, mDuration / 2); // At least 1 second, max 2 seconds or half duration
976
+
977
+ // Ensure zoom duration doesn't exceed video duration
978
+ newDuration = Math.min(newDuration, mDuration);
979
+
980
+ long rangeStart;
981
+ if (isTrimmingLeading) {
982
+ // Zoom around the start time, but ensure we don't go before video start
983
+ rangeStart = Math.max(0, startTime - (newDuration / 2));
984
+ // If range would extend past video end, adjust start
985
+ if (rangeStart + newDuration > mDuration) {
986
+ rangeStart = Math.max(0, mDuration - newDuration);
987
+ }
988
+ } else {
989
+ // Zoom around the end time
990
+ rangeStart = Math.max(0, endTime - (newDuration / 2));
991
+ // If range would extend past video end, adjust start
992
+ if (rangeStart + newDuration > mDuration) {
993
+ rangeStart = Math.max(0, mDuration - newDuration);
994
+ }
995
+ }
996
+
997
+ // Final bounds check
998
+ zoomedInRangeStart = Math.max(0, rangeStart);
999
+ zoomedInRangeDuration = Math.min(newDuration, mDuration - zoomedInRangeStart);
831
1000
 
832
1001
  isZoomedIn = true;
1002
+
1003
+ // Start progressive thumbnail generation immediately
1004
+ startProgressiveThumbnailGeneration();
1005
+
1006
+ // Update handle positions immediately without delay
1007
+ updateHandlePositionsForZoom(currentLeadingX, currentTrailingX);
1008
+
1009
+ // Provide haptic feedback
1010
+ playHapticFeedback(true);
1011
+ }
1012
+
1013
+ private void updateHandlePositionsForZoom(float previousLeadingX, float previousTrailingX) {
1014
+ // During zoom, we want to keep handles at their current visual positions
1015
+ // Don't recalculate based on zoom range - this causes jumping
1016
+
1017
+ Log.d(TAG, "Maintaining handle positions during zoom - Leading: " + previousLeadingX + ", Trailing: " + previousTrailingX);
1018
+
1019
+ // Keep handles exactly where they were visually
1020
+ leadingHandle.setX(previousLeadingX);
1021
+ trailingHandle.setX(previousTrailingX);
1022
+
1023
+ // Don't update times here - let the individual handle drag logic handle that
1024
+ // This prevents unwanted changes to startTime/endTime during zoom transition
1025
+
1026
+ // Update trimmer container width based on current handle positions
1027
+ updateTrimmerContainerWidth();
1028
+
1029
+ // Ensure progress indicator is positioned correctly within the handle bounds
1030
+ float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
1031
+ float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
1032
+ float currentX = progressIndicator.getX();
1033
+
1034
+ // If progress indicator is out of bounds, position it properly
1035
+ if (currentX < leftBoundary || currentX > rightBoundary) {
1036
+ // Position it based on the current media position
1037
+ updateCurrentTime(true);
1038
+ } else {
1039
+ updateCurrentTime(false);
1040
+ }
1041
+
1042
+ trimmerContainerWrapper.setVisibility(View.VISIBLE);
1043
+ if (trimmerContainerWrapper.getAlpha() == 0f) {
1044
+ trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start();
1045
+ }
1046
+ }
1047
+
1048
+ private void startProgressiveThumbnailGeneration() {
1049
+ if (isGeneratingThumbnails || mediaMetadataRetriever == null) {
1050
+ return;
1051
+ }
1052
+
1053
+ isGeneratingThumbnails = true;
1054
+
1055
+ // Immediately create placeholder thumbnails with subtle animation
1056
+ UiThreadExecutor.runTask("", () -> {
1057
+ mThumbnailContainer.removeAllViews();
1058
+
1059
+ // Calculate proper number of thumbnails based on container width
1060
+ final int thumbnailWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
1061
+ final int numberOfThumbnails = Math.max(8, mThumbnailContainer.getWidth() / thumbnailWidth);
1062
+
1063
+ // Create placeholder thumbnails first
1064
+ for (int i = 0; i < numberOfThumbnails; i++) {
1065
+ ImageView placeholder = new ImageView(getContext());
1066
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(thumbnailWidth, LinearLayout.LayoutParams.MATCH_PARENT);
1067
+ placeholder.setLayoutParams(layoutParams);
1068
+ placeholder.setBackgroundColor(Color.parseColor("#F0F0F0")); // Light gray placeholder
1069
+ placeholder.setAlpha(0.2f);
1070
+ mThumbnailContainer.addView(placeholder);
1071
+ }
1072
+ }, 0);
1073
+
1074
+ // Start background thumbnail generation
1075
+ BackgroundExecutor.execute(new BackgroundExecutor.Task("progressive_thumbs", 0L, "") {
1076
+ @Override
1077
+ public void execute() {
1078
+ try {
1079
+ final int thumbnailWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
1080
+ final int numberOfThumbnails = Math.max(8, mThumbnailContainer.getWidth() / thumbnailWidth);
1081
+ final long visibleDuration = isZoomedIn ? zoomedInRangeDuration : mDuration;
1082
+ final long visibleStart = isZoomedIn ? zoomedInRangeStart : 0;
1083
+ final long interval = visibleDuration > 0 ? visibleDuration / numberOfThumbnails : 0;
1084
+
1085
+ // Generate thumbnails progressively
1086
+ for (int i = 0; i < numberOfThumbnails && isGeneratingThumbnails && isZoomedIn; i++) {
1087
+ // Check if we should continue generating
1088
+ if (!isGeneratingThumbnails || !isZoomedIn) {
1089
+ Log.d(TAG, "Thumbnail generation cancelled at index " + i);
1090
+ return;
1091
+ }
1092
+
1093
+ final int index = i;
1094
+ final long timeUs = (visibleStart + (i * interval)) * 1000; // Convert to microseconds
1095
+ final long clampedTimeUs = Math.max(0, Math.min(timeUs, mDuration * 1000L));
1096
+
1097
+ try {
1098
+ Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
1099
+ if (bitmap != null && isGeneratingThumbnails && isZoomedIn) {
1100
+ // Update UI immediately for each thumbnail
1101
+ UiThreadExecutor.runTask("", () -> {
1102
+ // Double-check zoom state before updating UI
1103
+ if (isZoomedIn && index < mThumbnailContainer.getChildCount()) {
1104
+ ImageView thumbnailView = (ImageView) mThumbnailContainer.getChildAt(index);
1105
+ if (thumbnailView != null) {
1106
+ thumbnailView.setImageBitmap(bitmap);
1107
+ thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
1108
+ thumbnailView.setBackground(null); // Remove placeholder background
1109
+
1110
+ // Smooth fade-in animation for each thumbnail
1111
+ thumbnailView.animate()
1112
+ .alpha(1.0f)
1113
+ .setDuration(150)
1114
+ .setStartDelay(index * 50L) // Stagger animations
1115
+ .start();
1116
+ }
1117
+ }
1118
+ }, 0);
1119
+
1120
+ // Small delay between generations to avoid blocking
1121
+ Thread.sleep(10);
1122
+ }
1123
+ } catch (Exception e) {
1124
+ Log.w(TAG, "Error generating progressive thumbnail at " + clampedTimeUs, e);
1125
+ }
1126
+ }
1127
+
1128
+ // Only finalize if we're still in zoom mode
1129
+ if (isGeneratingThumbnails && isZoomedIn) {
1130
+ isGeneratingThumbnails = false;
1131
+ } else {
1132
+ isGeneratingThumbnails = false;
1133
+ }
1134
+
1135
+ } catch (Exception e) {
1136
+ Log.e(TAG, "Error in progressive thumbnail generation", e);
1137
+ isGeneratingThumbnails = false;
1138
+ }
1139
+ }
1140
+ });
1141
+ }
1142
+
1143
+ private void animateZoomTransition(Runnable onComplete) {
1144
+ // Only animate if we're still transitioning
1145
+ if (mThumbnailContainer != null) {
1146
+ mThumbnailContainer.animate()
1147
+ .alpha(0.7f)
1148
+ .setDuration(200) // Shorter duration for better responsiveness
1149
+ .withEndAction(() -> {
1150
+ if (onComplete != null) {
1151
+ onComplete.run();
1152
+ }
1153
+ if (mThumbnailContainer != null) {
1154
+ mThumbnailContainer.animate()
1155
+ .alpha(1.0f)
1156
+ .setDuration(200)
1157
+ .start();
1158
+ }
1159
+ })
1160
+ .start();
1161
+ } else if (onComplete != null) {
1162
+ onComplete.run();
1163
+ }
833
1164
  }
834
1165
 
835
1166
  private void ignoreSystemGestureForView(View v) {
@@ -850,4 +1181,54 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
850
1181
  );
851
1182
  }
852
1183
  }
1184
+
1185
+ // Helper methods for position/time conversion considering zoom state
1186
+ private long timeForPosition(float position) {
1187
+ if (trimmerContainerBg.getWidth() <= 0) return 0;
1188
+
1189
+ if (isZoomedIn) {
1190
+ // Convert position to time within zoomed range
1191
+ float ratio = position / trimmerContainerBg.getWidth();
1192
+ return zoomedInRangeStart + (long) (ratio * zoomedInRangeDuration);
1193
+ } else {
1194
+ // Convert position to time within full duration
1195
+ return (long) ((position / trimmerContainerBg.getWidth()) * mDuration);
1196
+ }
1197
+ }
1198
+
1199
+ private float positionForTime(long time) {
1200
+ if (isZoomedIn) {
1201
+ // Convert time to position within zoomed range
1202
+ if (zoomedInRangeDuration <= 0) return 0;
1203
+ float ratio = (float) (time - zoomedInRangeStart) / zoomedInRangeDuration;
1204
+ return Math.max(0, Math.min(trimmerContainerBg.getWidth(), ratio * trimmerContainerBg.getWidth()));
1205
+ } else {
1206
+ // Convert time to position within full duration
1207
+ if (mDuration <= 0) return 0;
1208
+ return Math.max(0, Math.min(trimmerContainerBg.getWidth(), ((float) time / mDuration) * trimmerContainerBg.getWidth()));
1209
+ }
1210
+ }
1211
+
1212
+ private long getVisibleRangeStart() {
1213
+ return isZoomedIn ? zoomedInRangeStart : 0;
1214
+ }
1215
+
1216
+ private long getVisibleRangeDuration() {
1217
+ return isZoomedIn ? zoomedInRangeDuration : mDuration;
1218
+ }
1219
+
1220
+ private void restoreCachedThumbnails() {
1221
+ // Clear current thumbnails
1222
+ mThumbnailContainer.removeAllViews();
1223
+
1224
+ // Restore cached thumbnails efficiently
1225
+ for (ImageView cachedThumbnail : cachedFullViewThumbnails) {
1226
+ // Create a new ImageView with the same bitmap to avoid view reuse issues
1227
+ ImageView restoredView = new ImageView(getContext());
1228
+ restoredView.setImageBitmap(((android.graphics.drawable.BitmapDrawable) cachedThumbnail.getDrawable()).getBitmap());
1229
+ restoredView.setScaleType(ImageView.ScaleType.CENTER_CROP);
1230
+ restoredView.setLayoutParams(cachedThumbnail.getLayoutParams());
1231
+ mThumbnailContainer.addView(restoredView);
1232
+ }
1233
+ }
853
1234
  }
@@ -811,7 +811,7 @@ extension VideoTrim {
811
811
 
812
812
  // Old Arch
813
813
  @objc(closeEditor)
814
- func closeEditor() -> Void {
814
+ func _closeEditor() -> Void {
815
815
  closeEditor()
816
816
  }
817
817
  }
@@ -465,8 +465,14 @@ class VideoTrimmerViewController: UIViewController {
465
465
  }
466
466
 
467
467
  public func configure(config: NSDictionary) {
468
- maximumDuration = config["maxDuration"] as? Int ?? 0
469
- minimumDuration = config["minDuration"] as? Int ?? 0
468
+ if let maxDuration = config["maxDuration"] as? Int, maxDuration > 0 {
469
+ maximumDuration = maxDuration
470
+ }
471
+
472
+ if let minDuration = config["minDuration"] as? Int, minDuration > 0 {
473
+ minimumDuration = minDuration
474
+ }
475
+
470
476
  cancelButtonText = config["cancelButtonText"] as? String ?? "Cancel"
471
477
  saveButtonText = config["saveButtonText"] as? String ?? "Save"
472
478
  jumpToPositionOnLoad = config["jumpToPositionOnLoad"] as? Double ?? 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-video-trim",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "description": "Video trimmer for your React Native app",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",