PixelDroid-App-Android/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt

463 lines
16 KiB
Kotlin

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