Crop images (#163)

* Beginning of edit photos activity

* First batch for edition of photos

* EditActivity working properly except flow & save

* Added tests

* Changed name of tabLayouts back to tabs

* Resolved 2 errors from last build

* Truly resolved the 2 issues with requireContext/Activity

* Made test work with API23 emulator

* added 2 tests

* Corrected test @Before to have the right button to click on

* Added flow to newPost and few tests

* Added a test and refactor PhotoEditActivity

* Added flow from upload picture, tests doesn't work

* Added CropImageActivity from ucrop library, crashes for now

* Modified test FiltersIsSwipeableAndClickeable but still doesn't work

* Merge with master

* rectified test SaveButtonLaunchNewPostActivity

* FiltersIsSwipeableAndClickeable test completed

* Ready to merge to master

* resolved error in merge

* Added button save and upload, removed BitmapUtils

* Removed unnecessary libraries and imports

* Remove dependency on library for permissions

* Added crop, rescale of big images to avoid lag, bug fixes

* Remove unnessecary imports

Co-authored-by: Joachim Dunant <joachim.dunant@epfl.ch>
Co-authored-by: Matthieu De Beule <61561059+Wv5twkFEKh54vo4tta9yu7dHa3@users.noreply.github.com>
This commit is contained in:
Sanimys 2020-05-15 11:46:12 +02:00 committed by GitHub
parent 8fb5074f84
commit 5ac3967400
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 253 additions and 107 deletions

View File

@ -83,6 +83,7 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.github.yalantis:ucrop:2.2.5-native'
implementation("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"

View File

@ -108,10 +108,11 @@ class EditPhotoTest {
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(5, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@Test
fun BirghtnessSaturationContrastTest() {
fun BrightnessSaturationContrastTest() {
Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1))
Thread.sleep(1000)
@ -143,7 +144,6 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.action_save)).perform(click())
Espresso.onView(withId(com.google.android.material.R.id.snackbar_text))
.check(matches(withText("Image succesfully saved")))
}
@Test
@ -153,4 +153,11 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.post_creation_picture_frame)).check(matches(isDisplayed()))
}
@Test
fun croppingIsPossible() {
Espresso.onView(withId(R.id.cropImageButton)).perform(click())
Thread.sleep(1000)
Espresso.onView(withId(R.id.menu_crop)).perform(click())
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
}

View File

