Profile room: continue working on it (try to get a nice animation) [WIP]

This commit is contained in:
ganfra 2019-11-15 20:37:36 +01:00
parent 0edc953a23
commit 8aab46804b
11 changed files with 374 additions and 57 deletions

View file

@ -0,0 +1,223 @@
/*
* Copyright 2019 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.core.animations.behavior
import android.animation.ArgbEvaluator
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import im.vector.riotx.R
import kotlin.math.abs
private const val UNSPECIFIED_INT = Integer.MAX_VALUE
private val UNSPECIFIED_FLOAT = Float.MAX_VALUE
private const val DEPEND_TYPE_HEIGHT = 0
private const val DEPEND_TYPE_WIDTH = 1
private const val DEPEND_TYPE_X = 2
private const val DEPEND_TYPE_Y = 3
class PercentViewBehavior<V : View>(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<V>(context, attrs) {
private var dependType: Int = 0
private var dependViewId: Int = 0
private var dependTarget: Int = 0
private var dependStartX: Int = 0
private var dependStartY: Int = 0
private var dependStartWidth: Int = 0
private var dependStartHeight: Int = 0
private var startX: Int = 0
private var startY: Int = 0
private var startWidth: Int = 0
private var startHeight: Int = 0
private var startBackgroundColor: Int = 0
private var startAlpha: Float = 0f
private var startRotateX: Float = 0f
private var startRotateY: Float = 0f
private var targetX: Int = 0
private var targetY: Int = 0
private var targetWidth: Int = 0
private var targetHeight: Int = 0
private var targetBackgroundColor: Int = 0
private var targetAlpha: Float = 0f
private var targetRotateX: Float = 0f
private var targetRotateY: Float = 0f
/**
* Is the values prepared to be use
*/
private var isPrepared: Boolean = false
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior)
dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
a.recycle()
}
private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) {
dependStartX = dependency.x.toInt()
dependStartY = dependency.y.toInt()
dependStartWidth = dependency.width
dependStartHeight = dependency.height
startX = child.x.toInt()
startY = child.y.toInt()
startWidth = child.width
startHeight = child.height
startAlpha = child.alpha
startRotateX = child.rotationX
startRotateY = child.rotationY
// only set the start background color when the background is color drawable
val background = child.background
if (background is ColorDrawable) {
startBackgroundColor = background.color
}
// if parent fitsSystemWindows is true, add status bar height to target y if specified
if (parent.fitsSystemWindows && targetY != UNSPECIFIED_INT) {
var result = 0
val resources = parent.context.resources
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = resources.getDimensionPixelSize(resourceId)
}
targetY += result
}
isPrepared = true
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
return dependency.id == dependViewId
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
// first time, prepare values before continue
if (!isPrepared) {
prepare(parent, child, dependency)
}
updateView(child, dependency)
return false
}
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
val bool = super.onLayoutChild(parent, child, layoutDirection)
if (isPrepared) {
updateView(child, parent.getDependencies(child)[0])
}
return bool
}
/**
* Update the child view from the dependency states
*
* @param child child view
* @param dependency dependency view
*/
private fun updateView(child: V, dependency: View) {
var percent = 0f
var start = 0f
var current = 0f
var end = UNSPECIFIED_INT.toFloat()
when (dependType) {
DEPEND_TYPE_WIDTH -> {
start = dependStartWidth.toFloat()
current = dependency.width.toFloat()
end = dependTarget.toFloat()
}
DEPEND_TYPE_HEIGHT -> {
start = dependStartHeight.toFloat()
current = dependency.height.toFloat()
end = dependTarget.toFloat()
}
DEPEND_TYPE_X -> {
start = dependStartX.toFloat()
current = dependency.x
end = dependTarget.toFloat()
}
DEPEND_TYPE_Y -> {
start = dependStartY.toFloat()
current = dependency.y
end = dependTarget.toFloat()
}
}
// need to define target value according to the depend type, if not then skip
if (end != UNSPECIFIED_INT.toFloat()) {
percent = abs(current - start) / abs(end - start)
}
updateViewWithPercent(child, if (percent > 1f) 1f else percent)
}
private fun updateViewWithPercent(child: View, percent: Float) {
var newX = if (targetX == UNSPECIFIED_INT) 0f else (targetX - startX) * percent
var newY = if (targetY == UNSPECIFIED_INT) 0f else (targetY - startY) * percent
// set scale
if (targetWidth != UNSPECIFIED_INT) {
val newWidth = startWidth + (targetWidth - startWidth) * percent
child.scaleX = newWidth / startWidth
newX -= (startWidth - newWidth) / 2
}
if (targetHeight != UNSPECIFIED_INT) {
val newHeight = startHeight + (targetHeight - startHeight) * percent
child.scaleY = newHeight / startHeight
newY -= (startHeight - newHeight) / 2
}
// set new position
child.translationX = newX
child.translationY = newY
// set alpha
if (targetAlpha != UNSPECIFIED_FLOAT) {
child.alpha = startAlpha + (targetAlpha - startAlpha) * percent
}
// set background color
if (targetBackgroundColor != UNSPECIFIED_INT && startBackgroundColor != 0) {
val evaluator = ArgbEvaluator()
val color = evaluator.evaluate(percent, startBackgroundColor, targetBackgroundColor) as Int
child.setBackgroundColor(color)
}
// set rotation
if (targetRotateX != UNSPECIFIED_FLOAT) {
child.rotationX = startRotateX + (targetRotateX - startRotateX) * percent
}
if (targetRotateY != UNSPECIFIED_FLOAT) {
child.rotationY = startRotateY + (targetRotateY - startRotateY) * percent
}
child.requestLayout()
}
}

