From bb543c3217a5e3f4dccf39e4ff8208e4ab7ac437 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sat, 29 Oct 2022 16:38:36 +0200 Subject: [PATCH] Use plugin from jitpack! --- app/build.gradle | 4 +- .../app/postCreation/PostCreationActivity.kt | 1 - .../app/postCreation/PostCreationViewModel.kt | 7 + .../java/org/pixeldroid/app/utils/Utils.kt | 8 - app/src/main/res/values/strings.xml | 3 + gradle/verification-metadata.xml | 26 + mediaEditor/.gitignore | 1 - mediaEditor/build.gradle | 62 -- mediaEditor/proguard-rules.pro | 21 - .../media_editor/ExampleInstrumentedTest.kt | 24 - mediaEditor/src/main/AndroidManifest.xml | 3 - .../photoEdit/EditImageFragment.kt | 88 --- .../photoEdit/FilterListFragment.kt | 97 --- .../photoEdit/PhotoEditActivity.kt | 462 ------------- .../photoEdit/ThumbnailAdapter.kt | 55 -- .../media_editor/photoEdit/Utils.kt | 95 --- .../photoEdit/VideoEditActivity.kt | 647 ------------------ .../photoEdit/cropper/CropImageView.kt | 105 --- .../photoEdit/cropper/CropOverlayView.kt | 490 ------------- .../photoEdit/cropper/CropWindowHandler.kt | 269 -------- .../cropper/CropWindowMoveHandler.kt | 405 ----------- .../src/main/res/drawable/check_circle_24.xml | 12 - .../src/main/res/drawable/double_circle.xml | 11 - .../main/res/drawable/ic_crop_black_24dp.xml | 9 - .../src/main/res/drawable/ic_save_24dp.xml | 5 - .../src/main/res/drawable/restore_24dp.xml | 9 - .../src/main/res/drawable/selector_mute.xml | 9 - mediaEditor/src/main/res/drawable/speed.xml | 5 - .../src/main/res/drawable/thumb_left.xml | 25 - .../src/main/res/drawable/thumb_right.xml | 29 - .../src/main/res/drawable/video_stable.xml | 5 - .../src/main/res/drawable/volume_off.xml | 5 - .../src/main/res/drawable/volume_up.xml | 5 - .../main/res/layout/activity_photo_edit.xml | 89 --- .../main/res/layout/activity_video_edit.xml | 251 ------- .../main/res/layout/crop_image_activity.xml | 6 - .../src/main/res/layout/crop_image_view.xml | 31 - .../main/res/layout/fragment_edit_image.xml | 79 --- .../main/res/layout/fragment_filter_list.xml | 17 - .../main/res/layout/thumbnail_list_item.xml | 26 - mediaEditor/src/main/res/menu/edit_menu.xml | 20 - .../src/main/res/values-night/themes.xml | 16 - mediaEditor/src/main/res/values/colors.xml | 10 - mediaEditor/src/main/res/values/public.xml | 4 - mediaEditor/src/main/res/values/strings.xml | 36 - mediaEditor/src/main/res/values/themes.xml | 16 - .../media_editor/ExampleUnitTest.kt | 17 - settings.gradle | 3 +- 48 files changed, 38 insertions(+), 3585 deletions(-) delete mode 100644 mediaEditor/.gitignore delete mode 100644 mediaEditor/build.gradle delete mode 100644 mediaEditor/proguard-rules.pro delete mode 100644 mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt delete mode 100644 mediaEditor/src/main/AndroidManifest.xml delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt delete mode 100644 mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt delete mode 100644 mediaEditor/src/main/res/drawable/check_circle_24.xml delete mode 100644 mediaEditor/src/main/res/drawable/double_circle.xml delete mode 100644 mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml delete mode 100644 mediaEditor/src/main/res/drawable/ic_save_24dp.xml delete mode 100644 mediaEditor/src/main/res/drawable/restore_24dp.xml delete mode 100644 mediaEditor/src/main/res/drawable/selector_mute.xml delete mode 100644 mediaEditor/src/main/res/drawable/speed.xml delete mode 100644 mediaEditor/src/main/res/drawable/thumb_left.xml delete mode 100644 mediaEditor/src/main/res/drawable/thumb_right.xml delete mode 100644 mediaEditor/src/main/res/drawable/video_stable.xml delete mode 100644 mediaEditor/src/main/res/drawable/volume_off.xml delete mode 100644 mediaEditor/src/main/res/drawable/volume_up.xml delete mode 100644 mediaEditor/src/main/res/layout/activity_photo_edit.xml delete mode 100644 mediaEditor/src/main/res/layout/activity_video_edit.xml delete mode 100644 mediaEditor/src/main/res/layout/crop_image_activity.xml delete mode 100644 mediaEditor/src/main/res/layout/crop_image_view.xml delete mode 100644 mediaEditor/src/main/res/layout/fragment_edit_image.xml delete mode 100644 mediaEditor/src/main/res/layout/fragment_filter_list.xml delete mode 100644 mediaEditor/src/main/res/layout/thumbnail_list_item.xml delete mode 100644 mediaEditor/src/main/res/menu/edit_menu.xml delete mode 100644 mediaEditor/src/main/res/values-night/themes.xml delete mode 100644 mediaEditor/src/main/res/values/colors.xml delete mode 100644 mediaEditor/src/main/res/values/public.xml delete mode 100644 mediaEditor/src/main/res/values/strings.xml delete mode 100644 mediaEditor/src/main/res/values/themes.xml delete mode 100644 mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt diff --git a/app/build.gradle b/app/build.gradle index 9e5d3e18..a5116986 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -196,10 +196,8 @@ dependencies { implementation 'com.github.connyduck:sparkbutton:4.1.0' - implementation 'info.androidhive:imagefilters:1.0.7' - implementation 'com.github.yalantis:ucrop:2.2.8-native' + implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.0' implementation project(path: ':scrambler') - implementation project(path: ':mediaEditor') implementation('com.github.bumptech.glide:glide:4.14.2') { exclude group: "com.android.support" diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index e6263268..7386599f 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -21,7 +21,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.net.toFile import androidx.core.net.toUri -import androidx.core.os.HandlerCompat import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index b78f9a9c..710ac45a 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -450,6 +450,13 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null _uiState.update { it.copy(newPostDescriptionText = text.toString()) } } + /** + * @param originalUri the Uri of the file you sent to be edited + * @param progress percentage of (this pass of) encoding that is done + * @param firstPass Whether this is the first pass (currently for analysis of video stabilization) or the second (and last) pass. + * @param outputVideoPath when not null, it means the encoding is done and the result is saved in this file + * @param error is true when there has been an error during encoding. + */ private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){ photoData.value?.indexOfFirst { it.imageUri == originalUri }?.let { position -> diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index 621ae44e..2d646bcc 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.arthenica.ffmpegkit.FFmpegKitConfig import com.google.android.material.color.MaterialColors import com.google.gson.JsonDeserializer import com.google.gson.JsonElement @@ -96,13 +95,6 @@ fun normalizeDomain(domain: String): String { .trim(Char::isWhitespace) } -fun Context.ffmpegCompliantUri(inputUri: Uri?): String = - if (inputUri?.scheme == "content") - FFmpegKitConfig.getSafParameterForRead(this, inputUri) - else inputUri.toString() - - - fun BaseActivity.openUrl(url: String): Boolean { val intent = CustomTabsIntent.Builder().build() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e241f04..016627ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -168,6 +168,9 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" "%d\nFollowing" "%d\nFollowing" + Edit + Unable to save image + Image successfully saved Could not get follow status Failed to open edit page Nothing to see here :( diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 30a561c5..854c6060 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -9555,6 +9555,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mediaEditor/.gitignore b/mediaEditor/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/mediaEditor/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/mediaEditor/build.gradle b/mediaEditor/build.gradle deleted file mode 100644 index e415469e..00000000 --- a/mediaEditor/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'org.pixeldroid.media_editor' - compileSdk 33 - - defaultConfig { - minSdk 23 - targetSdk 33 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - viewBinding = true - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.7.0' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - - implementation 'info.androidhive:imagefilters:1.0.7' - implementation 'com.github.yalantis:ucrop:2.2.8-native' - - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1' - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" - implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1" - implementation 'androidx.media2:media2-widget:1.2.1' - implementation 'androidx.media2:media2-player:1.2.1' - implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation 'com.arthenica:ffmpeg-kit-min-gpl:5.1.LTS' - implementation('com.github.bumptech.glide:glide:4.14.2') { - exclude group: "com.android.support" - } - -} \ No newline at end of file diff --git a/mediaEditor/proguard-rules.pro b/mediaEditor/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/mediaEditor/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt b/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt deleted file mode 100644 index 89737fff..00000000 --- a/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.pixeldroid.media_editor - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("org.pixeldroid.media_editor", appContext.packageName) - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/AndroidManifest.xml b/mediaEditor/src/main/AndroidManifest.xml deleted file mode 100644 index 69fc4129..00000000 --- a/mediaEditor/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt deleted file mode 100644 index 7777c928..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.SeekBar -import org.pixeldroid.media_editor.databinding.FragmentEditImageBinding - -class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener { - - private var listener: PhotoEditActivity? = null - private lateinit var binding: FragmentEditImageBinding - - private var BRIGHTNESS_MAX = 200 - private var SATURATION_MAX = 20 - private var CONTRAST_MAX= 30 - private var BRIGHTNESS_START = BRIGHTNESS_MAX/2 - private var SATURATION_START = SATURATION_MAX/2 - private var CONTRAST_START = CONTRAST_MAX/2 - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // Inflate the layout for this fragment - binding = FragmentEditImageBinding.inflate(inflater, container, false) - - binding.seekbarBrightness.max = BRIGHTNESS_MAX - binding.seekbarBrightness.progress = BRIGHTNESS_START - - binding.seekbarContrast.max = CONTRAST_MAX - binding.seekbarContrast.progress = CONTRAST_START - - binding.seekbarSaturation.max = SATURATION_MAX - binding.seekbarSaturation.progress = SATURATION_START - - setOnSeekBarChangeListeners(this) - - return binding.root - } - - private fun setOnSeekBarChangeListeners(listener: EditImageFragment?){ - binding.seekbarBrightness.setOnSeekBarChangeListener(listener) - binding.seekbarContrast.setOnSeekBarChangeListener(listener) - binding.seekbarSaturation.setOnSeekBarChangeListener(listener) - } - - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - var prog = progress - - listener?.let { - when(seekBar) { - binding.seekbarBrightness -> it.onBrightnessChange(progress - 100) - binding.seekbarSaturation -> { - prog += 10 - it.onSaturationChange(.10f * prog) - } - binding.seekbarContrast -> { - it.onContrastChange(.10f * prog) - } - } - } - } - - fun resetControl() { - // Make sure to ignore seekbar change events, since we don't want to have the reset cause - // filter applications due to the onProgressChanged calls - setOnSeekBarChangeListeners(null) - binding.seekbarBrightness.progress = BRIGHTNESS_START - binding.seekbarContrast.progress = CONTRAST_START - binding.seekbarSaturation.progress = SATURATION_START - setOnSeekBarChangeListeners(this) - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) { - listener?.onEditStarted() - } - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - listener?.onEditCompleted() - } - - fun setListener(listener: PhotoEditActivity) { - this.listener = listener - } -} diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt deleted file mode 100644 index dcb3a589..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.graphics.Bitmap -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import com.zomato.photofilters.FilterPack -import com.zomato.photofilters.imageprocessors.Filter -import com.zomato.photofilters.utils.ThumbnailItem -import com.zomato.photofilters.utils.ThumbnailsManager -import kotlinx.coroutines.launch -import org.pixeldroid.media_editor.R -import org.pixeldroid.media_editor.databinding.FragmentFilterListBinding - -class FilterListFragment : Fragment() { - - private lateinit var binding: FragmentFilterListBinding - - private var listener : ((Filter) -> Unit)? = null - internal lateinit var adapter: ThumbnailAdapter - private lateinit var tbItemList: MutableList - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // Inflate the layout for this fragment - binding = FragmentFilterListBinding.inflate(inflater, container, false) - - tbItemList = ArrayList() - - binding.recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false) - - adapter = ThumbnailAdapter(requireActivity(), tbItemList, this) - binding.recyclerView.adapter = adapter - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - displayImage() - } - - private fun displayImage() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { - val tbImage: Bitmap = bitmapFromUri(requireActivity().contentResolver, - PhotoEditActivity.imageUri - ) - setupFilter(tbImage) - - tbItemList.addAll(ThumbnailsManager.processThumbs(context)) - adapter.notifyDataSetChanged() - } - } - } - - private fun setupFilter(tbImage: Bitmap?) { - ThumbnailsManager.clearThumbs() - tbItemList.clear() - - val tbItem = ThumbnailItem() - tbItem.image = tbImage - tbItem.filter.name = getString(R.string.normal_filter) - tbItem.filterName = tbItem.filter.name - ThumbnailsManager.addThumb(tbItem) - - val filters = FilterPack.getFilterPack(context) - - for (filter in filters) { - val item = ThumbnailItem() - item.image = tbImage - item.filter = filter - item.filterName = filter.name - ThumbnailsManager.addThumb(item) - } - } - - fun resetSelectedFilter(){ - adapter.resetSelected() - } - - fun onFilterSelected(filter: Filter) { - listener?.invoke(filter) - } - - fun setListener(listFragmentListener: (filter: Filter) -> Unit) { - this.listener = listFragmentListener - } -} diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt deleted file mode 100644 index 37e30302..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt +++ /dev/null @@ -1,462 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.app.Activity -import android.app.AlertDialog -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.Point -import android.graphics.drawable.BitmapDrawable -import android.net.Uri -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View.GONE -import android.view.View.VISIBLE -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayoutMediator -import com.yalantis.ucrop.UCrop -import com.zomato.photofilters.imageprocessors.Filter -import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter -import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter -import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter -import org.pixeldroid.media_editor.databinding.ActivityPhotoEditBinding -import org.pixeldroid.media_editor.R -import java.io.File -import java.io.IOException -import java.io.OutputStream -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors.newSingleThreadExecutor -import java.util.concurrent.Future - -// This is an arbitrary number we are using to keep track of the permission -// request. Where an app has multiple context for requesting permission, -// this can help differentiate the different contexts. -private const val REQUEST_CODE_PERMISSIONS_SEND_PHOTO = 7 -private val REQUIRED_PERMISSIONS = arrayOf( - android.Manifest.permission.READ_EXTERNAL_STORAGE, - android.Manifest.permission.WRITE_EXTERNAL_STORAGE -) - -class PhotoEditActivity : AppCompatActivity() { - - var saving: Boolean = false - private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 - private val BRIGHTNESS_START = 0 - private val SATURATION_START = 1.0f - private val CONTRAST_START = 1.0f - - private var originalImage: Bitmap? = null - private var compressedImage: Bitmap? = null - private var compressedOriginalImage: Bitmap? = null - private lateinit var filteredImage: Bitmap - - private var actualFilter: Filter? = null - - private lateinit var filterListFragment: FilterListFragment - private lateinit var editImageFragment: EditImageFragment - - private var picturePosition: Int? = null - - private var brightnessFinal = BRIGHTNESS_START - private var saturationFinal = SATURATION_START - private var contrastFinal = CONTRAST_START - - init { - System.loadLibrary("NativeImageProcessor") - } - - companion object{ - const val PICTURE_URI = "picture_uri" - const val PICTURE_POSITION = "picture_position" - - private var executor: ExecutorService = newSingleThreadExecutor() - private var future: Future<*>? = null - - private var saveExecutor: ExecutorService = newSingleThreadExecutor() - private var saveFuture: Future<*>? = null - - private var initialUri: Uri? = null - internal var imageUri: Uri? = null - } - - private lateinit var binding: ActivityPhotoEditBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityPhotoEditBinding.inflate(layoutInflater) - - setContentView(binding.root) - - - supportActionBar?.setTitle(R.string.toolbar_title_edit) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setHomeButtonEnabled(true) - - initialUri = intent.getParcelableExtra(PICTURE_URI) - picturePosition = intent.getIntExtra(PICTURE_POSITION, 0) - imageUri = initialUri - - // Crop button on-click listener - binding.cropImageButton.setOnClickListener { - startCrop() - } - - loadImage() - - setupViewPager(binding.viewPager) - } - - private fun loadImage() { - originalImage = bitmapFromUri(contentResolver, imageUri) - - compressedImage = resizeImage(originalImage!!) - compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true) - filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true) - Glide.with(this).load(compressedImage).into(binding.imagePreview) - } - - private fun resizeImage(image: Bitmap): Bitmap { - val display = windowManager.defaultDisplay - val size = Point() - display.getSize(size) - - val newY = size.y * 0.7 - val scale = newY / image.height - return Bitmap.createScaledBitmap(image, (image.width * scale).toInt(), newY.toInt(), true) - } - - private fun setupViewPager(viewPager: ViewPager2) { - filterListFragment = FilterListFragment() - filterListFragment.setListener(::onFilterSelected) - - editImageFragment = EditImageFragment() - editImageFragment.setListener(this) - - val tabs: List<() -> Fragment> = listOf({ filterListFragment }, { editImageFragment }) - - // Keep both tabs loaded at all times because values are needed there - viewPager.offscreenPageLimit = 1 - - //Disable swiping in viewpager - viewPager.isUserInputEnabled = false - - viewPager.adapter = object : FragmentStateAdapter(this) { - override fun createFragment(position: Int): Fragment { - return tabs[position]() - } - - override fun getItemCount(): Int { - return tabs.size - } - } - TabLayoutMediator(binding.tabs, viewPager) { tab, position -> - tab.setText(when(position) { - 0 -> R.string.tab_filters - else -> R.string.edit - }) - }.attach() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.edit_menu, menu) - return true - } - - override fun onStop() { - super.onStop() - saving = false - } - - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (noEdits()) super.onBackPressed() - else { - val builder = AlertDialog.Builder(this) - builder.apply { - setMessage(R.string.save_before_returning) - setPositiveButton(android.R.string.ok) { _, _ -> - saveImageToGallery() - } - setNegativeButton(R.string.no_cancel_edit) { _, _ -> - super.onBackPressed() - } - } - // Create the AlertDialog - builder.show() - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - - when(item.itemId) { - android.R.id.home -> onBackPressed() - R.id.action_save -> { - saveImageToGallery() - } - R.id.action_reset -> { - resetControls() - actualFilter = null - imageUri = initialUri - loadImage() - filterListFragment.resetSelectedFilter() - } - } - - return super.onOptionsItemSelected(item) - } - - fun onFilterSelected(filter: Filter) { - filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true) - binding.imagePreview.setImageBitmap(filter.processFilter(filteredImage)) - compressedImage = filteredImage.copy(BITMAP_CONFIG, true) - actualFilter = filter - resetControls() - } - - private fun resetControls() { - brightnessFinal = BRIGHTNESS_START - saturationFinal = SATURATION_START - contrastFinal = CONTRAST_START - - editImageFragment.resetControl() - } - - - private fun applyFilterAndShowImage(filter: Filter, image: Bitmap?) { - future?.cancel(true) - future = executor.submit { - val bitmap = filter.processFilter(image!!.copy(BITMAP_CONFIG, true)) - binding.imagePreview.post { - binding.imagePreview.setImageBitmap(bitmap) - } - } - } - - fun onBrightnessChange(brightness: Int) { - brightnessFinal = brightness - val myFilter = Filter() - myFilter.addEditFilters(brightness, saturationFinal, contrastFinal) - applyFilterAndShowImage(myFilter, filteredImage) - } - - fun onSaturationChange(saturation: Float) { - saturationFinal = saturation - val myFilter = Filter() - myFilter.addEditFilters(brightnessFinal, saturation, contrastFinal) - applyFilterAndShowImage(myFilter, filteredImage) - } - - fun onContrastChange(contrast: Float) { - contrastFinal = contrast - val myFilter = Filter() - myFilter.addEditFilters(brightnessFinal, saturationFinal, contrast) - applyFilterAndShowImage(myFilter, filteredImage) - } - - private fun Filter.addEditFilters(br: Int, sa: Float, co: Float): Filter { - addSubFilter(BrightnessSubFilter(br)) - addSubFilter(ContrastSubFilter(co)) - addSubFilter(SaturationSubfilter(sa)) - return this - } - - fun onEditStarted() { - } - - fun onEditCompleted() { - val myFilter = Filter() - myFilter.addEditFilters(brightnessFinal, saturationFinal, contrastFinal) - val bitmap = filteredImage.copy(BITMAP_CONFIG, true) - - compressedImage = myFilter.processFilter(bitmap) - } - - - private fun startCrop() { - val file = File.createTempFile("temp_crop_img", ".png", cacheDir) - - val options: UCrop.Options = UCrop.Options().apply { - setStatusBarColor(this@PhotoEditActivity.getColorFromAttr(R.attr.colorPrimaryDark)) - setToolbarWidgetColor(this@PhotoEditActivity.getColorFromAttr(R.attr.colorOnSurface)) - setToolbarColor(this@PhotoEditActivity.getColorFromAttr(R.attr.colorSurface)) - setActiveControlsWidgetColor(this@PhotoEditActivity.getColorFromAttr(R.attr.colorPrimary)) - setFreeStyleCropEnabled(true) - } - val uCrop: UCrop = UCrop.of(initialUri!!, Uri.fromFile(file)).withOptions(options) - uCrop.start(this) - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if(resultCode == Activity.RESULT_OK) { - if (requestCode == UCrop.RESULT_ERROR) { - handleCropError(data) - } else { - handleCropResult(data) - } - } - } - - private fun resetFilteredImage(){ - val newBr = if(brightnessFinal != 0) BRIGHTNESS_START/brightnessFinal else 0 - val newSa = if(saturationFinal != 0.0f) SATURATION_START/saturationFinal else 0.0f - val newCo = if(contrastFinal != 0.0f) CONTRAST_START/contrastFinal else 0.0f - val myFilter = Filter().addEditFilters(newBr, newSa, newCo) - - filteredImage = myFilter.processFilter(filteredImage) - } - - private fun handleCropResult(data: Intent?) { - val resultCrop: Uri? = UCrop.getOutput(data!!) - if(resultCrop != null) { - imageUri = resultCrop - binding.imagePreview.setImageURI(resultCrop) - val bitmap = (binding.imagePreview.drawable as BitmapDrawable).bitmap - originalImage = bitmap.copy(Bitmap.Config.ARGB_8888, true) - compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true)) - compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true) - filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true) - resetFilteredImage() - } else { - Toast.makeText(this, R.string.crop_result_error, Toast.LENGTH_SHORT).show() - } - } - - private fun handleCropError(data: Intent?) { - val resultError = UCrop.getError(data!!) - if(resultError != null) { - Toast.makeText(this, "" + resultError, Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this, R.string.crop_result_error, Toast.LENGTH_SHORT).show() - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if(grantResults.size > 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED - && grantResults[1] == PackageManager.PERMISSION_GRANTED) { - // permission was granted - permissionsGrantedToSave() - } else { - Snackbar.make(binding.root, getString(R.string.permission_denied), - Snackbar.LENGTH_LONG).show() - } - } - - private fun applyFinalFilters(image: Bitmap?): Bitmap { - val editFilter = Filter().addEditFilters(brightnessFinal, saturationFinal, contrastFinal) - - var finalImage = editFilter.processFilter(image!!.copy(BITMAP_CONFIG, true)) - if (actualFilter!=null) finalImage = actualFilter!!.processFilter(finalImage) - return finalImage - } - - private fun sendBackImage(file: String) { - val intent = Intent() - .apply { - putExtra(PICTURE_URI, file) - putExtra(PICTURE_POSITION, picturePosition) - addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - } - - setResult(Activity.RESULT_OK, intent) - finish() - } - - private fun saveImageToGallery() { - // runtime permission and process - if (!allPermissionsGranted()) { - ActivityCompat.requestPermissions( - this, - REQUIRED_PERMISSIONS, - REQUEST_CODE_PERMISSIONS_SEND_PHOTO - ) - } else { - permissionsGrantedToSave() - } - } - - /** - * Check if all permission specified in the manifest have been granted - */ - private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { - ContextCompat.checkSelfPermission( - applicationContext, it) == PackageManager.PERMISSION_GRANTED - } - - - private fun OutputStream.writeBitmap(bitmap: Bitmap) { - use { out -> - //(quality is ignored for PNG) - bitmap.compress(Bitmap.CompressFormat.PNG, 85, out) - out.flush() - } - } - - private fun noEdits(): Boolean = - brightnessFinal == BRIGHTNESS_START - && contrastFinal == CONTRAST_START - && saturationFinal == SATURATION_START - && actualFilter?.let { it.name == getString(R.string.normal_filter)} ?: true - - private fun permissionsGrantedToSave() { - if (saving) { - val builder = AlertDialog.Builder(this) - builder.apply { - setMessage(R.string.busy_dialog_text) - setNegativeButton(R.string.busy_dialog_ok_button) { _, _ -> } - } - // Create the AlertDialog - builder.show() - return - } - saving = true - binding.progressBarSaveFile.visibility = VISIBLE - saveFuture = saveExecutor.submit { - try { - val path: String - if(!noEdits()) { - // Save modified image in cache - val tempFile = File.createTempFile("temp_edit_img", ".png", cacheDir) - path = Uri.fromFile(tempFile).toString() - tempFile.outputStream().writeBitmap(applyFinalFilters(originalImage)) - } - else { - path = imageUri.toString() - } - - if(saving) { - this.runOnUiThread { - sendBackImage(path) - binding.progressBarSaveFile.visibility = GONE - saving = false - } - } - } catch (e: IOException) { - this.runOnUiThread { - Snackbar.make( - binding.root, getString(R.string.save_image_failed), - Snackbar.LENGTH_LONG - ).show() - binding.progressBarSaveFile.visibility = GONE - saving = false - } - } - } - } -} diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt deleted file mode 100644 index 95a3e989..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.zomato.photofilters.utils.ThumbnailItem -import org.pixeldroid.media_editor.R -import org.pixeldroid.media_editor.databinding.ThumbnailListItemBinding - -class ThumbnailAdapter (private val context: Context, - private val tbItemList: List, - private val listener: FilterListFragment -): RecyclerView.Adapter() { - - private var selectedIndex = 0 - - fun resetSelected(){ - selectedIndex = 0 - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - val itemBinding = ThumbnailListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return MyViewHolder(itemBinding) - } - - override fun getItemCount(): Int { - return tbItemList.size - } - - override fun onBindViewHolder(holder: MyViewHolder, position: Int) { - val tbItem = tbItemList[position] - holder.thumbnail.setImageBitmap(tbItem.image) - holder.thumbnail.setOnClickListener { - listener.onFilterSelected(tbItem.filter) - selectedIndex = holder.bindingAdapterPosition - notifyDataSetChanged() - } - - holder.filterName.text = tbItem.filterName - - if(selectedIndex == position) - holder.filterName.setTextColor(context.getColorFromAttr(R.attr.colorPrimary)) - else - holder.filterName.setTextColor(context.getColorFromAttr(R.attr.colorOnBackground)) - } - - class MyViewHolder(itemBinding: ThumbnailListItemBinding): RecyclerView.ViewHolder(itemBinding.root) { - var thumbnail: ImageView = itemBinding.thumbnail - var filterName: TextView = itemBinding.filterName - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt deleted file mode 100644 index 151b5e33..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.content.ContentResolver -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.ImageDecoder -import android.graphics.Matrix -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import android.util.TypedValue -import android.webkit.MimeTypeMap -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.exifinterface.media.ExifInterface -import com.arthenica.ffmpegkit.FFmpegKitConfig -import com.google.android.material.color.MaterialColors - - -fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder - .decodeBitmap( - ImageDecoder.createSource(contentResolver, uri!!) - ) - { decoder, _, _ -> decoder.isMutableRequired = true } - } else { - @Suppress("DEPRECATION") - val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri) - modifyOrientation(bitmap!!, contentResolver, uri!!) - } - -fun modifyOrientation( - bitmap: Bitmap, - contentResolver: ContentResolver, - uri: Uri -): Bitmap { - val inputStream = contentResolver.openInputStream(uri)!! - val ei = ExifInterface(inputStream) - return when (ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) { - ExifInterface.ORIENTATION_ROTATE_90 -> bitmap.rotate(90f) - ExifInterface.ORIENTATION_ROTATE_180 -> bitmap.rotate(180f) - ExifInterface.ORIENTATION_ROTATE_270 -> bitmap.rotate(270f) - ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> bitmap.flip(horizontal = true, vertical = false) - ExifInterface.ORIENTATION_FLIP_VERTICAL -> bitmap.flip(horizontal = false, vertical = true) - else -> bitmap - } -} - -fun Bitmap.rotate(degrees: Float): Bitmap { - val matrix = Matrix() - matrix.postRotate(degrees) - return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) -} - -fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap { - val matrix = Matrix() - matrix.preScale(if (horizontal) -1f else 1f, if (vertical) -1f else 1f) - return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) -} - -@ColorInt -fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK) - -fun Context.ffmpegCompliantUri(inputUri: Uri?): String = - if (inputUri?.scheme == "content") - FFmpegKitConfig.getSafParameterForRead(this, inputUri) - else inputUri.toString() - -/** - * This method converts dp unit to equivalent pixels, depending on device density. - */ -fun Int.dpToPx(context: Context): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this.toFloat(), - context.resources.displayMetrics - ).toInt() -} - - -/** Maps a Float from this range to target range */ -fun ClosedRange.convert(number: Float, target: ClosedRange): Float { - val ratio = number / (endInclusive - start) - return (ratio * (target.endInclusive - target.start)) -} - -fun Uri.fileExtension(contentResolver: ContentResolver): String? { - return if (scheme == "content") { - contentResolver.getType(this)?.takeLastWhile { it != '/' } - } else { - MimeTypeMap.getFileExtensionFromUrl(toString()).ifEmpty { null } - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt deleted file mode 100644 index d93fe9de..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt +++ /dev/null @@ -1,647 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit - -import android.app.Activity -import android.app.AlertDialog -import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.graphics.Rect -import android.media.AudioManager -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.text.format.DateUtils -import android.util.Log -import android.util.TypedValue -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.appcompat.app.AppCompatActivity -import androidx.core.net.toUri -import androidx.core.os.HandlerCompat -import androidx.core.view.isVisible -import androidx.media.AudioAttributesCompat -import androidx.media2.common.MediaMetadata -import androidx.media2.common.UriMediaItem -import androidx.media2.player.MediaPlayer -import com.arthenica.ffmpegkit.FFmpegKit -import com.arthenica.ffmpegkit.FFmpegKitConfig -import com.arthenica.ffmpegkit.FFmpegSession -import com.arthenica.ffmpegkit.FFprobeKit -import com.arthenica.ffmpegkit.MediaInformation -import com.arthenica.ffmpegkit.ReturnCode -import com.arthenica.ffmpegkit.Statistics -import com.bumptech.glide.Glide -import com.google.android.material.slider.RangeSlider -import com.google.android.material.slider.Slider -import org.pixeldroid.media_editor.R -import org.pixeldroid.media_editor.databinding.ActivityVideoEditBinding -import java.io.File -import java.io.Serializable -import kotlin.math.absoluteValue -import kotlin.math.roundToInt - -const val TAG = "VideoEditActivity" - -class VideoEditActivity : AppCompatActivity() { - - data class RelativeCropPosition( - // Width of the selected part of the video, relative to the width of the video - val relativeWidth: Float = 1f, - // Height of the selected part of the video, relative to the height of the video - val relativeHeight: Float = 1f, - // Distance of left corner of selected part, relative to the width of the video - val relativeX: Float = 0f, - // Distance of top of selected part, relative to the height of the video - val relativeY: Float = 0f, - ): Serializable { - fun notCropped(): Boolean = - (relativeWidth - 1f).absoluteValue < 0.001f - && (relativeHeight - 1f).absoluteValue < 0.001f - && relativeX.absoluteValue < 0.001f - && relativeY.absoluteValue < 0.001f - - } - - data class VideoEditArguments( - val muted: Boolean, - val videoStart: Float?, - val videoEnd: Float? , - val speedIndex: Int, - val videoCrop: RelativeCropPosition, - val videoStabilize: Float - ): Serializable - - private lateinit var videoUri: Uri - private lateinit var mediaPlayer: MediaPlayer - private var videoPosition: Int = -1 - - private var cropRelativeDimensions: RelativeCropPosition = RelativeCropPosition() - - private var stabilization: Float = 0f - set(value){ - field = value - if(value > 0.01f && value <= 100f){ - // Stabilization requested, show UI - binding.stabilisationSaved.isVisible = true - val typedValue = TypedValue() - val color: Int = if (binding.stabilizer.context.theme - .resolveAttribute(R.attr.colorSecondary, typedValue, true) - ) typedValue.data else Color.TRANSPARENT - - binding.stabilizer.drawable.setTint(color) - } - else { - binding.stabilisationSaved.isVisible = false - binding.stabilizer.drawable.setTintList(null) - } - } - - private var speed: Int = 1 - set(value) { - field = value - - mediaPlayer.playbackSpeed = speedChoices[value].toFloat() - - if(speed != 1) binding.muter.callOnClick() - } - - private lateinit var binding: ActivityVideoEditBinding - // Map photoData indexes to FFmpeg Session IDs - private val sessionList: ArrayList = arrayListOf() - private val tempFiles: ArrayList = ArrayList() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityVideoEditBinding.inflate(layoutInflater) - setContentView(binding.root) - - supportActionBar?.setTitle(R.string.toolbar_title_edit) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setHomeButtonEnabled(true) - - - binding.videoRangeSeekBar.setCustomThumbDrawablesForValues(R.drawable.thumb_left,R.drawable.double_circle,R.drawable.thumb_right) - binding.videoRangeSeekBar.thumbRadius = 20.dpToPx(this) - - - val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper()) - - videoUri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI)!! - - videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1) - - val inputVideoPath = ffmpegCompliantUri(videoUri) - val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation - - //Duration in seconds, or null - val duration: Float? = mediaInformation?.duration?.toFloatOrNull() - - binding.videoRangeSeekBar.valueFrom = 0f - binding.videoRangeSeekBar.valueTo = duration ?: 100f - binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f) - - - val mediaItem: UriMediaItem = UriMediaItem.Builder(videoUri).build() - mediaItem.metadata = MediaMetadata.Builder() - .putString(MediaMetadata.METADATA_KEY_TITLE, "") - .build() - - mediaPlayer = MediaPlayer(this) - mediaPlayer.setMediaItem(mediaItem) - - //binding.videoView.mediaControlView?.setMediaController() - - // Configure audio - mediaPlayer.setAudioAttributes(AudioAttributesCompat.Builder() - .setLegacyStreamType(AudioManager.STREAM_MUSIC) - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE) - .build() - ) - - findViewById(R.id.progress_bar)?.visibility = View.GONE - - mediaPlayer.prepare() - - - binding.muter.setOnClickListener { - if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f - else { - mediaPlayer.playerVolume = 1f - speed = 1 - } - binding.muter.isSelected = !binding.muter.isSelected - } - - binding.cropper.setOnClickListener { - showCropInterface(show = true, uri = videoUri) - } - - binding.saveCropButton.setOnClickListener { - // This is the rectangle selected by the crop - val cropRect = binding.cropImageView.cropWindowRect - - // This is the rectangle of the whole image - val fullImageRect: Rect = binding.cropImageView.getInitialCropWindowRect() - - // x, y are coordinates of top left, in the ImageView - val x = cropRect.left - fullImageRect.left - val y = cropRect.top - fullImageRect.top - - // width and height selected by the crop - val width = cropRect.width() - val height = cropRect.height() - - // To avoid having to calculate the dimensions of the video here, we pass - // relative width, height and x, y back to be treated in FFmpeg - cropRelativeDimensions = RelativeCropPosition( - relativeWidth = width/fullImageRect.width(), - relativeHeight = height/fullImageRect.height(), - relativeX = x/fullImageRect.width(), - relativeY = y/fullImageRect.height() - ) - - // If a crop was saved, change the color of the crop button to give a visual indication - if(!cropRelativeDimensions.notCropped()){ - val typedValue = TypedValue() - val color: Int = if (binding.checkMarkCropped.context.theme - .resolveAttribute(R.attr.colorSecondary, typedValue, true) - ) typedValue.data else Color.TRANSPARENT - - binding.cropper.drawable.setTint(color) - } else { - // Else reset the tint - binding.cropper.drawable.setTintList(null) - } - - showCropInterface(show = false) - } - - binding.videoView.setPlayer(mediaPlayer) - - mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong()) - - object : Runnable { - override fun run() { - val getCurrent = mediaPlayer.currentPosition / 1000f - if(getCurrent >= binding.videoRangeSeekBar.values[0] && getCurrent <= binding.videoRangeSeekBar.values[2] ) { - binding.videoRangeSeekBar.values = listOf(binding.videoRangeSeekBar.values[0],getCurrent, binding.videoRangeSeekBar.values[2]) - } - Handler(Looper.getMainLooper()).postDelayed(this, 1000) - } - }.run() - - binding.videoRangeSeekBar.addOnChangeListener { rangeSlider: RangeSlider, value, fromUser -> - // Responds to when the middle slider's value is changed - if(fromUser && value != rangeSlider.values[0] && value != rangeSlider.values[2]) { - mediaPlayer.seekTo((rangeSlider.values[1]*1000).toLong()) - } - } - - binding.videoRangeSeekBar.setLabelFormatter { value: Float -> - DateUtils.formatElapsedTime(value.toLong()) - } - - - - binding.speeder.setOnClickListener { - AlertDialog.Builder(this).apply { - setIcon(R.drawable.speed) - setTitle(R.string.video_speed) - setSingleChoiceItems(speedChoices.map { it.toString() + "x" }.toTypedArray(), speed) { dialog, which -> - // update the selected item which is selected by the user so that it should be selected - // when user opens the dialog next time and pass the instance to setSingleChoiceItems method - speed = which - - // when selected an item the dialog should be closed with the dismiss method - dialog.dismiss() - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - - binding.stabilizer.setOnClickListener { - AlertDialog.Builder(this).apply { - setIcon(R.drawable.video_stable) - setTitle(R.string.stabilize_video_intensity) - val slider = Slider(context).apply { - valueFrom = 0f - valueTo = 100f - value = stabilization - } - setView(slider) - setNegativeButton(android.R.string.cancel) { _, _ -> } - setPositiveButton(android.R.string.ok) { _, _ -> stabilization = slider.value} - }.show() - } - - - val thumbInterval: Float? = duration?.div(7) - - thumbInterval?.let { - thumbnail(videoUri, resultHandler, binding.thumbnail1, it) - thumbnail(videoUri, resultHandler, binding.thumbnail2, it.times(2)) - thumbnail(videoUri, resultHandler, binding.thumbnail3, it.times(3)) - thumbnail(videoUri, resultHandler, binding.thumbnail4, it.times(4)) - thumbnail(videoUri, resultHandler, binding.thumbnail5, it.times(5)) - thumbnail(videoUri, resultHandler, binding.thumbnail6, it.times(6)) - thumbnail(videoUri, resultHandler, binding.thumbnail7, it.times(7)) - } - - resetControls() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.edit_menu, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - - when(item.itemId) { - R.id.action_save -> { - returnWithValues() - } - R.id.action_reset -> { - resetControls() - } - } - - return super.onOptionsItemSelected(item) - } - - override fun onBackPressed() { - if(binding.cropImageView.isVisible) { - showCropInterface(false) - } else if (noEdits()) super.onBackPressed() - else { - val builder = AlertDialog.Builder(this) - builder.apply { - setMessage(R.string.save_before_returning) - setPositiveButton(android.R.string.ok) { _, _ -> - returnWithValues() - } - setNegativeButton(R.string.no_cancel_edit) { _, _ -> - super.onBackPressed() - } - } - // Create the AlertDialog - builder.show() - } - } - - private fun noEdits(): Boolean { - val videoPositions = binding.videoRangeSeekBar.values.let { - it[0] == 0f && it[2] == binding.videoRangeSeekBar.valueTo - } - val muted = binding.muter.isSelected - val speedUnchanged = speed == 1 - - val stabilizationUnchanged = stabilization <= 0.01f || stabilization > 100.5f - - return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped() && stabilizationUnchanged - } - - private fun showCropInterface(show: Boolean, uri: Uri? = null){ - val visibilityOfOthers = if(show) View.GONE else View.VISIBLE - val visibilityOfCrop = if(show) View.VISIBLE else View.GONE - - if(show) mediaPlayer.pause() - - if(show) binding.cropSavedCard.visibility = View.GONE - else if(!cropRelativeDimensions.notCropped()) binding.cropSavedCard.visibility = View.VISIBLE - - binding.stabilisationSaved.visibility = - if(!show && stabilization > 0.01f && stabilization <= 100f) View.VISIBLE - else View.GONE - - binding.muter.visibility = visibilityOfOthers - binding.speeder.visibility = visibilityOfOthers - binding.cropper.visibility = visibilityOfOthers - binding.stabilizer.visibility = visibilityOfOthers - binding.videoRangeSeekBar.visibility = visibilityOfOthers - binding.videoView.visibility = visibilityOfOthers - binding.thumbnail1.visibility = visibilityOfOthers - binding.thumbnail2.visibility = visibilityOfOthers - binding.thumbnail3.visibility = visibilityOfOthers - binding.thumbnail4.visibility = visibilityOfOthers - binding.thumbnail5.visibility = visibilityOfOthers - binding.thumbnail6.visibility = visibilityOfOthers - binding.thumbnail7.visibility = visibilityOfOthers - - - binding.cropImageView.visibility = visibilityOfCrop - binding.saveCropButton.visibility = visibilityOfCrop - - if(show && uri != null) binding.cropImageView.setImageUriAsync(uri, cropRelativeDimensions) - } - - private fun returnWithValues() { - //TODO Check if some of these should be null to indicate no changes in that category? Ex start/end - val intent = Intent() - .apply { - putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition) - putExtra(VIDEO_ARGUMENTS_TAG, VideoEditArguments( - binding.muter.isSelected, binding.videoRangeSeekBar.values.first(), - binding.videoRangeSeekBar.values[2], - speed, - cropRelativeDimensions, - stabilization - ) - ) - putExtra(MODIFIED, !noEdits()) - addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - } - setResult(Activity.RESULT_OK, intent) - finish() - } - - private fun resetControls() { - binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo) - binding.muter.isSelected = false - - binding.cropImageView.resetCropRect() - cropRelativeDimensions = RelativeCropPosition() - binding.cropper.drawable.setTintList(null) - binding.stabilizer.drawable.setTintList(null) - binding.cropSavedCard.visibility = View.GONE - stabilization = 0f - } - - override fun onDestroy() { - super.onDestroy() - sessionList.forEach { - FFmpegKit.cancel(it) - } - tempFiles.forEach{ - it.delete() - } - mediaPlayer.close() - } - - private fun thumbnail( - inputUri: Uri?, - resultHandler: Handler, - thumbnail: ImageView, - thumbTime: Float, - ) { - val file = File.createTempFile("temp_img", ".bmp", cacheDir) - tempFiles.add(file) - val fileUri = file.toUri() - val ffmpegCompliantUri = ffmpegCompliantUri(inputUri) - - val outputImagePath = - if(fileUri.toString().startsWith("content://")) - FFmpegKitConfig.getSafParameterForWrite(this, fileUri) - else fileUri.toString() - val session = FFmpegKit.executeWithArgumentsAsync(arrayOf( - "-noaccurate_seek", "-ss", "$thumbTime", "-i", ffmpegCompliantUri, "-vf", - "scale=${thumbnail.width}:${thumbnail.height}", - "-frames:v", "1", "-f", "image2", "-y", outputImagePath), { session -> - val state = session.state - val returnCode = session.returnCode - - if (ReturnCode.isSuccess(returnCode)) { - // SUCCESS - resultHandler.post { - if(!this.isFinishing) - Glide.with(this).load(outputImagePath).centerCrop().into(thumbnail) - } - } - // CALLED WHEN SESSION IS EXECUTED - Log.d("VideoEditActivity", "FFmpeg process exited with state $state and rc $returnCode.${session.failStackTrace}") - }, - {/* CALLED WHEN SESSION PRINTS LOGS */ }, { /*CALLED WHEN SESSION GENERATES STATISTICS*/ }) - sessionList.add(session.sessionId) - } - - override fun onPause() { - super.onPause() - mediaPlayer.pause() - } - - companion object { - const val VIDEO_ARGUMENTS_TAG = "org.pixeldroid.media_editor.VideoEditTag" - // List of choices of speeds - val speedChoices: List = listOf(0.5, 1, 2, 4, 8) - const val MODIFIED = "VideoEditModifiedTag" - - /** - * @param muted should audio tracks be removed in the output - * @param videoStart when we want to start the video, in seconds, or null if we - * don't want to remove the start - * @param videoEnd when we want to end the video, in seconds, or null if we - * don't want to remove the end - */ - fun startEncoding( - originalUri: Uri, - arguments: VideoEditArguments, - context: Context, - //TODO make interfaces for these callbacks, or something more explicit - registerNewFFmpegSession: (Uri, Long) -> Unit, - trackTempFile: (File) -> Unit, - videoEncodeProgress: (Uri, Int, Boolean, Uri?, Boolean) -> Unit, - ) { - - // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output - val suffix = originalUri.fileExtension(context.contentResolver) - val file = File.createTempFile("temp_video", ".$suffix", context.cacheDir) - //val file = File.createTempFile("temp_video", ".webm", cacheDir) - trackTempFile(file) - val fileUri = file.toUri() - val outputVideoPath = context.ffmpegCompliantUri(fileUri) - - val ffmpegCompliantUri: String = context.ffmpegCompliantUri(originalUri) - - val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(context.ffmpegCompliantUri(originalUri)).mediaInformation - val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull() - - fun secondPass(stabilizeString: String = ""){ - val speed = speedChoices[arguments.speedIndex] - - val mutedString = if(arguments.muted || arguments.speedIndex != 1) "-an" else null - val startString: List = if(arguments.videoStart != null) listOf("-ss", "${arguments.videoStart/speed.toFloat()}") else listOf(null, null) - - val endString: List = if(arguments.videoEnd != null) listOf("-to", "${arguments.videoEnd/speed.toFloat() - (arguments.videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null) - - // iw and ih are variables for the original width and height values, FFmpeg will know them - val cropString = if(arguments.videoCrop.notCropped()) "" else "crop=${arguments.videoCrop.relativeWidth}*iw:${arguments.videoCrop.relativeHeight}*ih:${arguments.videoCrop.relativeX}*iw:${arguments.videoCrop.relativeY}*ih" - val separator = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) "," else "" - val speedString = if(arguments.speedIndex != 1) "setpts=PTS/${speed}" else "" - - val separatorStabilize = if(stabilizeString == "" || (speedString == "" && cropString == "")) "" else "," - - val speedAndCropString: List = if(arguments.speedIndex!= 1 || !arguments.videoCrop.notCropped() || stabilizeString.isNotEmpty()) - listOf("-filter:v", stabilizeString + separatorStabilize + speedString + separator + cropString) - // Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding - else listOf("-c", "copy") - - // This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play) - val encodePreset: List = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null) - - val session: FFmpegSession = - FFmpegKit.executeWithArgumentsAsync(listOfNotNull( - startString[0], startString[1], - "-i", ffmpegCompliantUri, - speedAndCropString[0], speedAndCropString[1], - endString[0], endString[1], - mutedString, "-y", - encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3], - outputVideoPath, - ).toTypedArray(), - //val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath", - { session -> - val returnCode = session.returnCode - if (ReturnCode.isSuccess(returnCode)) { - - videoEncodeProgress(originalUri, 100, false, outputVideoPath.toUri(), false) - - Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds") - } else { - videoEncodeProgress(originalUri, 0, false, outputVideoPath.toUri(), true) - Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}") - } - }, - { log -> Log.d("PostCreationActivityEncoding", log.message) } - ) { statistics: Statistics? -> - - val timeInMilliseconds: Int? = statistics?.time - timeInMilliseconds?.let { - if (timeInMilliseconds > 0) { - val completePercentage = totalVideoDuration?.let { - val speedupDurationModifier = speedChoices[arguments.speedIndex].toFloat() - - val newTotalDuration = (it - (arguments.videoStart ?: 0f) - (it - (arguments.videoEnd ?: it)))/speedupDurationModifier - timeInMilliseconds / (10*newTotalDuration) - } - completePercentage?.let { - val rounded: Int = it.roundToInt() - videoEncodeProgress(originalUri, rounded, false, null, false) - } - Log.d(TAG, "Encoding video: %$completePercentage.") - } - } - } - registerNewFFmpegSession(originalUri, session.sessionId) - } - - fun stabilizationFirstPass(){ - - val shakeResultsFile = File.createTempFile("temp_shake_results", ".trf", context.cacheDir) - trackTempFile(shakeResultsFile) - val shakeResultsFileUri = shakeResultsFile.toUri() - val shakeResultsFileSafeUri = context.ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://") - - val inputSafeUri: String = context.ffmpegCompliantUri(originalUri) - - // Map chosen "stabilization force" to shakiness, from 3 to 10 - val shakiness = (0f..100f).convert(arguments.videoStabilize, 3f..10f).roundToInt() - - val analyzeVideoCommandList = listOf( - "-y", "-i", inputSafeUri, - "-vf", "vidstabdetect=shakiness=$shakiness:accuracy=15:result=$shakeResultsFileSafeUri", - "-f", "null", "-" - ).toTypedArray() - - val session: FFmpegSession = - FFmpegKit.executeWithArgumentsAsync(analyzeVideoCommandList, - { firstPass -> - if (ReturnCode.isSuccess(firstPass.returnCode)) { - // Map chosen "stabilization force" to shakiness, from 8 to 40 - val smoothing = (0f..100f).convert(arguments.videoStabilize, 8f..40f).roundToInt() - - val stabilizeVideoCommand = - "vidstabtransform=smoothing=$smoothing:input=${context.ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")}" - secondPass(stabilizeVideoCommand) - } else { - Log.e( - "PostCreationActivityEncoding", - "Video stabilization first pass failed!" - ) - } - }, - { log -> Log.d("PostCreationActivityEncoding", log.message) }, - { statistics: Statistics? -> - - val timeInMilliseconds: Int? = statistics?.time - timeInMilliseconds?.let { - if (timeInMilliseconds > 0) { - val completePercentage = totalVideoDuration?.let { - // At this stage, we didn't change speed or start/end of the video - timeInMilliseconds / (10 * it) - } - completePercentage?.let { - val rounded: Int = it.roundToInt() - videoEncodeProgress(originalUri, rounded, true, null, false) - } - - Log.d(TAG, "Stabilization pass: %$completePercentage.") - } - } - }) - registerNewFFmpegSession(originalUri, session.sessionId) - } - - if(arguments.videoStabilize > 0.01f) { - // Stabilization was requested: we need an additional first pass to get stabilization data - stabilizationFirstPass() - } else { - // Immediately call the second pass, no stabilization needed - secondPass() - } - - } - - fun cancelEncoding(){ - FFmpegKit.cancel() - } - fun cancelEncoding(sessionId: Long){ - FFmpegKit.cancel(sessionId) - } - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt deleted file mode 100644 index 46567226..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit.cropper - -// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is -// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid -// are under licensed under the GPLv3 or later, just like the rest of the PixelDroid project - -import android.content.Context -import android.graphics.Rect -import android.graphics.RectF -import android.graphics.drawable.Drawable -import android.net.Uri -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.FrameLayout -import androidx.core.graphics.toRect -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target -import org.pixeldroid.media_editor.databinding.CropImageViewBinding -import org.pixeldroid.media_editor.photoEdit.VideoEditActivity - - -/** Custom view that provides cropping capabilities to an image. */ -class CropImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : - FrameLayout(context!!, attrs) { - - - private val binding: CropImageViewBinding = - CropImageViewBinding.inflate(LayoutInflater.from(context), this, true) - - init { - binding.CropOverlayView.setInitialAttributeValues() - } - - /** - * Gets the crop window's position relative to the parent's view at screen. - * - * @return a Rect instance containing notCropped area boundaries of the source Bitmap - */ - val cropWindowRect: RectF - get() = binding.CropOverlayView.cropWindowRect - - - /** Reset crop window to initial rectangle. */ - fun resetCropRect() { - binding.CropOverlayView.resetCropWindowRect() - } - - fun getInitialCropWindowRect(): Rect = binding.CropOverlayView.initialCropWindowRect - - /** - * Sets the image loaded from the given URI as the content of the CropImageView - * - * @param uri the URI to load the image from - */ - fun setImageUriAsync(uri: Uri, cropRelativeDimensions: VideoEditActivity.RelativeCropPosition) { - // either no existing task is working or we canceled it, need to load new URI - binding.CropOverlayView.initialCropWindowRect = Rect() - - Glide.with(this).load(uri).fitCenter().listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - m: Any?, - t: Target?, - i: Boolean, - ): Boolean { - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean, - ): Boolean { - // Get width and height that the image will take on the screen - val drawnWidth = resource?.intrinsicWidth ?: width - val drawnHeight = resource?.intrinsicHeight ?: height - - binding.CropOverlayView.initialCropWindowRect = RectF( - (width - drawnWidth) / 2f, - (height - drawnHeight) / 2f, - (width + drawnWidth) / 2f, - (height + drawnHeight) / 2f - ).toRect() - binding.CropOverlayView.setCropWindowLimits( - drawnWidth.toFloat(), - drawnHeight.toFloat() - ) - binding.CropOverlayView.invalidate() - binding.CropOverlayView.setBounds(width, height) - binding.CropOverlayView.resetCropOverlayView() - if (!cropRelativeDimensions.notCropped()) binding.CropOverlayView.setRecordedCropWindowRect(cropRelativeDimensions) - binding.CropOverlayView.visibility = VISIBLE - - - // Indicate to Glide that the image hasn't been set yet - return false - } - }).into(binding.ImageViewImage) - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt deleted file mode 100644 index 152aa7c0..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt +++ /dev/null @@ -1,490 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit.cropper - -// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is -// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid -// are under licensed under the GPLv3 or later, just like the rest of the PixelDroid project - - -import android.content.Context -import android.content.res.Resources -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RectF -import android.util.AttributeSet -import android.util.TypedValue -import android.view.MotionEvent -import android.view.View -import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition -import kotlin.math.max -import kotlin.math.min - -/** A custom View representing the crop window and the shaded background outside the crop window. */ -class CropOverlayView // endregion -@JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : View(context, attrs) { - // region: Fields and Consts - /** Handler from crop window stuff, moving and knowing position. */ - private val mCropWindowHandler = CropWindowHandler() - - /** The Paint used to draw the white rectangle around the crop area. */ - private var mBorderPaint: Paint? = null - - /** The Paint used to draw the corners of the Border */ - private var mBorderCornerPaint: Paint? = null - - /** The Paint used to draw the guidelines within the crop area when pressed. */ - private var mGuidelinePaint: Paint? = null - - /** The bounding box around the Bitmap that we are cropping. */ - private val mCalcBounds = RectF() - - /** The bounding image view width used to know the crop overlay is at view edges. */ - private var mViewWidth = 0 - - /** The bounding image view height used to know the crop overlay is at view edges. */ - private var mViewHeight = 0 - - /** The Handle that is currently pressed; null if no Handle is pressed. */ - private var mMoveHandler: CropWindowMoveHandler? = null - - /** the initial crop window rectangle to set */ - private val mInitialCropWindowRect = Rect() - - /** Whether the Crop View has been initialized for the first time */ - private var initializedCropWindow = false - /** Get the left/top/right/bottom coordinates of the crop window. */ - /** Set the left/top/right/bottom coordinates of the crop window. */ - var cropWindowRect: RectF - get() = mCropWindowHandler.rect - set(rect) { - mCropWindowHandler.rect = rect - } - - /** - * Informs the CropOverlayView of the image's position relative to the ImageView. This is - * necessary to call in order to draw the crop window. - * - * @param viewWidth The bounding image view width. - * @param viewHeight The bounding image view height. - */ - fun setBounds(viewWidth: Int, viewHeight: Int) { - mViewWidth = viewWidth - mViewHeight = viewHeight - val cropRect = mCropWindowHandler.rect - if (cropRect.width() == 0f || cropRect.height() == 0f) { - initCropWindow() - } - } - - /** Resets the crop overlay view. */ - fun resetCropOverlayView() { - if (initializedCropWindow) { - cropWindowRect = RectF() - initCropWindow() - invalidate() - } - } - - /** - * Set the max width/height and scale factor of the shown image to original image to scale the - * limits appropriately. - */ - fun setCropWindowLimits(maxWidth: Float, maxHeight: Float) { - mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight) - } - /** Get crop window initial rectangle. */ - /** Set crop window initial rectangle to be used instead of default. */ - var initialCropWindowRect: Rect - get() = mInitialCropWindowRect - set(rect) { - mInitialCropWindowRect.set(rect) - if (initializedCropWindow) { - initCropWindow() - invalidate() - } - } - - fun setRecordedCropWindowRect(relativeCropPosition: RelativeCropPosition) { - val rect = RectF( - mInitialCropWindowRect.left + relativeCropPosition.relativeX * mInitialCropWindowRect.width(), - mInitialCropWindowRect.top + relativeCropPosition.relativeY * mInitialCropWindowRect.height(), - relativeCropPosition.relativeWidth * mInitialCropWindowRect.width() + mInitialCropWindowRect.left + relativeCropPosition.relativeX * mInitialCropWindowRect.width(), - relativeCropPosition.relativeHeight * mInitialCropWindowRect.height() + mInitialCropWindowRect.top + relativeCropPosition.relativeY * mInitialCropWindowRect.height() - ) - mCropWindowHandler.rect = rect - } - - /** Reset crop window to initial rectangle. */ - fun resetCropWindowRect() { - if (initializedCropWindow) { - initCropWindow() - invalidate() - } - } - - /** - * Sets all initial values, but does not call initCropWindow to reset the views.

- * Used once at the very start to initialize the attributes. - */ - fun setInitialAttributeValues() { - val dm = Resources.getSystem().displayMetrics - mBorderPaint = getNewPaintOfThickness( - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, dm), - Color.argb(170, 255, 255, 255) - ) - mBorderCornerPaint = getNewPaintOfThickness( - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, dm), - Color.WHITE - ) - mGuidelinePaint = getNewPaintOfThickness( - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, dm), - Color.argb(170, 255, 255, 255) - ) - } - // region: Private methods - /** - * Set the initial crop window size and position. This is dependent on the size and position of - * the image being cropped. - */ - private fun initCropWindow() { - val rect = RectF() - - // Tells the attribute functions the crop window has already been initialized - initializedCropWindow = true - if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) { - // Get crop window position relative to the displayed image. - rect.left = mInitialCropWindowRect.left.toFloat() - rect.top = mInitialCropWindowRect.top.toFloat() - rect.right = rect.left + mInitialCropWindowRect.width() - rect.bottom = rect.top + mInitialCropWindowRect.height() - } - fixCropWindowRectByRules(rect) - mCropWindowHandler.rect = rect - } - - /** Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. */ - private fun fixCropWindowRectByRules(rect: RectF) { - if (rect.width() < mCropWindowHandler.minCropWidth) { - val adj = (mCropWindowHandler.minCropWidth - rect.width()) / 2 - rect.left -= adj - rect.right += adj - } - if (rect.height() < mCropWindowHandler.minCropHeight) { - val adj = (mCropWindowHandler.minCropHeight - rect.height()) / 2 - rect.top -= adj - rect.bottom += adj - } - if (rect.width() > mCropWindowHandler.maxCropWidth) { - val adj = (rect.width() - mCropWindowHandler.maxCropWidth) / 2 - rect.left += adj - rect.right -= adj - } - if (rect.height() > mCropWindowHandler.maxCropHeight) { - val adj = (rect.height() - mCropWindowHandler.maxCropHeight) / 2 - rect.top += adj - rect.bottom -= adj - } - setBounds() - if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) { - val leftLimit = max(mCalcBounds.left, 0f) - val topLimit = max(mCalcBounds.top, 0f) - val rightLimit = min(mCalcBounds.right, width.toFloat()) - val bottomLimit = min(mCalcBounds.bottom, height.toFloat()) - if (rect.left < leftLimit) { - rect.left = leftLimit - } - if (rect.top < topLimit) { - rect.top = topLimit - } - if (rect.right > rightLimit) { - rect.right = rightLimit - } - if (rect.bottom > bottomLimit) { - rect.bottom = bottomLimit - } - } - } - - /** - * Draw crop overview by drawing background over image not in the cropping area, then borders and - * guidelines. - */ - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - // Draw translucent background for the notCropped area. - drawBackground(canvas) - if (mCropWindowHandler.showGuidelines()) { - // Determines whether guidelines should be drawn or not - if (mMoveHandler != null) { - // Draw only when resizing - drawGuidelines(canvas) - } - } - drawBorders(canvas) - drawCorners(canvas) - } - - /** Draw shadow background over the image not including the crop area. */ - private fun drawBackground(canvas: Canvas) { - val rect = mCropWindowHandler.rect - val background = getNewPaint(Color.argb(119, 0, 0, 0)) - canvas.drawRect( - mInitialCropWindowRect.left.toFloat(), - mInitialCropWindowRect.top.toFloat(), - rect.left, - mInitialCropWindowRect.bottom.toFloat(), - background - ) - canvas.drawRect( - rect.left, - rect.bottom, - mInitialCropWindowRect.right.toFloat(), - mInitialCropWindowRect.bottom.toFloat(), - background - ) - canvas.drawRect( - rect.right, - mInitialCropWindowRect.top.toFloat(), - mInitialCropWindowRect.right.toFloat(), - rect.bottom, - background - ) - canvas.drawRect( - rect.left, - mInitialCropWindowRect.top.toFloat(), - rect.right, - rect.top, - background - ) - } - - /** - * Draw 2 vertical and 2 horizontal guidelines inside the cropping area to split it into 9 equal - * parts. - */ - private fun drawGuidelines(canvas: Canvas) { - if (mGuidelinePaint != null) { - val sw: Float = if (mBorderPaint != null) mBorderPaint!!.strokeWidth else 0f - val rect = mCropWindowHandler.rect - rect.inset(sw, sw) - val oneThirdCropWidth = rect.width() / 3 - val oneThirdCropHeight = rect.height() / 3 - - // Draw vertical guidelines. - val x1 = rect.left + oneThirdCropWidth - val x2 = rect.right - oneThirdCropWidth - canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint!!) - canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint!!) - - // Draw horizontal guidelines. - val y1 = rect.top + oneThirdCropHeight - val y2 = rect.bottom - oneThirdCropHeight - canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint!!) - canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint!!) - } - } - - /** Draw borders of the crop area. */ - private fun drawBorders(canvas: Canvas) { - if (mBorderPaint != null) { - val w = mBorderPaint!!.strokeWidth - val rect = mCropWindowHandler.rect - // Make the rectangle a bit smaller to accommodate for the border - rect.inset(w / 2, w / 2) - - // Draw rectangle crop window border. - canvas.drawRect(rect, mBorderPaint!!) - } - } - - /** Draw the corner of crop overlay. */ - private fun drawCorners(canvas: Canvas) { - val dm = Resources.getSystem().displayMetrics - if (mBorderCornerPaint != null) { - val lineWidth: Float = if (mBorderPaint != null) mBorderPaint!!.strokeWidth else 0f - val cornerWidth = mBorderCornerPaint!!.strokeWidth - - // The corners should be a bit offset from the borders - val w = (cornerWidth / 2 - + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, dm)) - val rect = mCropWindowHandler.rect - rect.inset(w, w) - val cornerOffset = (cornerWidth - lineWidth) / 2 - val cornerExtension = cornerWidth / 2 + cornerOffset - - /* the length of the border corner to draw */ - val mBorderCornerLength = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14f, dm) - - // Top left - canvas.drawLine( - rect.left - cornerOffset, - rect.top - cornerExtension, - rect.left - cornerOffset, - rect.top + mBorderCornerLength, - mBorderCornerPaint!! - ) - canvas.drawLine( - rect.left - cornerExtension, - rect.top - cornerOffset, - rect.left + mBorderCornerLength, - rect.top - cornerOffset, - mBorderCornerPaint!! - ) - - // Top right - canvas.drawLine( - rect.right + cornerOffset, - rect.top - cornerExtension, - rect.right + cornerOffset, - rect.top + mBorderCornerLength, - mBorderCornerPaint!! - ) - canvas.drawLine( - rect.right + cornerExtension, - rect.top - cornerOffset, - rect.right - mBorderCornerLength, - rect.top - cornerOffset, - mBorderCornerPaint!! - ) - - // Bottom left - canvas.drawLine( - rect.left - cornerOffset, - rect.bottom + cornerExtension, - rect.left - cornerOffset, - rect.bottom - mBorderCornerLength, - mBorderCornerPaint!! - ) - canvas.drawLine( - rect.left - cornerExtension, - rect.bottom + cornerOffset, - rect.left + mBorderCornerLength, - rect.bottom + cornerOffset, - mBorderCornerPaint!! - ) - - // Bottom left - canvas.drawLine( - rect.right + cornerOffset, - rect.bottom + cornerExtension, - rect.right + cornerOffset, - rect.bottom - mBorderCornerLength, - mBorderCornerPaint!! - ) - canvas.drawLine( - rect.right + cornerExtension, - rect.bottom + cornerOffset, - rect.right - mBorderCornerLength, - rect.bottom + cornerOffset, - mBorderCornerPaint!! - ) - } - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - // If this View is not enabled, don't allow for touch interactions. - return if (isEnabled) { - /* Boolean to see if multi touch is enabled for the crop rectangle */ - when (event.action) { - MotionEvent.ACTION_DOWN -> { - onActionDown(event.x, event.y) - true - } - - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - parent.requestDisallowInterceptTouchEvent(false) - onActionUp() - true - } - - MotionEvent.ACTION_MOVE -> { - onActionMove(event.x, event.y) - parent.requestDisallowInterceptTouchEvent(true) - true - } - - else -> false - } - } else { - false - } - } - - /** - * On press down start crop window movement depending on the location of the press.

