Use simple dialog for avatar selection.

This commit is contained in:
onurays 2020-06-29 10:20:59 +03:00 committed by Benoit Marty
parent 512e4f0ce3
commit ad084e1fec
4 changed files with 56 additions and 267 deletions

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.media package im.vector.riotx.features.media
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -31,19 +32,19 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.roomprofile.AvatarSelectorView import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.MultiPicker
import im.vector.riotx.multipicker.entity.MultiPickerImageType import im.vector.riotx.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.activity_big_image_viewer.* import kotlinx.android.synthetic.main.activity_big_image_viewer.*
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback { class BigImageViewerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var stringProvider: StringProvider
private var uri: Uri? = null private var uri: Uri? = null
private lateinit var avatarSelector: AvatarSelectorView
override fun getMenuRes() = R.menu.vector_big_avatar_viewer override fun getMenuRes() = R.menu.vector_big_avatar_viewer
@ -91,23 +92,24 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback
return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false) return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false)
} }
private fun showAvatarSelector() {
if (!::avatarSelector.isInitialized) {
avatarSelector = AvatarSelectorView(this, layoutInflater, this)
}
avatarSelector.show(bigImageViewerToolbar, false)
}
private var avatarCameraUri: Uri? = null private var avatarCameraUri: Uri? = null
override fun onTypeSelected(type: AvatarSelectorView.Type) { private fun showAvatarSelector() {
when (type) { AlertDialog
AvatarSelectorView.Type.CAMERA -> { .Builder(this)
.setItems(arrayOf(
stringProvider.getString(R.string.attachment_type_camera),
stringProvider.getString(R.string.attachment_type_gallery)
)) { dialog, which ->
dialog.cancel()
when (which) {
0 -> {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
} }
AvatarSelectorView.Type.GALLERY -> { 1 -> {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
} }
} }
}.show()
} }
private fun onRoomAvatarSelected(image: MultiPickerImageType) { private fun onRoomAvatarSelected(image: MultiPickerImageType) {

View File

@ -1,216 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.TargetApi
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.Pair
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.doOnNextLayout
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.riotx.R
import im.vector.riotx.core.extensions.getMeasurements
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.features.roomprofile.AvatarSelectorView.Callback
import kotlin.math.max
private const val ANIMATION_DURATION = 250
/**
* This class is the view presenting choices for picking avatar.
* It will return result through [Callback].
*/
class AvatarSelectorView(context: Context,
inflater: LayoutInflater,
var callback: Callback?)
: PopupWindow(context) {
interface Callback {
fun onTypeSelected(type: Type)
}
private val iconColorGenerator = ColorGenerator.MATERIAL
private var galleryButton: ImageButton
private var cameraButton: ImageButton
private var anchor: View? = null
init {
val root = FrameLayout(context)
val layout = inflater.inflate(R.layout.view_avatar_selector, root, true)
galleryButton = layout.findViewById<ImageButton>(R.id.avatarGalleryButton).configure(Type.GALLERY)
cameraButton = layout.findViewById<ImageButton>(R.id.avatarCameraButton).configure(Type.CAMERA)
contentView = root
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@Suppress("DEPRECATION")
setBackgroundDrawable(BitmapDrawable())
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true
isTouchable = true
}
fun show(anchor: View, isKeyboardOpen: Boolean) {
this.anchor = anchor
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
if (isKeyboardOpen) {
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
} else {
val contentViewHeight = if (contentView.height == 0) {
contentView.getMeasurements().second
} else {
contentView.height
}
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
}
contentView.doOnNextLayout {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowInCircular(anchor, contentView)
} else {
animateWindowInTranslate(contentView)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
}
}
override fun dismiss() {
val capturedAnchor = anchor
if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowOutCircular(capturedAnchor, contentView)
} else {
animateWindowOutTranslate(contentView)
}
}
private fun animateButtonIn(button: View, delay: Int) {
val animation = AnimationSet(true)
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
animation.addAnimation(scale)
animation.interpolator = OvershootInterpolator(1f)
animation.duration = ANIMATION_DURATION.toLong()
animation.startOffset = delay.toLong()
button.startAnimation(animation)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowInCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(contentView,
coordinates.first,
coordinates.second,
0f,
max(contentView.width, contentView.height).toFloat())
animator.duration = ANIMATION_DURATION.toLong()
animator.start()
}
private fun animateWindowInTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
animation.duration = ANIMATION_DURATION.toLong()
getContentView().startAnimation(animation)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowOutCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
coordinates.first,
coordinates.second,
max(getContentView().width, getContentView().height).toFloat(),
0f)
animator.duration = ANIMATION_DURATION.toLong()
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super@AvatarSelectorView.dismiss()
}
})
animator.start()
}
private fun animateWindowOutTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
animation.duration = ANIMATION_DURATION.toLong()
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
super@AvatarSelectorView.dismiss()
}
override fun onAnimationRepeat(animation: Animation) {}
})
getContentView().startAnimation(animation)
}
private fun getClickCoordinates(anchor: View, contentView: View): Pair<Int, Int> {
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
val contentCoordinates = IntArray(2)
contentView.getLocationOnScreen(contentCoordinates)
val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2
val y = anchorCoordinates[1] - contentCoordinates[1]
return Pair(x, y)
}
private fun ImageButton.configure(type: Type): ImageButton {
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
this.setOnClickListener(TypeClickListener(type))
return this
}
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
override fun onClick(v: View) {
dismiss()
callback?.onTypeSelected(type)
}
}
/**
* The all possible types to pick with their required permissions.
*/
enum class Type(val permissionsBit: Int) {
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
GALLERY(PERMISSIONS_FOR_WRITING_FILES),
}
}

