1
0
mirror of https://gitlab.shinice.net/pixeldroid/PixelDroid synced 2025-01-27 05:54:50 +01:00

Edit photos (#114)

* 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

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-08 10:53:56 +02:00 committed by GitHub
parent b8b3112f1b
commit c4946dd61c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1125 additions and 8 deletions

View File

@ -30,6 +30,8 @@ android {
androidTest.java.srcDirs += 'src/androidTest/java'
}
buildTypes {
debug {
testCoverageEnabled true
@ -70,9 +72,13 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'info.androidhive:imagefilters:1.0.7'
implementation("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"
}

View File

@ -0,0 +1,156 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
import android.widget.SeekBar
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.MockServer
import kotlinx.android.synthetic.main.fragment_edit_image.*
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EditPhotoTest {
private val mockServer = MockServer()
private lateinit var activity: PhotoEditActivity
private lateinit var activityScenario: ActivityScenario<PhotoEditActivity>
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule
var mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
@Before
fun before() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
mockServer.start()
val baseUrl = mockServer.getUrl()
val preferences = context.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply()
// Launch PhotoEditActivity
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PhotoEditActivity::class.java).putExtra("uri", uri)
activityScenario = ActivityScenario.launch<PhotoEditActivity>(intent).onActivity{a -> activity = a}
Thread.sleep(1000)
}
private fun selectTabAtPosition(tabIndex: Int): ViewAction {
return object : ViewAction {
override fun getDescription() = "with tab at index $tabIndex"
override fun getConstraints() =
allOf(isDisplayed(), isAssignableFrom(TabLayout::class.java))
override fun perform(uiController: UiController, view: View) {
val tabLayout = view as TabLayout
val tabAtIndex: TabLayout.Tab = tabLayout.getTabAt(tabIndex)
?: throw PerformException.Builder()
.withCause(Throwable("No tab at index $tabIndex"))
.build()
tabAtIndex.select()
}
}
}
private fun setProgress(progress: Int): ViewAction? {
return object : ViewAction {
override fun getDescription() = "Set the progress on a SeekBar"
override fun getConstraints() = isAssignableFrom(SeekBar::class.java)
override fun perform(uiController: UiController, view: View) {
val seekBar = view as SeekBar
seekBar.progress = progress
}
}
}
@Test
fun PhotoEditActivityHasAnImagePreview() {
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@Test
fun FiltersIsSwipeableAndClickeable() {
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, CustomMatchers.slowSwipeLeft(false)))
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(5, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
}
@Test
fun BirghtnessSaturationContrastTest() {
Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1))
Thread.sleep(1000)
var change = 5
Espresso.onView(withId(R.id.seekbar_brightness)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
Thread.sleep(1000)
change = 15
Espresso.onView(withId(R.id.seekbar_brightness)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
}
@Test
fun SaveButton() {
Espresso.onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
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
fun buttonUploadLaunchNewPostActivity() {
Espresso.onView(withId(R.id.action_upload)).perform(click())
Thread.sleep(1000)
Espresso.onView(withId(R.id.post_creation_picture_frame)).check(matches(isDisplayed()))
}
}

View File

@ -46,6 +46,21 @@ abstract class CustomMatchers {
)
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way left
* 0: swipes half way left
*/
fun slowSwipeLeft(percent: Boolean) : ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.SLOW,
GeneralLocation.CENTER_RIGHT,
if(percent) GeneralLocation.CENTER_LEFT else GeneralLocation.CENTER,
Press.FINGER)
)
}
fun getText(matcher: Matcher<View?>?): String? {
val stringHolder = arrayOf<String?>(null)
Espresso.onView(matcher).perform(object : ViewAction {

View File

@ -4,8 +4,10 @@
package="com.h.pixeldroid">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera.any"
@ -20,6 +22,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".PhotoEditActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity android:name=".PostCreationActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" />
@ -68,6 +75,10 @@
android:scheme="@string/auth_scheme" />
</intent-filter>
</activity>
<activity android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="fullSensor"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".SearchActivity"
android:launchMode="singleTop"

View File

@ -0,0 +1,270 @@
package com.h.pixeldroid
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.EditPhotoViewPagerAdapter
import com.h.pixeldroid.fragments.EditImageFragment
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.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.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
// 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_SAVE_PHOTO = 8
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(), FilterListFragmentListener, EditImageFragmentListener {
val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private var originalImage: Bitmap? = null
private lateinit var filteredImage: Bitmap
private lateinit var finalImage: Bitmap
private lateinit var filterListFragment: FilterListFragment
private lateinit var editImageFragment: EditImageFragment
private lateinit var outputDirectory: File
lateinit var viewPager: NonSwipeableViewPager
lateinit var tabLayout: TabLayout
private var brightnessFinal = 0
private var saturationFinal = 1.0f
private var contrastFinal = 1.0f
private var resultUri: Uri? = null
object URI {var picture_uri: Uri? = null}
init {
System.loadLibrary("NativeImageProcessor")
}
override fun onCreate(savedInstanceState: Bundle?) {
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)
loadImage()
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabs)
setupViewPager(viewPager)
tabLayout.setupWithViewPager(viewPager)
outputDirectory = getOutputDirectory()
}
/** 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 loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, URI.picture_uri)
filteredImage = originalImage!!.copy(BITMAP_CONFIG, true)
finalImage = originalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(originalImage)
}
private fun setupViewPager(viewPager: NonSwipeableViewPager?) {
val adapter = EditPhotoViewPagerAdapter(supportFragmentManager)
filterListFragment = FilterListFragment()
filterListFragment.setListener(this)
editImageFragment = EditImageFragment()
editImageFragment.setListener(this)
adapter.addFragment(filterListFragment, "FILTERS")
adapter.addFragment(editImageFragment, "EDIT")
viewPager!!.adapter = adapter
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.edit_photo_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
android.R.id.home -> {
super.onBackPressed()
}
R.id.action_upload -> {
saveImageToGallery(false)
}
R.id.action_save -> {
saveImageToGallery(true)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if(grantResults.size > 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// permission was granted
when (requestCode) {
REQUEST_CODE_PERMISSIONS_SAVE_PHOTO -> permissionsGrantedToSave(true)
REQUEST_CODE_PERMISSIONS_SEND_PHOTO -> permissionsGrantedToSave(false)
}
} else {
Snackbar.make(coordinator_edit, "Permission denied", Snackbar.LENGTH_LONG).show()
}
}
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)
}
private fun saveImageToGallery(save: Boolean) {
// runtime permission and process
if (!allPermissionsGranted()) {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
if(save) REQUEST_CODE_PERMISSIONS_SAVE_PHOTO else REQUEST_CODE_PERMISSIONS_SEND_PHOTO
)
} else {
permissionsGrantedToSave(save)
}
}
/**
* 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 File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
out.flush()
}
}
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)
.format(System.currentTimeMillis()) + ".png")
}
try {
file.writeBitmap(finalImage)
} catch (e: IOException) {
Snackbar.make(coordinator_edit, "Unable to save image", Snackbar.LENGTH_LONG).show()
}
if (!save) {
uploadImage(file)
} else {
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)
}
}

View File

@ -47,9 +47,7 @@ class PostCreationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_creation)
val imageUri: Uri = intent.getParcelableExtra<Uri>("picture_uri")!!
saveImage(imageUri)
pictureFrame = findViewById<ImageView>(R.id.post_creation_picture_frame)

View File

@ -17,7 +17,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.objects.Relationship

View File

@ -0,0 +1,25 @@
package com.h.pixeldroid.adapters
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
class EditPhotoViewPagerAdapter (manager: FragmentManager):
FragmentPagerAdapter(manager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val fragmentList = ArrayList<Fragment>()
private val fragmentTitleList = ArrayList<String>()
override fun getItem(position: Int) = fragmentList[position]
override fun getCount() = fragmentList.size
fun addFragment(fragment: Fragment, title: String) {
fragmentList.add(fragment)
fragmentTitleList.add(title)
}
override fun getPageTitle(position: Int): CharSequence? {
return fragmentTitleList[position]
}
}

View File

@ -1,4 +1,4 @@
package com.h.pixeldroid.fragments
package com.h.pixeldroid.adapters
import android.content.Intent
import androidx.recyclerview.widget.RecyclerView

View File

@ -0,0 +1,57 @@
package com.h.pixeldroid.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.R
import com.h.pixeldroid.interfaces.FilterListFragmentListener
import com.zomato.photofilters.utils.ThumbnailItem
import kotlinx.android.synthetic.main.thumbnail_list_item.view.*
class ThumbnailAdapter (private val context: Context,
private val tbItemList: List<ThumbnailItem>,
private val listener: FilterListFragmentListener): RecyclerView.Adapter<ThumbnailAdapter.MyViewHolder>() {
private var selectedIndex = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(context).inflate(R.layout.thumbnail_list_item, parent, false)
return MyViewHolder(itemView)
}
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 = position
notifyDataSetChanged()
}
holder.filterName.text = tbItem.filterName
if(selectedIndex == position)
holder.filterName.setTextColor(ContextCompat.getColor(context, R.color.filterLabelSelected))
else
holder.filterName.setTextColor(ContextCompat.getColor(context, R.color.filterLabelNormal))
}
class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var thumbnail: ImageView
var filterName: TextView
init {
thumbnail = itemView.thumbnail
filterName = itemView.filter_name
}
}
}

View File

@ -0,0 +1,89 @@
package com.h.pixeldroid.fragments
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 com.h.pixeldroid.R
import com.h.pixeldroid.interfaces.EditImageFragmentListener
class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private var listener: EditImageFragmentListener? = null
private lateinit var seekbarBrightness: SeekBar
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
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_edit_image, container, false)
seekbarBrightness = view.findViewById(R.id.seekbar_brightness)
seekbarSaturation = view.findViewById(R.id.seekbar_saturation)
seekbarContrast = view.findViewById(R.id.seekbar_contrast)
seekbarBrightness.max = 200
seekbarBrightness.progress = BRIGHTNESS_START
seekbarContrast.max = 20
seekbarContrast.progress = CONTRAST_START
seekbarSaturation.max = 30
seekbarSaturation.progress = SATURATION_START
seekbarBrightness.setOnSeekBarChangeListener(this)
seekbarContrast.setOnSeekBarChangeListener(this)
seekbarSaturation.setOnSeekBarChangeListener(this)
return view
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
var prog = progress
if(listener != null) {
when(seekBar!!.id) {
R.id.seekbar_brightness -> listener!!.onBrightnessChange(progress - 100)
R.id.seekbar_saturation -> {
prog += 10
val tempProgress = .10f * prog
listener!!.onSaturationChange(tempProgress)
}
R.id.seekbar_contrast -> {
val tempProgress = .10f * prog
listener!!.onSaturationChange(tempProgress)
}
}
}
}
fun resetControl() {
seekbarBrightness.progress = BRIGHTNESS_START
seekbarContrast.progress = CONTRAST_START
seekbarSaturation.progress = SATURATION_START
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
if(listener != null)
listener!!.onEditStarted()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if(listener != null)
listener!!.onEditCompleted()
}
fun setListener(listener: EditImageFragmentListener) {
this.listener = listener
}
}

View File

@ -0,0 +1,104 @@
package com.h.pixeldroid.fragments
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.util.TypedValue
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.PhotoEditActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.interfaces.FilterListFragmentListener
import com.h.pixeldroid.utils.SpaceItemDecoration
import com.zomato.photofilters.FilterPack
import com.zomato.photofilters.imageprocessors.Filter
import com.zomato.photofilters.utils.ThumbnailItem
import com.zomato.photofilters.utils.ThumbnailsManager
class FilterListFragment : Fragment(), FilterListFragmentListener {
internal lateinit var recyclerView: RecyclerView
internal var listener : FilterListFragmentListener? = null
internal lateinit var adapter: ThumbnailAdapter
internal lateinit var tbItemList: MutableList<ThumbnailItem>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_filter_list, container, false)
tbItemList = ArrayList()
adapter = ThumbnailAdapter(requireActivity(), tbItemList, this)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
recyclerView.itemAnimator = DefaultItemAnimator()
val space = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt()
recyclerView.addItemDecoration(SpaceItemDecoration(space))
recyclerView.adapter = adapter
displayImage(null)
return view
}
fun displayImage(bitmap: Bitmap?) {
val r = Runnable {
val tbImage: Bitmap?
if (bitmap == null) {
tbImage = MediaStore.Images.Media.getBitmap(requireActivity().contentResolver, PhotoEditActivity.URI.picture_uri)
} else {
tbImage = Bitmap.createScaledBitmap(bitmap, 100, 100, false)
}
if (tbImage == null)
return@Runnable
setupFilter(tbImage)
tbItemList.addAll(ThumbnailsManager.processThumbs(activity))
requireActivity().runOnUiThread{ adapter.notifyDataSetChanged() }
}
Thread(r).start()
}
private fun setupFilter(tbImage: Bitmap?) {
ThumbnailsManager.clearThumbs()
tbItemList.clear()
val tbItem = ThumbnailItem()
tbItem.image = tbImage
tbItem.filterName = "Normal"
ThumbnailsManager.addThumb(tbItem)
val filters = FilterPack.getFilterPack(requireActivity())
for (filter in filters) {
val item = ThumbnailItem()
item.image = tbImage
item.filter = filter
item.filterName = filter.name
ThumbnailsManager.addThumb(item)
}
}
override fun onFilterSelected(filter: Filter) {
if(listener != null ){
listener!!.onFilterSelected(filter)
}
}
fun setListener(listFragmentListener: FilterListFragmentListener) {
this.listener = listFragmentListener
}
}

View File

@ -2,13 +2,14 @@ package com.h.pixeldroid.fragments
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import com.h.pixeldroid.PostCreationActivity
import com.h.pixeldroid.PhotoEditActivity
import com.h.pixeldroid.R
/**
@ -34,14 +35,21 @@ class NewPostFragment : Fragment() {
uploadPicture()
}
val takePictureButton: Button = view.findViewById(R.id.takePictureButton)
takePictureButton.setOnClickListener{
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PhotoEditActivity::class.java).putExtra("uri", uri)
startActivity(intent)
}
return view
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && data != null
&& requestCode == PICK_IMAGE_REQUEST && data.data != null)
startActivity(Intent(activity, PostCreationActivity::class.java)
.putExtra("picture_uri", data.data)
startActivity(Intent(activity, PhotoEditActivity::class.java)
.putExtra("uri", data.data)
)
}
@ -56,4 +64,4 @@ class NewPostFragment : Fragment() {
)
}
}
}
}

View File

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.api.PixelfedAPI

View File

@ -0,0 +1,13 @@
package com.h.pixeldroid.interfaces
interface EditImageFragmentListener {
fun onBrightnessChange(brightness: Int)
fun onSaturationChange(saturation: Float)
fun onContrastChange(contrast: Float)
fun onEditStarted()
fun onEditCompleted()
}

View File

@ -0,0 +1,7 @@
package com.h.pixeldroid.interfaces
import com.zomato.photofilters.imageprocessors.Filter
interface FilterListFragmentListener {
fun onFilterSelected(filter: Filter)
}

View File

@ -0,0 +1,43 @@
package com.h.pixeldroid.utils
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.viewpager.widget.ViewPager
class NonSwipeableViewPager: ViewPager {
constructor(context: Context):super(context) {
setMyScroller()
}
constructor(context: Context,attributeSet: AttributeSet): super(context, attributeSet) {
setMyScroller()
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return false
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return false
}
private fun setMyScroller() {
try {
val viewPager = ViewPager::class.java
val scroller = viewPager.getDeclaredField("mScroller")
scroller.isAccessible = true
scroller.set(this, FilterScroller(context))
} catch (e:Exception) {
e.printStackTrace()
}
}
}
class FilterScroller(context: Context): Scroller(context, DecelerateInterpolator()) {
override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
super.startScroll(startX, startY, dx, dy, 400)
}
}

View File

@ -0,0 +1,22 @@
package com.h.pixeldroid.utils
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class SpaceItemDecoration(private val space: Int): RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (parent.getChildAdapterPosition(view) == state.itemCount - 1) {
outRect.left = space
outRect.right = 0
} else {
outRect.left = 0
outRect.right = space
}
}
}

View File

@ -0,0 +1,9 @@
<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="#FF000000"
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z"/>
</vector>

View File

@ -0,0 +1,9 @@
<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="#FF000000"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>

View File

@ -0,0 +1,9 @@
<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="#FF000000"
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

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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=".PhotoEditActivity">
<com.google.android.material.appbar.AppBarLayout
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"
android:background="@color/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_photo_edit" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:background="@android:color/white"
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"
>
<ImageView
android:id="@+id/image_preview"
android:scaleType="centerInside"
android:layout_width="match_parent"
android:layout_height="400dp"/>
<com.h.pixeldroid.utils.NonSwipeableViewPager
android:id="@+id/viewPager"
android:layout_above="@+id/tabs"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_below="@+id/image_preview"
android:layout_width="match_parent"
android:layout_height="80dp" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
app:tabGravity="fill"
app:tabMode="fixed"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>

View File

@ -0,0 +1,39 @@
<?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:id="@+id/camera_fragment_main_linear_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
tools:context=".fragments.CameraFragment"
android:orientation="vertical">
<Button
android:id="@+id/upload_picture_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="15dp"
android:layout_margin="15dp"
android:text="@string/upload_a_picture"
android:background="@color/colorPrimary"
android:textColor="@color/cardview_light_background"/>
<Button
android:id="@+id/edit_picture_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="15dp"
android:layout_margin="15dp"
android:text="Edit a picture"
android:background="@color/colorPrimary"
android:textColor="@color/cardview_light_background"/>
<ImageView
android:id="@+id/uploaded_picture_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
tools:srcCompat="@tools:sample/avatars" />
</LinearLayout>

View File

@ -0,0 +1,72 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:context=".fragments.EditImageFragment">
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/lbl_brightness"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<SeekBar
android:id="@+id/seekbar_brightness"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/lbl_contrast"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<SeekBar
android:id="@+id/seekbar_contrast"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/lbl_saturation"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<SeekBar
android:id="@+id/seekbar_saturation"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,17 @@
<?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=".fragments.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

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/filter_name"
android:text="FILTER_NAME"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/thumbnail"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_width="80dp"
android:layout_height="80dp"
android:contentDescription="thumbnail of filter" />
</LinearLayout>

View File

@ -0,0 +1,20 @@
<?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_upload"
android:orderInCategory="100"
android:title="CREATE POST"
android:icon="@drawable/ic_file_upload_black_24dp"
app:showAsAction="always"/>
<item
android:id="@+id/action_save"
android:orderInCategory="101"
android:title="SAVE"
android:icon="@drawable/ic_save_black_24dp"
app:showAsAction="always"/>
</menu>

View File

@ -3,4 +3,8 @@
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="filterLabelNormal">#8A8889</color>
<color name="filterLabelSelected">#221F20</color>
<color name="colorOptionMenu">#FF3990</color>
</resources>

View File

@ -27,6 +27,7 @@
<string name="mention_notification">%1$s mentioned you</string>
<string name="shared_notification">%1$s shared your post</string>
<string name="liked_notification">%1$s liked your post</string>
<string name="create_a_new_post">Create a new post!</string>
<string name="take_a_picture">Take a picture</string>
<string name="upload_a_picture">Upload a picture</string>
@ -35,9 +36,17 @@
<string name="send">send</string>
<string name="whats_an_instance">"What's an instance?"</string>
<string name="logout">Logout</string>
<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="tab_edit">EDIT</string>
<string name="save_to_gallery">Save to Gallery…</string>
<string name="image_download_failed">Download has been failed, please try again</string>
<string name="image_download_downloading">Downloading…</string>
<string name="image_download_success">Image downloaded successfully</string>
<string name="share_picture">Share picture…</string>
</resources>

View File

@ -15,4 +15,21 @@
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.NoActionBar.FullScreen">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="posts_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>