- * if press is far from crop window then no move handler is returned (null). - */ - private fun onActionDown(x: Float, y: Float) { - val dm = Resources.getSystem().displayMetrics - mMoveHandler = mCropWindowHandler.getMoveHandler( - x, - y, - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, dm) - ) - if (mMoveHandler != null) { - invalidate() - } - } - - /** Clear move handler starting in [.onActionDown] if exists. */ - private fun onActionUp() { - if (mMoveHandler != null) { - mMoveHandler = null - invalidate() - } - } - - /** - * Handle move of crop window using the move handler created in [.onActionDown].

- * The move handler will do the proper move/resize of the crop window. - */ - private fun onActionMove(x: Float, y: Float) { - if (mMoveHandler != null) { - val rect = mCropWindowHandler.rect - setBounds() - val dm = Resources.getSystem().displayMetrics - val snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, dm) - mMoveHandler!!.move( - rect, - x, - y, - mCalcBounds, - mViewWidth, - mViewHeight, - snapRadius - ) - mCropWindowHandler.rect = rect - invalidate() - } - } - - /** - * Calculate the bounding rectangle for current crop window - * The bounds rectangle is the bitmap rectangle - */ - private fun setBounds() { - mCalcBounds.set(mInitialCropWindowRect) - } - - companion object { - /** Creates the Paint object for drawing. */ - private fun getNewPaint(color: Int): Paint { - val paint = Paint() - paint.color = color - return paint - } - - /** Creates the Paint object for given thickness and color */ - private fun getNewPaintOfThickness(thickness: Float, color: Int): Paint { - val borderPaint = Paint() - borderPaint.color = color - borderPaint.strokeWidth = thickness - borderPaint.style = Paint.Style.STROKE - borderPaint.isAntiAlias = true - return borderPaint - } - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt deleted file mode 100644 index fb77634e..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt +++ /dev/null @@ -1,269 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit.cropper - -// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is -// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid -// are under licensed under the GPLv3 or later, just like the rest of the PixelDroid project - - -import android.content.res.Resources -import android.graphics.RectF -import android.util.TypedValue -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -/** Handler from crop window stuff, moving and knowing position. */ -internal class CropWindowHandler { - /** The 4 edges of the crop window defining its coordinates and size */ - private val mEdges = RectF() - - /** - * Rectangle used to return the edges rectangle without ability to change it and without - * creating new all the time. - */ - private val mGetEdges = RectF() - - /** Maximum width in pixels that the crop window can CURRENTLY get. */ - private var mMaxCropWindowWidth = 0f - - /** Maximum height in pixels that the crop window can CURRENTLY get. */ - private var mMaxCropWindowHeight = 0f - - /** The left/top/right/bottom coordinates of the crop window. */ - var rect: RectF - get() { - mGetEdges.set(mEdges) - return mGetEdges - } - set(rect) { - mEdges.set(rect) - } - - /** Minimum width in pixels that the crop window can get. */ - val minCropWidth: Float - get() { - val dm = Resources.getSystem().displayMetrics - val mMinCropResultWidth = 40f - return max( - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, dm).toInt().toFloat(), - mMinCropResultWidth - ) - } - - /** Minimum height in pixels that the crop window can get. */ - val minCropHeight: Float - get() { - val dm = Resources.getSystem().displayMetrics - val mMinCropResultHeight = 40f - return max( - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, dm).toInt().toFloat(), - mMinCropResultHeight - ) - } - - /** Maximum width in pixels that the crop window can get. */ - val maxCropWidth: Float - get() { - val mMaxCropResultWidth = 99999f - return min(mMaxCropWindowWidth, mMaxCropResultWidth) - } - - /** Maximum height in pixels that the crop window can get. */ - val maxCropHeight: Float - get() { - val mMaxCropResultHeight = 99999f - return min(mMaxCropWindowHeight, mMaxCropResultHeight) - } - - /** - * Set the max width/height of the shown image to original image to scale the limits appropriately - */ - fun setCropWindowLimits(maxWidth: Float, maxHeight: Float) { - mMaxCropWindowWidth = maxWidth - mMaxCropWindowHeight = maxHeight - } - - /** - * Indicates whether the crop window is small enough that the guidelines should be shown. Public - * because this function is also used to determine if the center handle should be focused. - * - * @return boolean Whether the guidelines should be shown or not - */ - fun showGuidelines(): Boolean { - return !(mEdges.width() < 100 || mEdges.height() < 100) - } - - /** - * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding - * box, and the touch radius. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param targetRadius the target radius in pixels - * @return the Handle that was pressed; null if no Handle was pressed - */ - fun getMoveHandler(x: Float, y: Float, targetRadius: Float): CropWindowMoveHandler? { - val type = getRectanglePressedMoveType(x, y, targetRadius) - return if (type != null) CropWindowMoveHandler(type, this, x, y) else null - } - // region: Private methods - /** - * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding - * box, and the touch radius. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param targetRadius the target radius in pixels - * @return the Handle that was pressed; null if no Handle was pressed - */ - private fun getRectanglePressedMoveType( - x: Float, y: Float, targetRadius: Float - ): CropWindowMoveHandler.Type? { - var moveType: CropWindowMoveHandler.Type? = null - - // Note: corner-handles take precedence, then side-handles, then center. - if (isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) { - moveType = CropWindowMoveHandler.Type.TOP_LEFT - } else if (isInCornerTargetZone( - x, y, mEdges.right, mEdges.top, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.TOP_RIGHT - } else if (isInCornerTargetZone( - x, y, mEdges.left, mEdges.bottom, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT - } else if (isInCornerTargetZone( - x, y, mEdges.right, mEdges.bottom, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT - } else if (isInCenterTargetZone( - x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom - ) - && focusCenter() - ) { - moveType = CropWindowMoveHandler.Type.CENTER - } else if (isInHorizontalTargetZone( - x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.TOP - } else if (isInHorizontalTargetZone( - x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.BOTTOM - } else if (isInVerticalTargetZone( - x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.LEFT - } else if (isInVerticalTargetZone( - x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius - ) - ) { - moveType = CropWindowMoveHandler.Type.RIGHT - } else if (isInCenterTargetZone( - x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom - ) - && !focusCenter() - ) { - moveType = CropWindowMoveHandler.Type.CENTER - } - return moveType - } - - /** - * Determines if the cropper should focus on the center handle or the side handles. If it is a - * small image, focus on the center handle so the user can move it. If it is a large image, focus - * on the side handles so user can grab them. Corresponds to the appearance of the - * RuleOfThirdsGuidelines. - * - * @return true if it is small enough such that it should focus on the center; less than - * show_guidelines limit - */ - private fun focusCenter(): Boolean = !showGuidelines() - - // endregion - - companion object { - /** - * Determines if the specified coordinate is in the target touch zone for a corner handle. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param handleX the x-coordinate of the corner handle - * @param handleY the y-coordinate of the corner handle - * @param targetRadius the target radius in pixels - * @return true if the touch point is in the target touch zone; false otherwise - */ - private fun isInCornerTargetZone( - x: Float, y: Float, handleX: Float, handleY: Float, targetRadius: Float - ): Boolean { - return abs(x - handleX) <= targetRadius && abs(y - handleY) <= targetRadius - } - - /** - * Determines if the specified coordinate is in the target touch zone for a horizontal bar handle. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param handleXStart the left x-coordinate of the horizontal bar handle - * @param handleXEnd the right x-coordinate of the horizontal bar handle - * @param handleY the y-coordinate of the horizontal bar handle - * @param targetRadius the target radius in pixels - * @return true if the touch point is in the target touch zone; false otherwise - */ - private fun isInHorizontalTargetZone( - x: Float, - y: Float, - handleXStart: Float, - handleXEnd: Float, - handleY: Float, - targetRadius: Float - ): Boolean { - return x > handleXStart && x < handleXEnd && abs(y - handleY) <= targetRadius - } - - /** - * Determines if the specified coordinate is in the target touch zone for a vertical bar handle. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param handleX the x-coordinate of the vertical bar handle - * @param handleYStart the top y-coordinate of the vertical bar handle - * @param handleYEnd the bottom y-coordinate of the vertical bar handle - * @param targetRadius the target radius in pixels - * @return true if the touch point is in the target touch zone; false otherwise - */ - private fun isInVerticalTargetZone( - x: Float, - y: Float, - handleX: Float, - handleYStart: Float, - handleYEnd: Float, - targetRadius: Float - ): Boolean { - return abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd - } - - /** - * Determines if the specified coordinate falls anywhere inside the given bounds. - * - * @param x the x-coordinate of the touch point - * @param y the y-coordinate of the touch point - * @param left the x-coordinate of the left bound - * @param top the y-coordinate of the top bound - * @param right the x-coordinate of the right bound - * @param bottom the y-coordinate of the bottom bound - * @return true if the touch point is inside the bounding rectangle; false otherwise - */ - private fun isInCenterTargetZone( - x: Float, y: Float, left: Float, top: Float, right: Float, bottom: Float - ): Boolean { - return x > left && x < right && y > top && y < bottom - } - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt deleted file mode 100644 index 920d7774..00000000 --- a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt +++ /dev/null @@ -1,405 +0,0 @@ -package org.pixeldroid.media_editor.photoEdit.cropper - -// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is -// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid -// are under licensed under the GPLv3 or later, just like the rest of the PixelDroid project - -import android.graphics.PointF -import android.graphics.RectF - -/** - * Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center. - */ -internal class CropWindowMoveHandler( - /** The type of crop window move that is handled. */ - private val mType: Type, - cropWindowHandler: CropWindowHandler, touchX: Float, touchY: Float -) { - /** Minimum width in pixels that the crop window can get. */ - private val mMinCropWidth: Float - - /** Minimum width in pixels that the crop window can get. */ - private val mMinCropHeight: Float - - /** Maximum height in pixels that the crop window can get. */ - private val mMaxCropWidth: Float - - /** Maximum height in pixels that the crop window can get. */ - private val mMaxCropHeight: Float - - /** - * Holds the x and y offset between the exact touch location and the exact handle location that is - * activated. There may be an offset because we allow for some leeway (specified by mHandleRadius) - * in activating a handle. However, we want to maintain these offset values while the handle is - * being dragged so that the handle doesn't jump. - */ - private val mTouchOffset = PointF() - - init { - mMinCropWidth = cropWindowHandler.minCropWidth - mMinCropHeight = cropWindowHandler.minCropHeight - mMaxCropWidth = cropWindowHandler.maxCropWidth - mMaxCropHeight = cropWindowHandler.maxCropHeight - calculateTouchOffset(cropWindowHandler.rect, touchX, touchY) - } - - /** - * Updates the crop window by change in the touch location. - * Move type handled by this instance, as initialized in creation, affects how the change in - * touch location changes the crop window position and size. - * After the crop window position/size is changed by touch move it may result in values that - * violate constraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or - * mismatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it - * by the "primary" edge movement. - * Primary is the edge directly affected by move type, secondary is the other edge. - * The crop window is changed by directly setting the Edge coordinates. - * - * @param x the new x-coordinate of this handle - * @param y the new y-coordinate of this handle - * @param bounds the bounding rectangle of the image - * @param viewWidth The bounding image view width used to know the crop overlay is at view edges. - * @param viewHeight The bounding image view height used to know the crop overlay is at view - * edges. - * @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the - * image - */ - fun move( - rect: RectF, - x: Float, - y: Float, - bounds: RectF, - viewWidth: Int, - viewHeight: Int, - snapMargin: Float - ) { - - // Adjust the coordinates for the finger position's offset (i.e. the - // distance from the initial touch to the precise handle location). - // We want to maintain the initial touch's distance to the pressed - // handle so that the crop window size does not "jump". - val adjX = x + mTouchOffset.x - val adjY = y + mTouchOffset.y - if (mType == Type.CENTER) { - moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin) - } else { - changeSize(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin) - } - } - // region: Private methods - /** - * Calculates the offset of the touch point from the precise location of the specified handle.

- * Save these values in a member variable since we want to maintain this offset as we drag the - * handle. - */ - private fun calculateTouchOffset(rect: RectF, touchX: Float, touchY: Float) { - var touchOffsetX = 0f - var touchOffsetY = 0f - when (mType) { - Type.TOP_LEFT -> { - touchOffsetX = rect.left - touchX - touchOffsetY = rect.top - touchY - } - - Type.TOP_RIGHT -> { - touchOffsetX = rect.right - touchX - touchOffsetY = rect.top - touchY - } - - Type.BOTTOM_LEFT -> { - touchOffsetX = rect.left - touchX - touchOffsetY = rect.bottom - touchY - } - - Type.BOTTOM_RIGHT -> { - touchOffsetX = rect.right - touchX - touchOffsetY = rect.bottom - touchY - } - - Type.LEFT -> { - touchOffsetX = rect.left - touchX - touchOffsetY = 0f - } - - Type.TOP -> { - touchOffsetX = 0f - touchOffsetY = rect.top - touchY - } - - Type.RIGHT -> { - touchOffsetX = rect.right - touchX - touchOffsetY = 0f - } - - Type.BOTTOM -> { - touchOffsetX = 0f - touchOffsetY = rect.bottom - touchY - } - - Type.CENTER -> { - touchOffsetX = rect.centerX() - touchX - touchOffsetY = rect.centerY() - touchY - } - } - mTouchOffset.x = touchOffsetX - mTouchOffset.y = touchOffsetY - } - - /** Center move only changes the position of the crop window without changing the size. */ - private fun moveCenter( - rect: RectF, - x: Float, - y: Float, - bounds: RectF, - viewWidth: Int, - viewHeight: Int, - snapRadius: Float - ) { - var dx = x - rect.centerX() - var dy = y - rect.centerY() - if (rect.left + dx < 0 || rect.right + dx > viewWidth || rect.left + dx < bounds.left || rect.right + dx > bounds.right) { - dx /= 1.05f - mTouchOffset.x -= dx / 2 - } - if (rect.top + dy < 0 || rect.bottom + dy > viewHeight || rect.top + dy < bounds.top || rect.bottom + dy > bounds.bottom) { - dy /= 1.05f - mTouchOffset.y -= dy / 2 - } - rect.offset(dx, dy) - snapEdgesToBounds(rect, bounds, snapRadius) - } - - /** - * Change the size of the crop window on the required edge (or edges in the case of a corner) - */ - private fun changeSize( - rect: RectF, - x: Float, - y: Float, - bounds: RectF, - viewWidth: Int, - viewHeight: Int, - snapMargin: Float - ) { - when (mType) { - Type.TOP_LEFT -> { - adjustTop(rect, y, bounds, snapMargin) - adjustLeft(rect, x, bounds, snapMargin) - } - - Type.TOP_RIGHT -> { - adjustTop(rect, y, bounds, snapMargin) - adjustRight(rect, x, bounds, viewWidth, snapMargin) - } - - Type.BOTTOM_LEFT -> { - adjustBottom(rect, y, bounds, viewHeight, snapMargin) - adjustLeft(rect, x, bounds, snapMargin) - } - - Type.BOTTOM_RIGHT -> { - adjustBottom(rect, y, bounds, viewHeight, snapMargin) - adjustRight(rect, x, bounds, viewWidth, snapMargin) - } - - Type.LEFT -> adjustLeft(rect, x, bounds, snapMargin) - Type.TOP -> adjustTop(rect, y, bounds, snapMargin) - Type.RIGHT -> adjustRight(rect, x, bounds, viewWidth, snapMargin) - Type.BOTTOM -> adjustBottom(rect, y, bounds, viewHeight, snapMargin) - else -> {} - } - } - - /** Check if edges have gone out of bounds (including snap margin), and fix if needed. */ - private fun snapEdgesToBounds(edges: RectF, bounds: RectF, margin: Float) { - if (edges.left < bounds.left + margin) { - edges.offset(bounds.left - edges.left, 0f) - } - if (edges.top < bounds.top + margin) { - edges.offset(0f, bounds.top - edges.top) - } - if (edges.right > bounds.right - margin) { - edges.offset(bounds.right - edges.right, 0f) - } - if (edges.bottom > bounds.bottom - margin) { - edges.offset(0f, bounds.bottom - edges.bottom) - } - } - - /** - * Get the resulting x-position of the left edge of the crop window given the handle's position - * and the image's bounding box and snap radius. - * - * @param left the position that the left edge is dragged to - * @param bounds the bounding box of the image that is being notCropped - * @param snapMargin the snap distance to the image edge (in pixels) - */ - private fun adjustLeft( - rect: RectF, - left: Float, - bounds: RectF, - snapMargin: Float - ) { - var newLeft = left - if (newLeft < 0) { - newLeft /= 1.05f - mTouchOffset.x -= newLeft / 1.1f - } - if (newLeft < bounds.left) { - mTouchOffset.x -= (newLeft - bounds.left) / 2f - } - if (newLeft - bounds.left < snapMargin) { - newLeft = bounds.left - } - - // Checks if the window is too small horizontally - if (rect.right - newLeft < mMinCropWidth) { - newLeft = rect.right - mMinCropWidth - } - - // Checks if the window is too large horizontally - if (rect.right - newLeft > mMaxCropWidth) { - newLeft = rect.right - mMaxCropWidth - } - if (newLeft - bounds.left < snapMargin) { - newLeft = bounds.left - } - rect.left = newLeft - } - - /** - * Get the resulting x-position of the right edge of the crop window given the handle's position - * and the image's bounding box and snap radius. - * - * @param right the position that the right edge is dragged to - * @param bounds the bounding box of the image that is being notCropped - * @param viewWidth - * @param snapMargin the snap distance to the image edge (in pixels) - */ - private fun adjustRight( - rect: RectF, - right: Float, - bounds: RectF, - viewWidth: Int, - snapMargin: Float - ) { - var newRight = right - if (newRight > viewWidth) { - newRight = viewWidth + (newRight - viewWidth) / 1.05f - mTouchOffset.x -= (newRight - viewWidth) / 1.1f - } - if (newRight > bounds.right) { - mTouchOffset.x -= (newRight - bounds.right) / 2f - } - - // If close to the edge - if (bounds.right - newRight < snapMargin) { - newRight = bounds.right - } - - // Checks if the window is too small horizontally - if (newRight - rect.left < mMinCropWidth) { - newRight = rect.left + mMinCropWidth - } - - // Checks if the window is too large horizontally - if (newRight - rect.left > mMaxCropWidth) { - newRight = rect.left + mMaxCropWidth - } - - // If close to the edge - if (bounds.right - newRight < snapMargin) { - newRight = bounds.right - } - rect.right = newRight - } - - /** - * Get the resulting y-position of the top edge of the crop window given the handle's position and - * the image's bounding box and snap radius. - * - * @param top the x-position that the top edge is dragged to - * @param bounds the bounding box of the image that is being notCropped - * @param snapMargin the snap distance to the image edge (in pixels) - */ - private fun adjustTop( - rect: RectF, - top: Float, - bounds: RectF, - snapMargin: Float - ) { - var newTop = top - if (newTop < 0) { - newTop /= 1.05f - mTouchOffset.y -= newTop / 1.1f - } - if (newTop < bounds.top) { - mTouchOffset.y -= (newTop - bounds.top) / 2f - } - if (newTop - bounds.top < snapMargin) { - newTop = bounds.top - } - - // Checks if the window is too small vertically - if (rect.bottom - newTop < mMinCropHeight) { - newTop = rect.bottom - mMinCropHeight - } - - // Checks if the window is too large vertically - if (rect.bottom - newTop > mMaxCropHeight) { - newTop = rect.bottom - mMaxCropHeight - } - if (newTop - bounds.top < snapMargin) { - newTop = bounds.top - } - rect.top = newTop - } - - /** - * Get the resulting y-position of the bottom edge of the crop window given the handle's position - * and the image's bounding box and snap radius. - * - * @param bottom the position that the bottom edge is dragged to - * @param bounds the bounding box of the image that is being notCropped - * @param viewHeight - * @param snapMargin the snap distance to the image edge (in pixels) - */ - private fun adjustBottom( - rect: RectF, - bottom: Float, - bounds: RectF, - viewHeight: Int, - snapMargin: Float - ) { - var newBottom = bottom - if (newBottom > viewHeight) { - newBottom = viewHeight + (newBottom - viewHeight) / 1.05f - mTouchOffset.y -= (newBottom - viewHeight) / 1.1f - } - if (newBottom > bounds.bottom) { - mTouchOffset.y -= (newBottom - bounds.bottom) / 2f - } - if (bounds.bottom - newBottom < snapMargin) { - newBottom = bounds.bottom - } - - // Checks if the window is too small vertically - if (newBottom - rect.top < mMinCropHeight) { - newBottom = rect.top + mMinCropHeight - } - - // Checks if the window is too small vertically - if (newBottom - rect.top > mMaxCropHeight) { - newBottom = rect.top + mMaxCropHeight - } - if (bounds.bottom - newBottom < snapMargin) { - newBottom = bounds.bottom - } - rect.bottom = newBottom - } - // endregion - - /** The type of crop window move that is handled. */ - enum class Type { - TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, LEFT, TOP, RIGHT, BOTTOM, CENTER - } -} \ No newline at end of file diff --git a/mediaEditor/src/main/res/drawable/check_circle_24.xml b/mediaEditor/src/main/res/drawable/check_circle_24.xml deleted file mode 100644 index 86bf1fbe..00000000 --- a/mediaEditor/src/main/res/drawable/check_circle_24.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/mediaEditor/src/main/res/drawable/double_circle.xml b/mediaEditor/src/main/res/drawable/double_circle.xml deleted file mode 100644 index 907d1235..00000000 --- a/mediaEditor/src/main/res/drawable/double_circle.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml b/mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml deleted file mode 100644 index 6875a20b..00000000 --- a/mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/ic_save_24dp.xml b/mediaEditor/src/main/res/drawable/ic_save_24dp.xml deleted file mode 100644 index 8cf3d5a7..00000000 --- a/mediaEditor/src/main/res/drawable/ic_save_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/restore_24dp.xml b/mediaEditor/src/main/res/drawable/restore_24dp.xml deleted file mode 100644 index 0abe38ba..00000000 --- a/mediaEditor/src/main/res/drawable/restore_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/selector_mute.xml b/mediaEditor/src/main/res/drawable/selector_mute.xml deleted file mode 100644 index 7103cc0c..00000000 --- a/mediaEditor/src/main/res/drawable/selector_mute.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/drawable/speed.xml b/mediaEditor/src/main/res/drawable/speed.xml deleted file mode 100644 index 1add8a09..00000000 --- a/mediaEditor/src/main/res/drawable/speed.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/thumb_left.xml b/mediaEditor/src/main/res/drawable/thumb_left.xml deleted file mode 100644 index d6c10419..00000000 --- a/mediaEditor/src/main/res/drawable/thumb_left.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/mediaEditor/src/main/res/drawable/thumb_right.xml b/mediaEditor/src/main/res/drawable/thumb_right.xml deleted file mode 100644 index 6b5f6222..00000000 --- a/mediaEditor/src/main/res/drawable/thumb_right.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/mediaEditor/src/main/res/drawable/video_stable.xml b/mediaEditor/src/main/res/drawable/video_stable.xml deleted file mode 100644 index 7456637a..00000000 --- a/mediaEditor/src/main/res/drawable/video_stable.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/volume_off.xml b/mediaEditor/src/main/res/drawable/volume_off.xml deleted file mode 100644 index b71ede34..00000000 --- a/mediaEditor/src/main/res/drawable/volume_off.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/drawable/volume_up.xml b/mediaEditor/src/main/res/drawable/volume_up.xml deleted file mode 100644 index 836cad86..00000000 --- a/mediaEditor/src/main/res/drawable/volume_up.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/mediaEditor/src/main/res/layout/activity_photo_edit.xml b/mediaEditor/src/main/res/layout/activity_photo_edit.xml deleted file mode 100644 index a2e75050..00000000 --- a/mediaEditor/src/main/res/layout/activity_photo_edit.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/activity_video_edit.xml b/mediaEditor/src/main/res/layout/activity_video_edit.xml deleted file mode 100644 index 5398b6b5..00000000 --- a/mediaEditor/src/main/res/layout/activity_video_edit.xml +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/crop_image_activity.xml b/mediaEditor/src/main/res/layout/crop_image_activity.xml deleted file mode 100644 index d78bde52..00000000 --- a/mediaEditor/src/main/res/layout/crop_image_activity.xml +++ /dev/null @@ -1,6 +0,0 @@ - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/crop_image_view.xml b/mediaEditor/src/main/res/layout/crop_image_view.xml deleted file mode 100644 index 080a4aa0..00000000 --- a/mediaEditor/src/main/res/layout/crop_image_view.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/fragment_edit_image.xml b/mediaEditor/src/main/res/layout/fragment_edit_image.xml deleted file mode 100644 index 8875d589..00000000 --- a/mediaEditor/src/main/res/layout/fragment_edit_image.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/fragment_filter_list.xml b/mediaEditor/src/main/res/layout/fragment_filter_list.xml deleted file mode 100644 index 729b401e..00000000 --- a/mediaEditor/src/main/res/layout/fragment_filter_list.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/layout/thumbnail_list_item.xml b/mediaEditor/src/main/res/layout/thumbnail_list_item.xml deleted file mode 100644 index 98f5cf9d..00000000 --- a/mediaEditor/src/main/res/layout/thumbnail_list_item.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/menu/edit_menu.xml b/mediaEditor/src/main/res/menu/edit_menu.xml deleted file mode 100644 index 4730db3b..00000000 --- a/mediaEditor/src/main/res/menu/edit_menu.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/values-night/themes.xml b/mediaEditor/src/main/res/values-night/themes.xml deleted file mode 100644 index 11414c70..00000000 --- a/mediaEditor/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/colors.xml b/mediaEditor/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127d..00000000 --- a/mediaEditor/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/public.xml b/mediaEditor/src/main/res/values/public.xml deleted file mode 100644 index 7eb22d3b..00000000 --- a/mediaEditor/src/main/res/values/public.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/strings.xml b/mediaEditor/src/main/res/values/strings.xml deleted file mode 100644 index 9a2d6d5a..00000000 --- a/mediaEditor/src/main/res/values/strings.xml +++ /dev/null @@ -1,36 +0,0 @@ - - Media Editor - - Brightness - Contrast - Saturation - Filters - Edit - Thumbnail of filter - Normal - Still processing image, wait for that to finish first! - OK, wait for that. - "Couldn't retrieve image after crop" - Preview of the image being edited - Button to crop or rotate the image - Save your edits? - No, cancel edit - Error while editing - Edit - Stabilize video - Change intensity of stabilization - Unable to save image - Image successfully saved - Mute video - Save crop - Crop video - Select what to keep of the video - Change video speed - Crop saved - Stabilization saved - Reel showing thumbnails of the video you are editing - RESET - SAVE - Permission denied - - \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/themes.xml b/mediaEditor/src/main/res/values/themes.xml deleted file mode 100644 index f394411b..00000000 --- a/mediaEditor/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt b/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt deleted file mode 100644 index 5842564e..00000000 --- a/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.pixeldroid.media_editor - -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 diff --git a/settings.gradle b/settings.gradle index b5f573c9..706f76d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,4 @@ rootProject.name='PixelDroid' include ':app' include ':scrambler' -project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/') -include ':mediaEditor' \ No newline at end of file +project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/') \ No newline at end of file