View file

@ -16,6 +16,7 @@
package im.vector.riotx.core.epoxy.profiles
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
@ -37,9 +38,12 @@ abstract class ProfileItemAction : VectorEpoxyModel<ProfileItemAction.Holder>()
var iconRes: Int = 0
@EpoxyAttribute
var editable: Boolean = true
@EpoxyAttribute
lateinit var listener: View.OnClickListener
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener(listener)
holder.editable.isVisible = editable
holder.title.text = title
holder.subtitle.setTextOrHide(subtitle)

View file

@ -332,12 +332,10 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
*/
protected fun configureToolbar(toolbar: Toolbar, displayBack: Boolean = true) {
setSupportActionBar(toolbar)
if (displayBack) {
supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
supportActionBar?.let {
it.setDisplayShowHomeEnabled(displayBack)
it.setDisplayHomeAsUpEnabled(displayBack)
it.title = null
}
}

View file

@ -22,29 +22,49 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.profiles.profileItemAction
import im.vector.riotx.core.epoxy.profiles.profileItemSection
import im.vector.riotx.core.resources.StringProvider
import javax.inject.Inject
class RoomProfileController @Inject constructor()
class RoomProfileController @Inject constructor(private val stringProvider: StringProvider)
: TypedEpoxyController<RoomProfileViewState>() {
var callback: Callback? = null
interface Callback {
fun onLearnMoreClicked()
fun onMemberListClicked()
fun onSettingsClicked()
}
override fun buildModels(data: RoomProfileViewState?) {
if (data == null) {
return
}
val roomSummary = data.roomSummary()
profileItemSection {
id("section_security")
title("Security")
}
val learnMoreSubtitle = if (data.isEncrypted) {
R.string.room_profile_encrypted_subtitle
} else {
R.string.room_profile_not_encrypted_subtitle
}
profileItemAction {
id("action_learn_more")
title("Learn more")
editable(true)
subtitle("Messages in this room are not end-to-end encrypted.")
subtitle(stringProvider.getString(learnMoreSubtitle))
listener { _ ->
callback?.onLearnMoreClicked()
}
}
dividerItem{
dividerItem {
id("action_learn_more_divider")
}
@ -53,33 +73,29 @@ class RoomProfileController @Inject constructor()
title("Options")
}
val numberOfMembers = (roomSummary?.otherMemberIds?.size ?: 0) + 1
profileItemAction {
iconRes(R.drawable.ic_person_outline_black)
id("action_member_list")
title("88 people")
title(stringProvider.getString(R.string.room_profile_member_list_title, numberOfMembers))
editable(true)
listener { _ ->
callback?.onMemberListClicked()
}
}
dividerItem{
dividerItem {
id("action_member_list_divider")
}
profileItemAction {
iconRes(R.drawable.ic_attachment)
id("action_files")
title("12 files")
editable(true)
}
dividerItem{
id("action_files_divider")
}
profileItemAction {
iconRes(R.drawable.ic_settings_x)
iconRes(R.drawable.ic_room_actions_settings)
id("action_settings")
title("Room settings")
editable(true)
listener { _ ->
callback?.onSettingsClicked()
}
}
}

View file

@ -43,7 +43,7 @@ class RoomProfileFragment @Inject constructor(
private val roomProfileController: RoomProfileController,
private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
) : VectorBaseFragment() {
) : VectorBaseFragment(), RoomProfileController.Callback {
private val roomProfileArgs: RoomProfileArgs by args()
private val roomProfileViewModel: RoomProfileViewModel by fragmentViewModel()
@ -57,9 +57,11 @@ class RoomProfileFragment @Inject constructor(
}
private fun setupRecyclerView() {
roomProfileController.callback = this
roomProfileRecyclerView.setHasFixedSize(true)
roomProfileRecyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
roomProfileRecyclerView.adapter = roomProfileController.adapter
}
override fun onDestroyView() {
@ -74,6 +76,7 @@ class RoomProfileFragment @Inject constructor(
activity?.finish()
} else {
roomProfileNameView.text = it.displayName
roomProfileNameView2.text = it.displayName
roomProfileIdView.text = it.roomId
roomProfileTopicView.setTextOrHide(it.topic)
avatarRenderer.render(it, roomProfileAvatarView)
@ -82,5 +85,19 @@ class RoomProfileFragment @Inject constructor(
roomProfileController.setData(state)
}
// RoomProfileController.Callback
override fun onLearnMoreClicked() {
vectorBaseActivity.notImplemented()
}
override fun onMemberListClicked() {
vectorBaseActivity.notImplemented("Room member list")
}
override fun onSettingsClicked() {
vectorBaseActivity.notImplemented("Room settings")
}
}

View file

@ -55,7 +55,10 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: R
room.rx().liveRoomSummary()
.unwrap()
.execute {
copy(roomSummary = it)
copy(
roomSummary = it,
isEncrypted = room.isEncrypted()
)
}
}

View file

@ -24,7 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
data class RoomProfileViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized
val roomSummary: Async<RoomSummary> = Uninitialized,
val isEncrypted: Boolean = false
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View file

@ -7,49 +7,40 @@
android:background="?riotx_header_panel_background">
<com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:id="@+id/roomProfileAppBarLayout"
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomProfileToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:titleEnabled="false"
app:toolbarId="@+id/roomProfileToolbar">
<LinearLayout
android:id="@+id/roomProfileHeaderView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:orientation="vertical"
app:layout_collapseMode="parallax">
<ImageView
android:id="@+id/roomProfileAvatarView"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:src="@drawable/ic_person_outline_black" />
android:paddingTop="160dp"
android:minHeight="280dp"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.9">
<TextView
android:id="@+id/roomProfileNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textStyle="bold"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Random" />
<TextView
@ -77,12 +68,14 @@
android:textStyle="normal"
tools:text="Here is a room topic, it can be multi-line but should always be displayed in full 🍱" />
<Space
android:layout_width="match_parent"
android:layout_height="24dp"/>
</LinearLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomProfileToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
@ -92,7 +85,42 @@
android:id="@+id/roomProfileRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_profile_action" />
<ImageView
android:id="@+id/roomProfileAvatarView"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:elevation="10dp"
app:behavior_dependTarget="-176dp"
app:behavior_dependType="y"
app:behavior_dependsOn="@id/roomProfileAppBarLayout"
app:behavior_targetHeight="40dp"
app:behavior_targetWidth="40dp"
app:behavior_targetX="56dp"
app:behavior_targetY="8dp"
app:layout_behavior="im.vector.riotx.core.animations.behavior.PercentViewBehavior"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomProfileNameView2"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:layout_marginStart="104dp"
android:layout_marginTop="-100dp"
android:alpha="0"
android:elevation="10dp"
android:gravity="center_vertical"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="18dp"
app:behavior_dependTarget="-208dp"
app:behavior_dependType="y"
app:behavior_dependsOn="@id/roomProfileAppBarLayout"
app:behavior_targetAlpha="1"
app:behavior_targetY="0dp"
app:layout_behavior="im.vector.riotx.core.animations.behavior.PercentViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -25,8 +25,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="16dp"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_room_actions_notifications_all" />
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/actionTitle"
@ -45,7 +45,7 @@
app:layout_constraintBottom_toTopOf="@+id/actionSubtitle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
tools:text="zbla azjazj" />
tools:text="Learn more" />
<TextView
android:id="@+id/actionSubtitle"
@ -64,7 +64,7 @@
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toBottomOf="@id/actionTitle"
app:layout_constraintWidth_default="wrap"
tools:text="zbla azjazj sdqkqsdkqsd sdqkqsdkqsdk kqdkqsdqk" />
tools:text="Messages in this room are not end-to-end encrypted" />
<ImageView

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentViewBehavior">
<attr name="behavior_dependsOn" format="reference" />
<attr name="behavior_dependType">
<enum name="height" value="0" />
<enum name="width" value="1" />
<enum name="x" value="2" />
<enum name="y" value="3" />
</attr>
<attr name="behavior_dependTarget" format="dimension" />
<attr name="behavior_targetX" format="dimension" />
<attr name="behavior_targetY" format="dimension" />
<attr name="behavior_targetWidth" format="dimension" />
<attr name="behavior_targetHeight" format="dimension" />
<attr name="behavior_targetBackgroundColor" format="color" />
<attr name="behavior_targetAlpha" format="float" />
<attr name="behavior_targetRotateX" format="float" />
<attr name="behavior_targetRotateY" format="float" />
</declare-styleable>
</resources>

View file

@ -19,4 +19,8 @@
<string name="help_long_click_on_room_for_more_options">Long click on a room to see more options</string>
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
<string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.</string>
<string name="room_profile_member_list_title">"%1$d people"</string>
</resources>