Use plugin from jitpack!

This commit is contained in:
Matthieu 2022-10-29 16:38:36 +02:00
parent 963dcad8e4
commit bb543c3217
48 changed files with 38 additions and 3585 deletions

View File

@ -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"

View File

@ -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

View File

@ -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 ->

View File

@ -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()

View File

@ -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>

View File

@ -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"/>

View File

@ -1 +0,0 @@
/build

View File

@ -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"
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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 }
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public/>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}

View File

@ -2,4 +2,3 @@ rootProject.name='PixelDroid'
include ':app'
include ':scrambler'
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
include ':mediaEditor'