Use plugin from jitpack!
This commit is contained in:
parent
963dcad8e4
commit
bb543c3217
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -168,6 +168,9 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
|||
<item quantity="one">"%d\nFollowing"</item>
|
||||
<item quantity="other">"%d\nFollowing"</item>
|
||||
</plurals>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="save_image_failed">Unable to save image</string>
|
||||
<string name="save_image_success">Image successfully saved</string>
|
||||
<string name="follow_status_failed">Could not get follow status</string>
|
||||
<string name="edit_link_failed">Failed to open edit page</string>
|
||||
<string name="empty_feed">Nothing to see here :(</string>
|
||||
|
|
|
@ -9555,6 +9555,32 @@
|
|||
<sha256 value="ddd06913f147d70ae68e7a6e4356a55b33f14dde6162dbff2bd0e289581f1ad2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.pixeldroid.pixeldroid" name="android-media-editor" version="1.0">
|
||||
<artifact name="android-media-editor-1.0.aar">
|
||||
<sha256 value="06981e1fabfd22a5ce21df0919822519b7959213a922369b7f1d5cc069b581d9" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="android-media-editor-1.0.module">
|
||||
<sha256 value="c21bcb858b1bc025e584cddcfb61faa9c1a37e167c60b17a8fb2f9070535a4bc" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.pixeldroid.pixeldroid" name="android-media-editor" version="418d10d0cc">
|
||||
<artifact name="android-media-editor-418d10d0cc.aar">
|
||||
<sha256 value="d48e0693c962fc95a84537c77b03435d2e5f456e35fa35dfab5d3af2e37dfbeb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="android-media-editor-418d10d0cc.module">
|
||||
<sha256 value="59d43c37a029385f0b64052003a422123f2da46af8ea4c685c39ae250b967836" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.pixeldroid.pixeldroid" name="android-media-editor" version="a45bd0ea6c">
|
||||
<artifact name="android-media-editor-a45bd0ea6c.pom">
|
||||
<sha256 value="fc03195c6df5c77bfdc75f02d2d2f90db3bcb05f59af8d9612436c262d2f16ee" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.pixeldroid.pixeldroid" name="android-media-editor" version="d5c5c393ed">
|
||||
<artifact name="android-media-editor-d5c5c393ed.pom">
|
||||
<sha256 value="dd9c6f695c6445fb2950444611e550d2a8f85a1f5e2e8c716e849fa1d139c47c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.reactivestreams" name="reactive-streams" version="1.0.3">
|
||||
<artifact name="reactive-streams-1.0.3.pom">
|
||||
<sha256 value="cced467175f4257833f6cb07510ff97b3c75a06e1a58d882a39d79853d51c602" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
</manifest>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<ThumbnailItem>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<out String>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ThumbnailItem>,
|
||||
private val listener: FilterListFragment
|
||||
): RecyclerView.Adapter<ThumbnailAdapter.MyViewHolder>() {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<Float>.convert(number: Float, target: ClosedRange<Float>): 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 }
|
||||
}
|
||||
}
|
|
@ -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<Long> = arrayListOf()
|
||||
private val tempFiles: ArrayList<File> = 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<FrameLayout?>(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<Number> = 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<String?> = if(arguments.videoStart != null) listOf("-ss", "${arguments.videoStart/speed.toFloat()}") else listOf(null, null)
|
||||
|
||||
val endString: List<String?> = 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<String?> = 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<String?> = 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
m: Any?,
|
||||
t: Target<Drawable>?,
|
||||
i: Boolean,
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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.<br></br>
|
||||
* 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.<br></br>
|
||||
* 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].<br></br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.<br></br>
|
||||
* 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
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="?attr/colorOnPrimaryContainer" android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
|
||||
|
||||
<path
|
||||
android:fillColor="?attr/colorPrimaryContainer"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector android:height="10dp"
|
||||
android:viewportHeight="36" android:viewportWidth="36"
|
||||
android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/black" android:pathData=
|
||||
"M 18 18
|
||||
m -9, 0
|
||||
a 9,9 0 1,0 18,0
|
||||
a 9,9 0 1,0 -18,0"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,8C12.48,8,8,12.48,8,18s4.48,10,10,10s10,-4.48,10,-10S23.52,8,18,8zM18,26c-4.42,0,-8,-3.58,-8,-8s3.58,-8,8,-8s8,3.58,8,8s-3.58,8,-8,8z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,18m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorOnSurface" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"
|
||||
android:fillColor="?attr/colorOnSurface"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/volume_up"
|
||||
android:state_selected="false" />
|
||||
<item
|
||||
android:drawable="@drawable/volume_off"
|
||||
android:state_selected="true"/>
|
||||
</selector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z"/>
|
||||
</vector>
|
|
@ -1,25 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17.586dp"
|
||||
android:height="20.915dp"
|
||||
android:viewportWidth="17.586"
|
||||
android:viewportHeight="20.915">
|
||||
<path
|
||||
android:pathData="m5.29,0h8.006v20.915h-8.006a1,1 45,0 1,-1 -1v-18.915a1,1 135,0 1,1 -1z"
|
||||
android:strokeWidth="0.264583"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="m10.259,6.794 l-3.664,3.664 3.664,3.664z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -1,29 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17.586dp"
|
||||
android:height="20.915dp"
|
||||
android:viewportWidth="17.586"
|
||||
android:viewportHeight="20.915">
|
||||
<group
|
||||
android:scaleX="-1"
|
||||
android:translateX="17.586">
|
||||
<path
|
||||
android:pathData="m5.29,0h8.006v20.915h-8.006a1,1 45,0 1,-1 -1v-18.915a1,1 135,0 1,1 -1z"
|
||||
android:strokeWidth="0.264583"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="m10.259,6.794 l-3.664,3.664 3.664,3.664z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM4,18V6h2.95l-2.33,8.73L16.82,18H4zM20,18h-2.95l2.34,-8.73L7.18,6H20V18z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
|
||||
</vector>
|
|
@ -1,89 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinator_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.PhotoEditActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".PhotoEditActivity"
|
||||
tools:showIn="@layout/activity_photo_edit"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight=".70"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/image_preview" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight=".22" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
app:tabGravity="fill"
|
||||
app:tabMode="fixed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight=".08"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBarSaveFile"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/left_guideline"
|
||||
app:layout_constraintGuide_percent=".15"
|
||||
android:orientation="vertical"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/right_guideline"
|
||||
app:layout_constraintGuide_percent=".85"
|
||||
android:orientation="vertical"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/bottom_guideline"
|
||||
app:layout_constraintGuide_percent=".7"
|
||||
android:orientation="horizontal"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/cropImageButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="411dp"
|
||||
android:tint="?attr/colorOnBackground"
|
||||
android:src="@drawable/ic_crop_black_24dp"
|
||||
app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
|
||||
app:layout_constraintRight_toRightOf="@+id/right_guideline"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/bottom_guideline"
|
||||
android:contentDescription="@string/crop_button" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,251 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:scrollbarThumbHorizontal="@drawable/thumb_left">
|
||||
|
||||
|
||||
|
||||
<androidx.media2.widget.VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.pixeldroid.media_editor.photoEdit.cropper.CropImageView
|
||||
android:id="@+id/cropImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/save_crop_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/save_crop"
|
||||
android:contentDescription="@string/save_crop"
|
||||
app:icon="@drawable/ic_crop_black_24dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/muter"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@string/mute_video"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/selector_mute"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cropper"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@string/video_crop"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_crop_black_24dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toEndOf="@+id/muter"/>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@+id/cropper"
|
||||
app:layout_constraintEnd_toEndOf="@+id/cropper"
|
||||
app:layout_constraintBottom_toTopOf="@+id/cropper"
|
||||
android:id="@+id/cropSavedCard"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkMarkCropped"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/check_circle_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/checkMarkCropped"
|
||||
app:layout_constraintTop_toTopOf="@id/checkMarkCropped"
|
||||
app:layout_constraintStart_toEndOf="@id/checkMarkCropped"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/crop_saved" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/speeder"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@string/video_speed"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/speed"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toEndOf="@+id/cropper" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/stabilizer"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@string/stabilize_video"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/video_stable"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toEndOf="@+id/speeder" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@+id/stabilizer"
|
||||
app:layout_constraintEnd_toEndOf="@+id/stabilizer"
|
||||
app:layout_constraintBottom_toTopOf="@+id/stabilizer"
|
||||
android:id="@+id/stabilisation_saved"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkMarkStabilize"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/check_circle_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/checkMarkStabilize"
|
||||
app:layout_constraintTop_toTopOf="@id/checkMarkStabilize"
|
||||
app:layout_constraintStart_toEndOf="@id/checkMarkStabilize"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/stabilization_saved" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/videoRangeSeekBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/select_video_range"
|
||||
android:elevation="5dp"
|
||||
android:layout_marginStart="-15dp"
|
||||
android:layout_marginEnd="-15dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/thumbnail1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail2" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail1"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail3" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail3"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail5" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail4"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail6" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail5"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail7" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail7"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail6"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.pixeldroid.media_editor.photoEdit.cropper.CropImageView
|
||||
android:id="@+id/cropImageView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
|
||||
<org.pixeldroid.media_editor.photoEdit.cropper.CropOverlayView
|
||||
android:id="@+id/CropOverlayView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="2dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageView_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.EditImageFragment">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_brightness"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/lbl_brightness"
|
||||
app:layout_constraintBottom_toTopOf="@+id/label_contrast"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="spread" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_contrast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/lbl_contrast"
|
||||
app:layout_constraintBottom_toTopOf="@+id/label_saturation"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/label_brightness" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_saturation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/lbl_saturation"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/label_contrast" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar_brightness"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/label_brightness"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/label_barrier"
|
||||
app:layout_constraintTop_toTopOf="@+id/label_brightness" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar_saturation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/label_saturation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/seekbar_contrast"
|
||||
app:layout_constraintTop_toTopOf="@+id/label_saturation" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar_contrast"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/label_contrast"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/seekbar_brightness"
|
||||
app:layout_constraintTop_toTopOf="@+id/label_contrast" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/label_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="right"
|
||||
app:constraint_referenced_ids="label_brightness,label_contrast,label_saturation" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.FilterListFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:clipChildren="false"
|
||||
android:padding="4dp"
|
||||
android:scrollbars="none"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</FrameLayout>
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
tools:text="FILTER_NAME" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail"
|
||||
android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/filter_thumbnail" />
|
||||
</LinearLayout>
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/action_reset"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/reset_edit_menu"
|
||||
android:icon="@drawable/restore_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:orderInCategory="101"
|
||||
android:title="@string/save_edit_menu"
|
||||
android:icon="@drawable/ic_save_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
|
@ -1,16 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.PixelDroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<public/>
|
||||
</resources>
|
|
@ -1,36 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">Media Editor</string>
|
||||
<!-- Post editing -->
|
||||
<string name="lbl_brightness">Brightness</string>
|
||||
<string name="lbl_contrast">Contrast</string>
|
||||
<string name="lbl_saturation">Saturation</string>
|
||||
<string name="tab_filters">Filters</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="filter_thumbnail">Thumbnail of filter</string>
|
||||
<string name="normal_filter">Normal</string>
|
||||
<string name="busy_dialog_text">Still processing image, wait for that to finish first!</string>
|
||||
<string name="busy_dialog_ok_button">OK, wait for that.</string>
|
||||
<string name="crop_result_error">"Couldn't retrieve image after crop"</string>
|
||||
<string name="image_preview">Preview of the image being edited</string>
|
||||
<string name="crop_button">Button to crop or rotate the image</string>
|
||||
<string name="save_before_returning">Save your edits?</string>
|
||||
<string name="no_cancel_edit">No, cancel edit</string>
|
||||
<string name="error_editing">Error while editing</string>
|
||||
<string name="toolbar_title_edit">Edit</string>
|
||||
<string name="stabilize_video">Stabilize video</string>
|
||||
<string name="stabilize_video_intensity">Change intensity of stabilization</string>
|
||||
<string name="save_image_failed">Unable to save image</string>
|
||||
<string name="save_image_success">Image successfully saved</string>
|
||||
<string name="mute_video">Mute video</string>
|
||||
<string name="save_crop">Save crop</string>
|
||||
<string name="video_crop">Crop video</string>
|
||||
<string name="select_video_range">Select what to keep of the video</string>
|
||||
<string name="video_speed">Change video speed</string>
|
||||
<string name="crop_saved">Crop saved</string>
|
||||
<string name="stabilization_saved">Stabilization saved</string>
|
||||
<string name="thumbnail_reel_video_edit">Reel showing thumbnails of the video you are editing</string>
|
||||
<string name="reset_edit_menu">RESET</string>
|
||||
<string name="save_edit_menu">SAVE</string>
|
||||
<string name="permission_denied">Permission denied</string>
|
||||
|
||||
</resources>
|
|
@ -1,16 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.PixelDroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -2,4 +2,3 @@ rootProject.name='PixelDroid'
|
|||
include ':app'
|
||||
include ':scrambler'
|
||||
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
|
||||
include ':mediaEditor'
|
Loading…
Reference in New Issue