View File

@ -47,6 +47,9 @@ import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.core.utils.startSharePlainTextIntent
import im.vector.riotx.features.crypto.util.toImageRes import im.vector.riotx.features.crypto.util.toImageRes
@ -76,7 +79,7 @@ class RoomProfileFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory, val roomProfileViewModelFactory: RoomProfileViewModel.Factory,
val colorProvider: ColorProvider val colorProvider: ColorProvider
) : VectorBaseFragment(), RoomProfileController.Callback, AvatarSelectorView.Callback { ) : VectorBaseFragment(), RoomProfileController.Callback {
private val roomProfileArgs: RoomProfileArgs by args() private val roomProfileArgs: RoomProfileArgs by args()
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -85,8 +88,6 @@ class RoomProfileFragment @Inject constructor(
private var appBarStateChangeListener: AppBarStateChangeListener? = null private var appBarStateChangeListener: AppBarStateChangeListener? = null
private lateinit var avatarSelector: AvatarSelectorView
override fun getLayoutResId() = R.layout.fragment_matrix_profile override fun getLayoutResId() = R.layout.fragment_matrix_profile
override fun getMenuRes() = R.menu.vector_room_profile override fun getMenuRes() = R.menu.vector_room_profile
@ -251,23 +252,27 @@ class RoomProfileFragment @Inject constructor(
} }
private fun showAvatarSelector() { private fun showAvatarSelector() {
if (!::avatarSelector.isInitialized) { AlertDialog
avatarSelector = AvatarSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomProfileFragment) .Builder(requireContext())
} .setItems(arrayOf(
avatarSelector.show(vector_coordinator_layout, false) getString(R.string.attachment_type_camera),
getString(R.string.attachment_type_gallery)
)) { dialog, which ->
dialog.cancel()
onAvatarTypeSelected(isCamera = (which == 0))
}.show()
} }
private var avatarCameraUri: Uri? = null private var avatarCameraUri: Uri? = null
override fun onTypeSelected(type: AvatarSelectorView.Type) { private fun onAvatarTypeSelected(isCamera: Boolean) {
when (type) { if (isCamera) {
AvatarSelectorView.Type.CAMERA -> { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
} }
AvatarSelectorView.Type.GALLERY -> { } else {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
} }
} }
}
private fun onRoomAvatarSelected(image: MultiPickerImageType) { private fun onRoomAvatarSelected(image: MultiPickerImageType) {
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")

View File

@ -64,7 +64,6 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.media.createUCropWithDefaultSettings import im.vector.riotx.features.media.createUCropWithDefaultSettings
import im.vector.riotx.features.roomprofile.AvatarSelectorView
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.workers.signout.SignOutUiWorker import im.vector.riotx.features.workers.signout.SignOutUiWorker
import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.MultiPicker
@ -76,7 +75,7 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelectorView.Callback { class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_general_title override var titleRes = R.string.settings_general_title
override val preferenceXmlRes = R.xml.vector_settings_general override val preferenceXmlRes = R.xml.vector_settings_general
@ -84,7 +83,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect
private var mDisplayedEmails = ArrayList<String>() private var mDisplayedEmails = ArrayList<String>()
private var mDisplayedPhoneNumber = ArrayList<String>() private var mDisplayedPhoneNumber = ArrayList<String>()
private lateinit var avatarSelector: AvatarSelectorView
private var avatarCameraUri: Uri? = null private var avatarCameraUri: Uri? = null
private val mUserSettingsCategory by lazy { private val mUserSettingsCategory by lazy {
@ -296,7 +294,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
onTypeSelected(AvatarSelectorView.Type.CAMERA) onAvatarTypeSelected(true)
} }
} }
} }
@ -404,26 +402,26 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect
* Update the avatar. * Update the avatar.
*/ */
private fun onUpdateAvatarClick() { private fun onUpdateAvatarClick() {
if (!::avatarSelector.isInitialized) { AlertDialog
avatarSelector = AvatarSelectorView(activity!!, activity!!.layoutInflater, this) .Builder(requireContext())
} .setItems(arrayOf(
mUserAvatarPreference.mAvatarView?.let { getString(R.string.attachment_type_camera),
avatarSelector.show(it, false) getString(R.string.attachment_type_gallery)
} )) { dialog, which ->
dialog.cancel()
onAvatarTypeSelected(isCamera = (which == 0))
}.show()
} }
override fun onTypeSelected(type: AvatarSelectorView.Type) { private fun onAvatarTypeSelected(isCamera: Boolean) {
when (type) { if (isCamera) {
AvatarSelectorView.Type.CAMERA -> {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
} }
} } else {
AvatarSelectorView.Type.GALLERY -> {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
} }
} }
}
private fun onAvatarSelected(image: MultiPickerImageType) { private fun onAvatarSelected(image: MultiPickerImageType) {
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")