kopia lustrzana https://github.com/ryukoposting/Signal-Android
Conversation tab bar animations.
rodzic
2b5d65ae04
commit
fd930d0b1d
|
@ -1,10 +1,20 @@
|
||||||
package org.thoughtcrime.securesms.stories.tabs
|
package org.thoughtcrime.securesms.stories.tabs
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.animation.PathInterpolatorCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.airbnb.lottie.LottieProperty
|
||||||
|
import com.airbnb.lottie.model.KeyPath
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
@ -18,14 +28,32 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||||
|
|
||||||
private lateinit var chatsUnreadIndicator: TextView
|
private lateinit var chatsUnreadIndicator: TextView
|
||||||
private lateinit var storiesUnreadIndicator: TextView
|
private lateinit var storiesUnreadIndicator: TextView
|
||||||
private lateinit var chatsIcon: View
|
private lateinit var chatsIcon: LottieAnimationView
|
||||||
private lateinit var storiesIcon: View
|
private lateinit var storiesIcon: LottieAnimationView
|
||||||
|
private lateinit var chatsPill: ImageView
|
||||||
|
private lateinit var storiesPill: ImageView
|
||||||
|
|
||||||
|
private var pillAnimator: Animator? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
chatsUnreadIndicator = view.findViewById(R.id.chats_unread_indicator)
|
chatsUnreadIndicator = view.findViewById(R.id.chats_unread_indicator)
|
||||||
storiesUnreadIndicator = view.findViewById(R.id.stories_unread_indicator)
|
storiesUnreadIndicator = view.findViewById(R.id.stories_unread_indicator)
|
||||||
chatsIcon = view.findViewById(R.id.chats_tab_icon)
|
chatsIcon = view.findViewById(R.id.chats_tab_icon)
|
||||||
storiesIcon = view.findViewById(R.id.stories_tab_icon)
|
storiesIcon = view.findViewById(R.id.stories_tab_icon)
|
||||||
|
chatsPill = view.findViewById(R.id.chats_pill)
|
||||||
|
storiesPill = view.findViewById(R.id.stories_pill)
|
||||||
|
|
||||||
|
val iconTint = ContextCompat.getColor(requireContext(), R.color.signal_colorOnSecondaryContainer)
|
||||||
|
|
||||||
|
chatsIcon.addValueCallback(
|
||||||
|
KeyPath("**"),
|
||||||
|
LottieProperty.COLOR
|
||||||
|
) { iconTint }
|
||||||
|
|
||||||
|
storiesIcon.addValueCallback(
|
||||||
|
KeyPath("**"),
|
||||||
|
LottieProperty.COLOR
|
||||||
|
) { iconTint }
|
||||||
|
|
||||||
view.findViewById<View>(R.id.chats_tab_touch_point).setOnClickListener {
|
view.findViewById<View>(R.id.chats_tab_touch_point).setOnClickListener {
|
||||||
viewModel.onChatsSelected()
|
viewModel.onChatsSelected()
|
||||||
|
@ -39,9 +67,19 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update(state: ConversationListTabsState) {
|
private fun update(state: ConversationListTabsState) {
|
||||||
|
val wasChatSelected = chatsIcon.isSelected
|
||||||
|
|
||||||
chatsIcon.isSelected = state.tab == ConversationListTab.CHATS
|
chatsIcon.isSelected = state.tab == ConversationListTab.CHATS
|
||||||
storiesIcon.isSelected = state.tab == ConversationListTab.STORIES
|
storiesIcon.isSelected = state.tab == ConversationListTab.STORIES
|
||||||
|
|
||||||
|
chatsPill.isSelected = chatsIcon.isSelected
|
||||||
|
storiesPill.isSelected = storiesIcon.isSelected
|
||||||
|
|
||||||
|
if (chatsIcon.isSelected xor wasChatSelected) {
|
||||||
|
runLottieAnimations(chatsIcon, storiesIcon)
|
||||||
|
runPillAnimation(chatsPill, storiesPill)
|
||||||
|
}
|
||||||
|
|
||||||
chatsUnreadIndicator.visible = state.unreadChatsCount > 0
|
chatsUnreadIndicator.visible = state.unreadChatsCount > 0
|
||||||
chatsUnreadIndicator.text = formatCount(state.unreadChatsCount)
|
chatsUnreadIndicator.text = formatCount(state.unreadChatsCount)
|
||||||
|
|
||||||
|
@ -51,6 +89,47 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||||
requireView().visible = !state.isSearchOpen
|
requireView().visible = !state.isSearchOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun runLottieAnimations(vararg toAnimate: LottieAnimationView) {
|
||||||
|
toAnimate.forEach {
|
||||||
|
if (it.isSelected) {
|
||||||
|
it.resumeAnimation()
|
||||||
|
} else {
|
||||||
|
if (it.isAnimating) {
|
||||||
|
it.pauseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
it.progress = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runPillAnimation(vararg toAnimate: ImageView) {
|
||||||
|
val (selected, unselected) = toAnimate.partition { it.isSelected }
|
||||||
|
|
||||||
|
pillAnimator?.cancel()
|
||||||
|
pillAnimator = AnimatorSet().apply {
|
||||||
|
duration = 150
|
||||||
|
interpolator = PathInterpolatorCompat.create(0.17f, 0.17f, 0f, 1f)
|
||||||
|
playTogether(
|
||||||
|
selected.map { view ->
|
||||||
|
view.visibility = View.VISIBLE
|
||||||
|
ValueAnimator.ofInt(view.paddingLeft, 0).apply {
|
||||||
|
addUpdateListener {
|
||||||
|
view.setPadding(it.animatedValue as Int, 0, it.animatedValue as Int, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
unselected.forEach {
|
||||||
|
val smallPad = DimensionUnit.DP.toPixels(16f).toInt()
|
||||||
|
it.setPadding(smallPad, 0, smallPad, 0)
|
||||||
|
it.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatCount(count: Long): String {
|
private fun formatCount(count: Long): String {
|
||||||
if (count > 99L) {
|
if (count > 99L) {
|
||||||
return getString(R.string.ConversationListTabs__99p)
|
return getString(R.string.ConversationListTabs__99p)
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<item android:state_selected="true">
|
android:shape="rectangle">
|
||||||
<shape android:shape="rectangle">
|
<solid android:color="@color/signal_colorPrimaryContainer" />
|
||||||
<solid android:color="@color/signal_colorPrimaryContainer" />
|
<corners android:radius="18dp" />
|
||||||
<corners android:radius="18dp" />
|
</shape>
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:drawable="@color/transparent" />
|
|
||||||
</selector>
|
|
|
@ -6,14 +6,12 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/signal_colorSecondaryContainer"
|
android:background="@color/signal_colorSecondaryContainer"
|
||||||
android:minHeight="80dp"
|
android:minHeight="80dp"
|
||||||
android:paddingHorizontal="@dimen/dsl_settings_gutter"
|
android:paddingHorizontal="@dimen/dsl_settings_gutter">
|
||||||
android:paddingTop="8dp"
|
|
||||||
android:paddingBottom="8dp">
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/chats_tab_touch_point"
|
android:id="@+id/chats_tab_touch_point"
|
||||||
android:layout_width="58dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="0dp"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
|
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
|
||||||
|
@ -22,28 +20,49 @@
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/stories_tab_touch_point"
|
android:id="@+id/stories_tab_touch_point"
|
||||||
android:layout_width="58dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="0dp"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
|
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
|
||||||
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
|
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<ImageView
|
||||||
android:id="@+id/chats_tab_icon"
|
android:id="@+id/chats_pill"
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:background="@drawable/conversation_tab_icon_background"
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/conversation_tab_icon_background"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/chats_tab_icon" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/stories_pill"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:src="@drawable/conversation_tab_icon_background"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/stories_tab_icon"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/stories_tab_icon" />
|
||||||
|
|
||||||
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
|
android:id="@+id/chats_tab_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:tint="@color/signal_colorOnSecondaryContainer"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/chats_tab_label"
|
app:layout_constraintBottom_toTopOf="@id/chats_tab_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/tabs_center_guide"
|
app:layout_constraintEnd_toStartOf="@id/tabs_center_guide"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
app:srcCompat="@drawable/ic_chat_message_24" />
|
app:lottie_rawRes="@raw/chats_32" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/chats_tab_label"
|
android:id="@+id/chats_tab_label"
|
||||||
|
@ -65,20 +84,18 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintGuide_percent="0.5" />
|
app:layout_constraintGuide_percent="0.5" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
android:id="@+id/stories_tab_icon"
|
android:id="@+id/stories_tab_icon"
|
||||||
android:layout_width="64dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:background="@drawable/conversation_tab_icon_background"
|
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:tint="@color/signal_colorOnSecondaryContainer"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/stories_tab_label"
|
app:layout_constraintBottom_toTopOf="@id/stories_tab_label"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/tabs_center_guide"
|
app:layout_constraintStart_toEndOf="@id/tabs_center_guide"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
app:srcCompat="@drawable/ic_stories_24" />
|
app:lottie_rawRes="@raw/stories_32" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/stories_tab_label"
|
android:id="@+id/stories_tab_label"
|
||||||
|
@ -97,7 +114,8 @@
|
||||||
android:id="@+id/chats_unread_indicator"
|
android:id="@+id/chats_unread_indicator"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="16sp"
|
android:layout_height="16sp"
|
||||||
android:layout_marginBottom="21dp"
|
android:layout_marginStart="28.5dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
android:background="@drawable/tab_unread_circle"
|
android:background="@drawable/tab_unread_circle"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:minWidth="16sp"
|
android:minWidth="16sp"
|
||||||
|
@ -106,15 +124,16 @@
|
||||||
android:textAppearance="@style/Signal.Text.Caption"
|
android:textAppearance="@style/Signal.Text.Caption"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="11sp"
|
android:textSize="11sp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
|
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
|
||||||
app:layout_constraintEnd_toEndOf="@id/chats_tab_touch_point"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="3" />
|
tools:text="3" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/stories_unread_indicator"
|
android:id="@+id/stories_unread_indicator"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="16sp"
|
android:layout_height="16sp"
|
||||||
android:layout_marginBottom="21dp"
|
android:layout_marginStart="28.5dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
android:background="@drawable/tab_unread_circle"
|
android:background="@drawable/tab_unread_circle"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:minWidth="16sp"
|
android:minWidth="16sp"
|
||||||
|
@ -123,8 +142,8 @@
|
||||||
android:textAppearance="@style/Signal.Text.Caption"
|
android:textAppearance="@style/Signal.Text.Caption"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="11sp"
|
android:textSize="11sp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/stories_tab_icon"
|
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
|
||||||
app:layout_constraintEnd_toEndOf="@id/stories_tab_touch_point"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="99+" />
|
tools:text="99+" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Ładowanie…
Reference in New Issue