@ -1,16 +1,28 @@
package com.h.pixeldroid
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.BitmapFactory
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.Menu
import android.view.MenuItem
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 com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.EditPhotoViewPagerAdapter
@ -19,12 +31,14 @@ import com.h.pixeldroid.fragments.FilterListFragment
import com.h.pixeldroid.interfaces.EditImageFragmentListener
import com.h.pixeldroid.interfaces.FilterListFragmentListener
import com.h.pixeldroid.utils.NonSwipeableViewPager
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 kotlinx.android.synthetic.main.activity_photo_edit.*
import kotlinx.android.synthetic.main.content_photo_edit.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
@ -40,12 +54,19 @@ private val REQUIRED_PERMISSIONS = arrayOf(android.Manifest.permission.READ_EXTE
class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditImageFragmentListener {
val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
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 lateinit var finalImage: Bitmap
private var actualFilter: Filter? = null
private lateinit var filterListFragment: FilterListFragment
private lateinit var editImageFragment: EditImageFragment
@ -54,11 +75,12 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
lateinit var viewPager: NonSwipeableViewPager
lateinit var tabLayout: TabLayout
private var brightnessFinal = 0
private var saturationFinal = 1.0f
private var contrastFinal = 1.0f
private var brightnessFinal = BRIGHTNESS_START
private var saturationFinal = SATURATION_START
private var contrastFinal = CONTRAST_START
private var resultUri: Uri? = null
private var imageUri: Uri? = null
private var cropUri: Uri? = null
object URI {var picture_uri: Uri? = null}
@ -70,39 +92,48 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photo_edit)
URI.picture_uri = intent.getParcelableExtra("uri")
resultUri = URI.picture_uri
setSupportActionBar(toolbar)
supportActionBar!!.title = "Edit"
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setHomeButtonEnabled(true)
cropUri = intent.getParcelableExtra("uri")
loadImage()
val file = File.createTempFile("temp_compressed_img", ".png", cacheDir)
file.writeBitmap(compressedImage!!)
URI.picture_uri = Uri.fromFile(file)
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabs)
setupViewPager(viewPager)
tabLayout.setupWithViewPager(viewPager)
outputDirectory = getOutputDirectory()
val cropButton: FloatingActionButton = findViewById(R.id.cropImageButton)
// set on-click listener
cropButton.setOnClickListener {
startCrop()
}
}
/** Use external media if it is available, our app's file directory otherwise */
private fun getOutputDirectory(): File {
val appContext = applicationContext
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
//<editor-fold desc="ON LAUNCH">
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, URI.picture_uri)
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, cropUri)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(compressedImage)
}
filteredImage = originalImage!!.copy(BITMAP_CONFIG, true)
finalImage = originalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(originalImage)
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: NonSwipeableViewPager?) {
@ -142,6 +173,133 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
return super.onOptionsItemSelected(item)
}
//</editor-fold>
//<editor-fold desc="FILTERS">
override fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(filter.processFilter(filteredImage))
compressedImage = filteredImage.copy(BITMAP_CONFIG, true)
actualFilter = filter
}
private fun resetControls() {
editImageFragment.resetControl()
brightnessFinal = BRIGHTNESS_START
saturationFinal = SATURATION_START
contrastFinal = CONTRAST_START
}
//</editor-fold>
//<editor-fold desc="EDITS">
private fun applyFilterAndShowImage(filter: Filter, image: Bitmap?) {
image_preview.setImageBitmap(filter.processFilter(image!!.copy(BITMAP_CONFIG, true)))
}
override fun onBrightnessChange(brightness: Int) {
brightnessFinal = brightness
val myFilter = Filter()
myFilter.addSubFilter(BrightnessSubFilter(brightness))
applyFilterAndShowImage(myFilter, filteredImage)
}
override fun onSaturationChange(saturation: Float) {
saturationFinal = saturation
val myFilter = Filter()
myFilter.addSubFilter(SaturationSubfilter(saturation))
applyFilterAndShowImage(myFilter, filteredImage)
}
override fun onContrastChange(contrast: Float) {
contrastFinal = contrast
val myFilter = Filter()
myFilter.addSubFilter(ContrastSubFilter(contrast))
applyFilterAndShowImage(myFilter, filteredImage)
}
private fun addEditFilters(filter: Filter, br: Int, sa: Float, co: Float): Filter {
filter.addSubFilter(BrightnessSubFilter(br))
filter.addSubFilter(ContrastSubFilter(co))
filter.addSubFilter(SaturationSubfilter(sa))
return filter
}
override fun onEditStarted() {
}
override fun onEditCompleted() {
val bitmap = filteredImage.copy(BITMAP_CONFIG, true)
val myFilter = Filter()
addEditFilters(myFilter, brightnessFinal, saturationFinal, contrastFinal)
compressedImage = myFilter.processFilter(bitmap)
}
//</editor-fold>
//<editor-fold desc="CROPPING">
private fun startCrop() {
applyFinalFilters(MediaStore.Images.Media.getBitmap(contentResolver, cropUri))
val file = File.createTempFile("temp_crop_img", ".png", cacheDir)
file.writeBitmap(finalImage)
val uCrop: UCrop = UCrop.of(Uri.fromFile(file), URI.picture_uri!!)
uCrop.start(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK) {
imageUri = data!!.data
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 = addEditFilters(Filter(), newBr, newSa, newCo)
filteredImage = myFilter.processFilter(filteredImage)
}
private fun handleCropResult(data: Intent?) {
val resultCrop: Uri? = UCrop.getOutput(data!!)
if(resultCrop != null) {
image_preview.setImageURI(resultCrop)
val bitmap = (image_preview.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, "Cannot retrieve image", 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, "Unexpected Error", Toast.LENGTH_SHORT).show()
}
}
//</editor-fold>
//<editor-fold desc="FLOW">
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@ -160,10 +318,17 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
}
private fun applyFinalFilters(image: Bitmap?) {
var editFilter = Filter()
editFilter = addEditFilters(editFilter, brightnessFinal, saturationFinal, contrastFinal)
finalImage = editFilter.processFilter(image!!.copy(BITMAP_CONFIG, true))
if (actualFilter!=null) finalImage = actualFilter!!.processFilter(finalImage)
}
private fun uploadImage(file: File) {
val intent = Intent (applicationContext, PostCreationActivity::class.java)
intent.putExtra("picture_uri", Uri.fromFile(file))
//file.delete()
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext!!.startActivity(intent)
}
@ -189,6 +354,15 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
applicationContext, it) == PackageManager.PERMISSION_GRANTED
}
/** Use external media if it is available, our app's file directory otherwise */
private fun getOutputDirectory(): File {
val appContext = applicationContext
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
@ -197,15 +371,17 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
private fun permissionsGrantedToSave(save: Boolean) {
val file = if(!save){
//put picture in cache
File.createTempFile("temp_img", ".png", cacheDir)
} else{
// Save the picture (quality is ignored for PNG)
File(outputDirectory, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
val file =
if(!save){
//put picture in cache
File.createTempFile("temp_edit_img", ".png", cacheDir)
} else{
// Save the picture (quality is ignored for PNG)
File(outputDirectory, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png")
}
}
try {
applyFinalFilters(originalImage)
file.writeBitmap(finalImage)
} catch (e: IOException) {
Snackbar.make(coordinator_edit, "Unable to save image", Snackbar.LENGTH_LONG).show()
@ -217,54 +393,5 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
Snackbar.make(coordinator_edit, "Image succesfully saved", Snackbar.LENGTH_LONG).show()
}
}
override fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = originalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(filter.processFilter(filteredImage))
finalImage = filteredImage.copy(BITMAP_CONFIG, true)
}
private fun resetControls() {
editImageFragment.resetControl()
brightnessFinal = 0
saturationFinal = 1.0f
contrastFinal = 1.0f
}
override fun onBrightnessChange(brightness: Int) {
brightnessFinal = brightness
val myFilter = Filter()
myFilter.addSubFilter(BrightnessSubFilter(brightness))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onSaturationChange(saturation: Float) {
saturationFinal = saturation
val myFilter = Filter()
myFilter.addSubFilter(SaturationSubfilter(saturation))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onContrastChange(contrast: Float) {
contrastFinal = contrast
val myFilter = Filter()
myFilter.addSubFilter(ContrastSubFilter(contrast))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onEditStarted() {
}
override fun onEditCompleted() {
val bitmap = filteredImage.copy(BITMAP_CONFIG, true)
val myFilter = Filter()
myFilter.addSubFilter(ContrastSubFilter(contrastFinal))
myFilter.addSubFilter(SaturationSubfilter(saturationFinal))
myFilter.addSubFilter(BrightnessSubFilter(brightnessFinal))
finalImage = myFilter.processFilter(bitmap)
}
//</editor-fold>
}

View File

@ -17,9 +17,12 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private lateinit var seekbarSaturation: SeekBar
private lateinit var seekbarContrast: SeekBar
private var BRIGHTNESS_START = 100
private var SATURATION_START = 0
private var CONTRAST_START = 10
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?,
@ -32,13 +35,13 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
seekbarSaturation = view.findViewById(R.id.seekbar_saturation)
seekbarContrast = view.findViewById(R.id.seekbar_contrast)
seekbarBrightness.max = 200
seekbarBrightness.max = BRIGHTNESS_MAX
seekbarBrightness.progress = BRIGHTNESS_START
seekbarContrast.max = 20
seekbarContrast.max = CONTRAST_MAX
seekbarContrast.progress = CONTRAST_START
seekbarSaturation.max = 30
seekbarSaturation.max = SATURATION_MAX
seekbarSaturation.progress = SATURATION_START
seekbarBrightness.setOnSeekBarChangeListener(this)

View File

@ -11,15 +11,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_photo_edit" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<LinearLayout
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"
@ -13,9 +13,17 @@
<ImageView
android:id="@+id/image_preview"
android:scaleType="centerInside"
android:layout_width="match_parent"
android:layout_height="400dp"/>
android:layout_height="0dp"
android:layout_weight=".70"
android:scaleType="centerInside" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/cropImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_crop_black_24dp" />
<com.h.pixeldroid.utils.NonSwipeableViewPager
android:id="@+id/viewPager"
@ -23,7 +31,8 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_below="@+id/image_preview"
android:layout_width="match_parent"
android:layout_height="80dp" />
android:layout_height="0dp"
android:layout_weight=".22" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
@ -31,6 +40,7 @@
app:tabMode="fixed"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="0dp"
android:layout_weight=".08"/>
</RelativeLayout>
</LinearLayout>

View File

@ -12,8 +12,7 @@
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingBottom="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -31,8 +30,8 @@
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -51,8 +50,7 @@
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">