diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 87ec31ecf..48e2411c6 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -23,6 +23,7 @@
#99000000
#4D000000
@color/black_transp_light
+ @color/black
@color/black_transp_light
@color/black_transp_light_darker
#4D000000
diff --git a/build.gradle b/build.gradle
index 33629e1b3..92c92e372 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,6 +7,7 @@ buildscript {
google()
jcenter()
maven { url "https://dl.bintray.com/automattic/maven/" }
+ maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
@@ -30,6 +31,7 @@ allprojects {
repositories {
google()
jcenter()
+ maven { url "https://jitpack.io" }
}
if (tasks.findByPath('checkstyle') == null) {
diff --git a/photoeditor/build.gradle b/photoeditor/build.gradle
index f9f107303..95cc93b7c 100644
--- a/photoeditor/build.gradle
+++ b/photoeditor/build.gradle
@@ -54,6 +54,10 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.10.0'
kapt 'com.github.bumptech.glide:compiler:4.10.0'
+ implementation 'jp.wasabeef:glide-transformations:4.3.0'
+
+ implementation 'com.github.chrisbanes:PhotoView:2.3.0'
+
implementation project(path: ':mp4compose')
lintChecks 'org.wordpress:lint:1.0.1'
diff --git a/photoeditor/src/main/java/com/automattic/photoeditor/views/PhotoEditorView.kt b/photoeditor/src/main/java/com/automattic/photoeditor/views/PhotoEditorView.kt
index 637168fa6..f15e77bec 100644
--- a/photoeditor/src/main/java/com/automattic/photoeditor/views/PhotoEditorView.kt
+++ b/photoeditor/src/main/java/com/automattic/photoeditor/views/PhotoEditorView.kt
@@ -13,9 +13,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ImageView.ScaleType.CENTER_CROP
+import android.widget.ImageView.ScaleType.FIT_CENTER
import android.widget.ProgressBar
import android.widget.RelativeLayout
import androidx.annotation.RequiresApi
+import androidx.appcompat.widget.AppCompatImageView
import com.automattic.photoeditor.OnSaveBitmap
import com.automattic.photoeditor.R.styleable
import com.automattic.photoeditor.views.background.fixed.BackgroundImageView
@@ -24,6 +26,9 @@ import com.automattic.photoeditor.views.brush.BrushDrawingView
import com.automattic.photoeditor.views.filter.CustomEffect
import com.automattic.photoeditor.views.filter.ImageFilterView
import com.automattic.photoeditor.views.filter.PhotoFilter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import jp.wasabeef.glide.transformations.BlurTransformation
/**
*
@@ -40,9 +45,11 @@ import com.automattic.photoeditor.views.filter.PhotoFilter
class PhotoEditorView : RelativeLayout {
private lateinit var autoFitTextureView: AutoFitTextureView
private lateinit var backgroundImage: BackgroundImageView
+ private lateinit var backgroundImageBlurred: AppCompatImageView
private lateinit var brushDrawingView: BrushDrawingView
private lateinit var imageFilterView: ImageFilterView
private lateinit var progressBar: ProgressBar
+ private var attachedToWindow: Boolean = false
private var surfaceListeners: ArrayList = ArrayList()
@@ -72,6 +79,9 @@ class PhotoEditorView : RelativeLayout {
val source: ImageView
get() = backgroundImage
+ val sourceBlurredBkg: ImageView
+ get() = backgroundImageBlurred
+
val brush: BrushDrawingView
get() = brushDrawingView
@@ -103,13 +113,33 @@ class PhotoEditorView : RelativeLayout {
init(attrs)
}
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ attachedToWindow = false
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ attachedToWindow = true
+ }
+
+ fun onComposerDestroyed() {
+ attachedToWindow = false
+ }
+
@SuppressLint("Recycle")
private fun init(attrs: AttributeSet?) {
+ backgroundImageBlurred = AppCompatImageView(context).apply {
+ id = imgBlurSrcId
+ adjustViewBounds = true
+ scaleType = CENTER_CROP
+ }
+
// Setup image attributes
backgroundImage = BackgroundImageView(context).apply {
id = imgSrcId
adjustViewBounds = true
- scaleType = CENTER_CROP
+ scaleType = FIT_CENTER
}
val imgSrcParam = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT).apply {
@@ -159,6 +189,11 @@ class PhotoEditorView : RelativeLayout {
backgroundImage.setOnImageChangedListener(object : BackgroundImageView.OnImageChangedListener {
override fun onBitmapLoaded(sourceBitmap: Bitmap?) {
+ if (attachedToWindow) {
+ Glide.with(context).load(sourceBitmap)
+ .apply(RequestOptions.bitmapTransform(BlurTransformation(25, 3)))
+ .into(backgroundImageBlurred)
+ }
imageFilterView.setFilterEffect(PhotoFilter.NONE)
imageFilterView.setSourceBitmap(sourceBitmap)
Log.d(TAG, "onBitmapLoaded() called with: sourceBitmap = [$sourceBitmap]")
@@ -179,6 +214,9 @@ class PhotoEditorView : RelativeLayout {
// Add camera preview
addView(autoFitTextureView, cameraParam)
+ // Add image source
+ addView(backgroundImageBlurred, imgSrcParam)
+
// Add image source
addView(backgroundImage, imgSrcParam)
@@ -254,11 +292,13 @@ class PhotoEditorView : RelativeLayout {
internal fun turnTextureViewOn() {
backgroundImage.visibility = View.INVISIBLE
+ backgroundImageBlurred.visibility = View.INVISIBLE
autoFitTextureView.visibility = View.VISIBLE
}
internal fun turnTextureViewOff() {
backgroundImage.visibility = View.VISIBLE
+ backgroundImageBlurred.visibility = View.VISIBLE
autoFitTextureView.visibility = View.INVISIBLE
}
@@ -266,11 +306,13 @@ class PhotoEditorView : RelativeLayout {
backgroundImage.visibility = autoFitTextureView.visibility.also {
autoFitTextureView.visibility = backgroundImage.visibility
}
+ backgroundImageBlurred.visibility = backgroundImage.visibility
return autoFitTextureView.visibility == View.VISIBLE
}
internal fun turnTextureAndImageViewOff() {
backgroundImage.visibility = View.INVISIBLE
+ backgroundImageBlurred.visibility = View.INVISIBLE
autoFitTextureView.visibility = View.INVISIBLE
}
@@ -286,6 +328,7 @@ class PhotoEditorView : RelativeLayout {
companion object {
private val TAG = "PhotoEditorView"
+ private val imgBlurSrcId = 5
private val imgSrcId = 1
private val brushSrcId = 2
private val glFilterId = 3
diff --git a/photoeditor/src/main/java/com/automattic/photoeditor/views/background/fixed/BackgroundImageView.kt b/photoeditor/src/main/java/com/automattic/photoeditor/views/background/fixed/BackgroundImageView.kt
index ada546983..b60d64d34 100644
--- a/photoeditor/src/main/java/com/automattic/photoeditor/views/background/fixed/BackgroundImageView.kt
+++ b/photoeditor/src/main/java/com/automattic/photoeditor/views/background/fixed/BackgroundImageView.kt
@@ -10,14 +10,14 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.net.Uri
import android.util.AttributeSet
-import androidx.appcompat.widget.AppCompatImageView
+import com.github.chrisbanes.photoview.PhotoView
/**
* @author [Burhanuddin Rashid](https://github.com/burhanrashid52)
* @version 0.1.2
* @since 5/21/2018
*/
-internal class BackgroundImageView : AppCompatImageView {
+class BackgroundImageView : PhotoView {
private var mOnImageChangedListener: OnImageChangedListener? = null
val bitmap: Bitmap?
diff --git a/stories/build.gradle b/stories/build.gradle
index f8261b93c..1c02f8175 100644
--- a/stories/build.gradle
+++ b/stories/build.gradle
@@ -52,6 +52,8 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.10.0'
kapt 'com.github.bumptech.glide:compiler:4.10.0'
+ implementation 'jp.wasabeef:glide-transformations:4.3.0'
+
implementation 'org.greenrobot:eventbus:3.1.1'
implementation project(path: ':photoeditor')
@@ -63,6 +65,8 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
+ implementation 'com.github.chrisbanes:PhotoView:2.3.0'
+
lintChecks 'org.wordpress:lint:1.0.1'
testImplementation 'junit:junit:4.12'
diff --git a/stories/src/main/java/com/wordpress/stories/compose/ComposeLoopFrameActivity.kt b/stories/src/main/java/com/wordpress/stories/compose/ComposeLoopFrameActivity.kt
index 627257e83..e55e4b134 100644
--- a/stories/src/main/java/com/wordpress/stories/compose/ComposeLoopFrameActivity.kt
+++ b/stories/src/main/java/com/wordpress/stories/compose/ComposeLoopFrameActivity.kt
@@ -9,8 +9,10 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
+import android.graphics.drawable.Drawable
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
+import android.graphics.Matrix
import android.hardware.Camera
import android.media.MediaScannerConnection
import android.net.Uri
@@ -23,12 +25,16 @@ import android.os.Vibrator
import android.provider.Settings
import android.text.TextUtils
import android.util.Log
+import android.util.Size
import android.view.GestureDetector
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
import android.webkit.MimeTypeMap
+import android.widget.ImageView.ScaleType.CENTER_CROP
+import android.widget.ImageView.ScaleType.FIT_CENTER
+import android.widget.ImageView.ScaleType.FIT_START
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.Group
@@ -63,8 +69,15 @@ import com.automattic.photoeditor.views.ViewType
import com.automattic.photoeditor.views.ViewType.TEXT
import com.automattic.photoeditor.views.added.AddedViewList
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.Target
+import com.github.chrisbanes.photoview.PhotoView
import com.wordpress.stories.BuildConfig
import com.wordpress.stories.R
import com.wordpress.stories.compose.ComposeLoopFrameActivity.ExternalMediaPickerRequestCodesAndExtraKeys
@@ -90,6 +103,7 @@ import com.wordpress.stories.compose.story.StoryFrameItem
import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundSource
import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundSource.FileBackgroundSource
import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundSource.UriBackgroundSource
+import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundViewInfo
import com.wordpress.stories.compose.story.StoryFrameItemType
import com.wordpress.stories.compose.story.StoryFrameItemType.IMAGE
import com.wordpress.stories.compose.story.StoryFrameItemType.VIDEO
@@ -105,14 +119,21 @@ import com.wordpress.stories.compose.text.TextStyleGroupManager
import com.wordpress.stories.util.KEY_STORY_EDIT_MODE
import com.wordpress.stories.util.KEY_STORY_SAVE_RESULT
import com.wordpress.stories.util.STATE_KEY_CURRENT_STORY_INDEX
+import com.wordpress.stories.util.TARGET_RATIO_9_16
+import com.wordpress.stories.util.calculateAspectRatioForDrawable
import com.wordpress.stories.util.getDisplayPixelSize
+import com.wordpress.stories.util.getSizeRatio
import com.wordpress.stories.util.getStoryIndexFromIntentOrBundle
+import com.wordpress.stories.util.isAspectRatioSimilarByPercentage
+import com.wordpress.stories.util.isScreenTallerThan916
import com.wordpress.stories.util.isVideo
+import com.wordpress.stories.util.normalizeSizeExportTo916
import kotlinx.android.synthetic.main.activity_composer.*
import kotlinx.android.synthetic.main.content_composer.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -190,6 +211,7 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
private lateinit var backgroundSurfaceManager: BackgroundSurfaceManager
private var currentOriginalCapturedFile: File? = null
private lateinit var workingAreaRect: Rect
+ private var bottomOpaqueBarHeight: Int = 0 // default: no opaque bottom bar
private val timesUpRunnable = Runnable {
stopRecordingVideo(false) // time's up, it's not a cancellation
@@ -235,6 +257,10 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
private var genericAnnouncementDialogProvider: GenericAnnouncementDialogProvider? = null
private var showGenericAnnouncementDialogWhenReady = false
private var useTempCaptureFile = true
+ private var screenWidth: Int = 1080 // default
+ private var screenHeight: Int = 1920 // default
+ private var screenSizeRatio: Float = TARGET_RATIO_9_16
+ private lateinit var normalizedSize: Size
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
@@ -329,6 +355,8 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
bottomNavigationBarMargin = insets.systemWindowInsetBottom
workingAreaRect = calculateWorkingArea()
photoEditor.updateWorkAreaRect(workingAreaRect)
+ bottomOpaqueBarHeight = preCalculateOpaqueBarHeight()
+ screenSizeRatio = getSizeRatio(screenWidth, screenHeight)
delete_view.addBottomOffset(bottomNavigationBarMargin)
delete_slide_view.addBottomOffset(bottomNavigationBarMargin)
(bottom_strip_view as StoryFrameSelectorFragment).setBottomOffset(bottomNavigationBarMargin)
@@ -540,13 +568,30 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
}
}
+ private fun preCalculateOpaqueBarHeight(): Int {
+ // We used to obtain the screen dimensions by querying resources.displayMetrics but this value is the
+ // actual screen size minus the navigation bar height i.e. on a Pixel device we'd get 1794 instead of 1920.
+ // Given ComposeLoopFrameActivity is full screen, we can rely on the measuredHeight calculation instead.
+ screenWidth = photoEditorView.source.measuredWidth
+ screenHeight = photoEditorView.source.measuredHeight
+ normalizedSize = normalizeSizeExportTo916(screenWidth, screenHeight).toSize()
+ if (isScreenTallerThan916(screenWidth, screenHeight)) {
+ return (screenHeight - normalizedSize.height)
+ } else {
+ return 0
+ }
+ }
+
+ private fun setOpaqueBarHeight() {
+ if (bottomOpaqueBarHeight > 0) {
+ bottom_opaque_bar.layoutParams.height = bottomOpaqueBarHeight
+ } else {
+ bottom_opaque_bar.visibility = View.GONE
+ }
+ }
+
override fun onStart() {
super.onStart()
- val selectedFrameIndex = storyViewModel.getSelectedFrameIndex()
- if (!launchCameraRequestPending && !launchVideoPlayerRequestPending &&
- selectedFrameIndex < storyViewModel.getCurrentStorySize()) {
- updateBackgroundSurfaceUIWithStoryFrame(selectedFrameIndex)
- }
// upon loading an existing Story, show the generic announcement dialog if present
if (showGenericAnnouncementDialogWhenReady) {
showGenericAnnouncementDialogWhenReady = false
@@ -717,6 +762,7 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
}
override fun onDestroy() {
+ photoEditorView.onComposerDestroyed()
doUnbindService()
EventBus.getDefault().unregister(this)
super.onDestroy()
@@ -1108,6 +1154,7 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
private fun saveStoryPreHook() {
showLoading()
+ refreshBackgroundViewInfoOnSelectedFrame()
// disable layout change animations, we need this to make added views immediately visible, otherwise
// we may end up capturing a Bitmap of a backing drawable that still has not been updated
// (i.e. no visible added Views)
@@ -1116,6 +1163,15 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
preHookRun = true
}
+ private fun refreshBackgroundViewInfoOnSelectedFrame() {
+ storyViewModel.getSelectedFrame()?.let {
+ setBackgroundViewInfoOnFrame(
+ it,
+ photoEditor.composedCanvas.source as PhotoView
+ )
+ }
+ }
+
private fun saveStoryPostHook(result: StorySaveResult) {
doUnbindService()
// re-enable layout change animations
@@ -1575,9 +1631,12 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
private fun hideStoryFrameSelector() {
(bottom_strip_view as StoryFrameSelectorFragment).hide()
+ bottom_opaque_bar.visibility = View.INVISIBLE
}
private fun showStoryFrameSelector() {
+ setOpaqueBarHeight()
+ bottom_opaque_bar.visibility = View.VISIBLE
(bottom_strip_view as StoryFrameSelectorFragment).show()
}
@@ -1871,6 +1930,15 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
if (oldIndex >= 0) {
// only remember added views for frame if current index is valid
addCurrentViewsToFrameAtIndex(oldIndex)
+
+ // save current imageMatrix as the background image may have been resized
+ val oldSelectedFrame = storyViewModel.getCurrentStoryFrameAt(oldIndex)
+ if (oldSelectedFrame?.frameItemType is IMAGE) {
+ setBackgroundViewInfoOnFrame(
+ oldSelectedFrame,
+ photoEditor.composedCanvas.source as PhotoView
+ )
+ } // TODO add else clause and handle VIDEO frameItemType
}
// This is tricky. See https://stackoverflow.com/questions/45860434/cant-remove-view-from-root-view
@@ -1899,11 +1967,7 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
if (newSelectedFrame.frameItemType is VIDEO) {
showPlayVideoWithSurfaceSafeguard(source)
} else {
- val model = (source as? FileBackgroundSource)?.file ?: (source as UriBackgroundSource).contentUri
- Glide.with(this@ComposeLoopFrameActivity)
- .load(model)
- .transform(CenterCrop())
- .into(photoEditorView.source)
+ loadImageWithGlideToPrepare(newSelectedFrame)
showStaticBackground()
}
@@ -1922,8 +1986,154 @@ abstract class ComposeLoopFrameActivity : AppCompatActivity(), OnStoryFrameSelec
showRetryButtonAndHideEditControlsForErroredFrame(newSelectedFrame.saveResultReason !is SaveSuccess)
}
+ private fun setBackgroundViewInfoOnFrame(frame: StoryFrameItem, backgroundImageSource: PhotoView) {
+ val matrixValues = FloatArray(9)
+ val matrix = Matrix()
+ // fill in matrix with PhotoView Support matrix
+ backgroundImageSource.getSuppMatrix(matrix)
+ // extract matrix to float array matrixValues
+ matrix.getValues(matrixValues)
+ frame.source.backgroundViewInfo = BackgroundViewInfo(
+ imageMatrixValues = matrixValues,
+ scaleType = backgroundImageSource.scaleType
+ )
+ }
+
+ private fun setBackgroundViewInfoOnPhotoView(frame: StoryFrameItem, backgroundImageSource: PhotoView) {
+ val backgroundViewInfo = frame.source.backgroundViewInfo
+ // load image matrix from data if it exists
+ backgroundViewInfo?.let {
+ val matrix = Matrix()
+ matrix.setValues(it.imageMatrixValues)
+ backgroundImageSource.apply {
+ setSuppMatrix(matrix)
+ scaleType = it.scaleType
+ }
+ }
+ }
+
+ private fun loadImageWithGlideToPrepare(frame: StoryFrameItem) {
+ val model = (frame.source as? FileBackgroundSource)?.file
+ ?: (frame.source as UriBackgroundSource).contentUri
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val futureTarget = Glide.with(this@ComposeLoopFrameActivity)
+ .asDrawable()
+ .load(model)
+ .fitCenter() // we use fitCenter at first (instead of cropping) so we don't lose any information
+ .submit(screenWidth, screenHeight) // we're not going to export images greater than the screen size
+ val drawable = futureTarget.get()
+
+ val doAfterUse = object : ImageLoadedInterface {
+ override fun doAfter() {
+ // here setup the PhotoView support matrix
+ // we use a Handler because we need to set the support matrix only once the drawable
+ // has been set on the PhotoView, otherwise the matrix is not applied
+ // see
+ // https://github.com/Baseflow/PhotoView/blob/139a9ffeaf70bd628b015374cb6530fcf7d0bcb7/photoview/src/main/java/com/github/chrisbanes/photoview/PhotoViewAttacher.java#L279-L289
+ // if we're all good, doAfter() will be callled on Glide's `onResourceReady`, so
+ // let's setup the ViewMatrix
+ Handler().post {
+ setBackgroundViewInfoOnPhotoView(
+ frame,
+ photoEditor.composedCanvas.source as PhotoView
+ )
+ }
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ // 1. if the image being loaded matches the aspect ratio of the device screen, use center_crop
+ // no parts would actually be cropped, given the matching aspect ratio it should fit
+ // except the opaque bar given we're normalizing to 9:16 on export
+ val drawableAspectRatio = calculateAspectRatioForDrawable(drawable)
+ bottom_opaque_bar.visibility = View.VISIBLE
+ if (isAspectRatioSimilarByPercentage(drawableAspectRatio, screenSizeRatio, 0.01f)) {
+ photoEditorView.source.scaleType = CENTER_CROP
+ loadImageWithGlideToDraw(drawable, CenterCrop(), screenWidth, screenHeight, doAfterUse)
+ } else {
+ // 2. if the device is taller than 9:16, and image is portrait
+ // just crop the bottom (showing the opaque bar)
+ if (isScreenTallerThan916(screenWidth, screenHeight) &&
+ (drawable.intrinsicHeight > drawable.intrinsicWidth)
+ ) {
+ val transformToUse = if (drawable.intrinsicWidth >= screenWidth) {
+ // this aligns to top so there's no top black bar
+ photoEditorView.source.scaleType = FIT_START
+ null
+ } else {
+ photoEditorView.source.scaleType = FIT_CENTER
+ FitCenter()
+ }
+ loadImageWithGlideToDraw(drawable, transformToUse,
+ normalizedSize.width, normalizedSize.height, doAfterUse)
+ } else {
+ // 3. else, load with fit-center (black bars on the side that doesn't fit)
+ // see https://developer.android.com/reference/android/graphics/Matrix.ScaleToFit#CENTER
+ photoEditorView.source.scaleType = FIT_CENTER
+ loadImageWithGlideToDraw(drawable, FitCenter(), screenWidth, screenHeight, doAfterUse)
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun loadImageWithGlideToDraw(
+ drawable: Drawable,
+ transformToUse: BitmapTransformation? = null,
+ overrideWidth: Int,
+ overrideHeight: Int,
+ doAfterUse: ImageLoadedInterface
+ ) {
+ withContext(Dispatchers.Main) {
+ transformToUse?.let {
+ Glide.with(this@ComposeLoopFrameActivity)
+ .load(drawable)
+ .transform(it)
+ .listener(provideGlideRequestListener(doAfterUse))
+ .override(overrideWidth, overrideHeight)
+ .into(photoEditorView.source)
+ } ?: Glide.with(this@ComposeLoopFrameActivity)
+ .load(drawable)
+ .listener(provideGlideRequestListener(doAfterUse))
+ .override(overrideWidth, overrideHeight)
+ .into(photoEditorView.source)
+ }
+ }
+
+ interface ImageLoadedInterface {
+ fun doAfter()
+ }
+
+ private fun provideGlideRequestListener(callback: ImageLoadedInterface): RequestListener {
+ return object : RequestListener {
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean
+ ): Boolean {
+ // let the default implementation run
+ return false
+ }
+
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ callback.doAfter()
+ // return false to let Glide proceed and set the drawable
+ return false
+ }
+ }
+ }
+
override fun onStoryFrameAddTapped() {
addCurrentViewsToFrameAtIndex(storyViewModel.getSelectedFrameIndex())
+ refreshBackgroundViewInfoOnSelectedFrame()
showMediaPicker()
}
diff --git a/stories/src/main/java/com/wordpress/stories/compose/frame/FrameSaveManager.kt b/stories/src/main/java/com/wordpress/stories/compose/frame/FrameSaveManager.kt
index cbb3f2538..5748128f5 100644
--- a/stories/src/main/java/com/wordpress/stories/compose/frame/FrameSaveManager.kt
+++ b/stories/src/main/java/com/wordpress/stories/compose/frame/FrameSaveManager.kt
@@ -3,14 +3,19 @@ package com.wordpress.stories.compose.frame
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
+import android.graphics.Matrix
import android.net.Uri
import android.view.View
import android.view.ViewGroup.LayoutParams
+import android.widget.ImageView.ScaleType.CENTER_CROP
+import android.widget.ImageView.ScaleType.FIT_CENTER
+import android.widget.ImageView.ScaleType.FIT_START
import android.widget.RelativeLayout
import com.automattic.photoeditor.PhotoEditor
import com.automattic.photoeditor.PhotoEditor.OnSaveWithCancelAndProgressListener
import com.automattic.photoeditor.views.PhotoEditorView
import com.automattic.photoeditor.views.ViewType.STICKER_ANIMATED
+import com.automattic.photoeditor.views.background.fixed.BackgroundImageView
import com.wordpress.stories.compose.story.StoryFrameItem
import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundSource.FileBackgroundSource
import com.wordpress.stories.compose.story.StoryFrameItem.BackgroundSource.UriBackgroundSource
@@ -20,8 +25,8 @@ import com.wordpress.stories.compose.story.StoryFrameItemType.VIDEO
import com.wordpress.stories.util.cloneViewSpecs
import com.wordpress.stories.util.removeViewFromParent
import com.bumptech.glide.Glide
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.request.FutureTarget
+import com.bumptech.glide.request.RequestOptions
import com.wordpress.stories.util.isSizeRatio916
import com.wordpress.stories.util.normalizeSizeExportTo916
import kotlinx.coroutines.CoroutineScope
@@ -36,6 +41,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import java.io.File
import kotlin.coroutines.CoroutineContext
+import jp.wasabeef.glide.transformations.BlurTransformation
typealias FrameIndex = Int
@@ -142,8 +148,13 @@ class FrameSaveManager(
} else {
try {
// create ghost PhotoEditorView to be used for saving off-screen
+ val originalMatrix = Matrix()
+ frame.source.backgroundViewInfo?.let {
+ originalMatrix.setValues(it.imageMatrixValues)
+ }
+
val ghostPhotoEditorView = createGhostPhotoEditor(context, photoEditor.composedCanvas)
- frameFile = saveImageFrame(context, frame, ghostPhotoEditorView, frameIndex)
+ frameFile = saveImageFrame(context, frame, ghostPhotoEditorView, originalMatrix, frameIndex)
frame.composedFrameFile = frameFile
saveProgressListener?.onFrameSaveCompleted(frameIndex, frame)
} catch (ex: Exception) {
@@ -167,10 +178,11 @@ class FrameSaveManager(
context: Context,
frame: StoryFrameItem,
ghostPhotoEditorView: PhotoEditorView,
+ originalMatrix: Matrix,
frameIndex: FrameIndex
): File {
// prepare the ghostview with its background image and the AddedViews on top of it
- val futureTarget = preparePhotoEditorViewForSnapshot(context, frame, ghostPhotoEditorView)
+ val futureTargetPair = preparePhotoEditorViewForSnapshot(context, frame, originalMatrix, ghostPhotoEditorView)
val file = withContext(Dispatchers.IO) {
if (normalizeTo916 && !isSizeRatio916(ghostPhotoEditorView.width, ghostPhotoEditorView.height)) {
@@ -188,7 +200,8 @@ class FrameSaveManager(
}
releaseAddedViewsAfterSnapshot(frame)
- Glide.with(context).clear(futureTarget)
+ Glide.with(context).clear(futureTargetPair.first)
+ Glide.with(context).clear(futureTargetPair.second)
return file
}
@@ -276,21 +289,71 @@ class FrameSaveManager(
private suspend fun preparePhotoEditorViewForSnapshot(
context: Context,
frame: StoryFrameItem,
+ originalMatrix: Matrix,
ghostPhotoEditorView: PhotoEditorView
- ): FutureTarget {
+ ): Pair, FutureTarget> {
// prepare background
val uri = (frame.source as? UriBackgroundSource)?.contentUri
?: (frame.source as FileBackgroundSource).file
+ // -----------------------------------
+ // first set the background blurred image
+ val targetBlurredView = ghostPhotoEditorView.sourceBlurredBkg
+
+ // making use of Glide to decode bitmap and get the right orientation automatically
+ // http://bumptech.github.io/glide/doc/getting-started.html#background-threads
+ val futureBlurredTarget = Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ .apply(RequestOptions.bitmapTransform(BlurTransformation(25, 3)))
+ .submit(targetBlurredView.measuredWidth, targetBlurredView.measuredHeight)
+ targetBlurredView.setImageBitmap(futureBlurredTarget.get())
+
+ // -----------------------------------
+ // now set the actual background image
+ val targetView = ghostPhotoEditorView.source
+ val scaleType = frame.source.backgroundViewInfo?.scaleType
+
// making use of Glide to decode bitmap and get the right orientation automatically
// http://bumptech.github.io/glide/doc/getting-started.html#background-threads
- val futureTarget = Glide.with(context)
- .asBitmap()
- .load(uri)
- .transform(CenterCrop()) // also use CenterCrop as it's the same the user was seeing as per WYSIWYG
- .submit(ghostPhotoEditorView.source.measuredWidth, ghostPhotoEditorView.source.measuredHeight)
+ val futureTarget = when (scaleType) {
+ FIT_START ->
+ Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ // no transform used when FIT_START, see correlation in ComposeLoopFrameActivity's
+ // loadImageWithGlideToPrepare()
+ .submit(targetView.measuredWidth, targetView.measuredHeight)
+ FIT_CENTER ->
+ Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ .fitCenter() // we use fitCenter at first (instead of cropping) so we don't lose any information
+ .submit(targetView.measuredWidth, targetView.measuredHeight)
+ CENTER_CROP ->
+ Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ .centerCrop() // we use fitCenter at first (instead of cropping) so we don't lose any information
+ .submit(targetView.measuredWidth, targetView.measuredHeight)
+ else -> // default case with no transform needed so futureTarget is initialized,
+ // but we don't really expect to get this case
+ Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ .submit(targetView.measuredWidth, targetView.measuredHeight)
+ }
val bitmap = futureTarget.get()
- ghostPhotoEditorView.source.setImageBitmap(bitmap)
+ targetView.setImageBitmap(bitmap)
+
+ // IMPORTANT: scaleType and setSuppMatrix should only be called _after_ the bitmap is set on the targetView
+ // by means of targetView.setImageBitmap(). Calling this before will have no effect due to PhotoView's checks.
+ (targetView as BackgroundImageView).apply {
+ frame.source.backgroundViewInfo?.let {
+ this.scaleType = it.scaleType
+ }
+ setSuppMatrix(originalMatrix)
+ }
// removeViewFromParent for views that were added in the UI thread need to also run on the main thread
// otherwise we'd get a android.view.ViewRootImpl$CalledFromWrongThreadException:
@@ -308,7 +371,7 @@ class FrameSaveManager(
}
}
}
- return futureTarget
+ return Pair, FutureTarget>(futureTarget, futureBlurredTarget)
}
private fun getViewLayoutParams(): LayoutParams {
diff --git a/stories/src/main/java/com/wordpress/stories/compose/story/StoryFrameItem.kt b/stories/src/main/java/com/wordpress/stories/compose/story/StoryFrameItem.kt
index f47fb9370..a9e8a33f6 100644
--- a/stories/src/main/java/com/wordpress/stories/compose/story/StoryFrameItem.kt
+++ b/stories/src/main/java/com/wordpress/stories/compose/story/StoryFrameItem.kt
@@ -1,6 +1,7 @@
package com.wordpress.stories.compose.story
import android.net.Uri
+import android.widget.ImageView.ScaleType
import com.automattic.photoeditor.views.added.AddedView
import com.automattic.photoeditor.views.added.AddedViewList
import com.wordpress.stories.compose.frame.StorySaveEvents.SaveResultReason
@@ -27,8 +28,15 @@ data class StoryFrameItem(
var composedFrameFile: File? = null,
var id: String? = null
) {
+ @Serializable
+ data class BackgroundViewInfo(
+ val imageMatrixValues: FloatArray,
+ val scaleType: ScaleType
+ )
+
@Serializable
sealed class BackgroundSource {
+ var backgroundViewInfo: BackgroundViewInfo? = null
@Serializable
data class UriBackgroundSource(
@Serializable(with = UriSerializer::class)
diff --git a/stories/src/main/java/com/wordpress/stories/util/ViewUtils.kt b/stories/src/main/java/com/wordpress/stories/util/ViewUtils.kt
index 78175618c..b821edb1a 100644
--- a/stories/src/main/java/com/wordpress/stories/util/ViewUtils.kt
+++ b/stories/src/main/java/com/wordpress/stories/util/ViewUtils.kt
@@ -1,5 +1,6 @@
package com.wordpress.stories.util
+import android.graphics.drawable.Drawable
import android.util.Size
import android.view.View
import android.view.ViewGroup
@@ -39,6 +40,10 @@ fun isSizeRatio916(originalWidth: Int, originalHeight: Int): Boolean {
return (originalWidth.toFloat() / originalHeight.toFloat()) == TARGET_RATIO_9_16
}
+fun isScreenTallerThan916(originalWidth: Int, originalHeight: Int): Boolean {
+ return (originalWidth.toFloat() / originalHeight.toFloat()) < TARGET_RATIO_9_16
+}
+
fun normalizeSizeExportTo916(originalWidth: Int, originalHeight: Int): ScreenSize {
/*
1. if the screen is 16:9, we're OK
@@ -65,3 +70,16 @@ fun normalizeSizeExportTo916(originalWidth: Int, originalHeight: Int): ScreenSiz
}
}
}
+
+fun calculateAspectRatioForDrawable(drawable: Drawable): Float {
+ val width = drawable.intrinsicWidth
+ val height = drawable.intrinsicHeight
+ return width.toFloat() / height.toFloat()
+}
+
+fun isAspectRatioSimilarByPercentage(aspectRatio1: Float, aspectRatio2: Float, percentage: Float): Boolean {
+ return (Math.abs(aspectRatio1 - aspectRatio2) < percentage)
+}
+fun getSizeRatio(originalWidth: Int, originalHeight: Int): Float {
+ return (originalWidth.toFloat() / originalHeight.toFloat())
+}
diff --git a/stories/src/main/res/layout/content_composer.xml b/stories/src/main/res/layout/content_composer.xml
index d18431d0c..10fadc358 100644
--- a/stories/src/main/res/layout/content_composer.xml
+++ b/stories/src/main/res/layout/content_composer.xml
@@ -20,6 +20,15 @@
android:animateLayoutChanges="true"
android:background="@color/black" />
+
+
#66000000
#99000000
@color/black_transp_light
+ @color/black
@color/black_transp_light
@color/black_transp_light_darker