PasswordFragment: Replace fab options with descriptive bottom sheet
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
eb5a30c3a9
commit
17385892cb
8 changed files with 141 additions and 191 deletions
|
@ -22,13 +22,13 @@ import androidx.lifecycle.observe
|
|||
import androidx.recyclerview.widget.FixOnItemTouchDispatchRecyclerView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
|
||||
import com.zeapo.pwdstore.git.BaseGitActivity
|
||||
import com.zeapo.pwdstore.git.GitOperationActivity
|
||||
import com.zeapo.pwdstore.ui.OnOffItemAnimator
|
||||
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
|
||||
import com.zeapo.pwdstore.ui.dialogs.ItemCreationBottomSheet
|
||||
import com.zeapo.pwdstore.utils.PasswordItem
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||
import java.io.File
|
||||
|
@ -57,17 +57,10 @@ class PasswordFragment : Fragment() {
|
|||
): View? {
|
||||
_binding = PasswordRecyclerViewBinding.inflate(inflater, container, false)
|
||||
initializePasswordList()
|
||||
val fab = binding.fab
|
||||
fab.setOnClickListener {
|
||||
toggleFabExpand(fab)
|
||||
}
|
||||
binding.createFolder.setOnClickListener {
|
||||
requireStore().createFolder()
|
||||
toggleFabExpand(fab)
|
||||
}
|
||||
binding.createPassword.setOnClickListener {
|
||||
requireStore().createPassword()
|
||||
toggleFabExpand(fab)
|
||||
binding.fab.setOnClickListener {
|
||||
ItemCreationBottomSheet().apply {
|
||||
setTargetFragment(this@PasswordFragment, 1000)
|
||||
}.show(parentFragmentManager, "BOTTOM_SHEET")
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
@ -145,14 +138,7 @@ class PasswordFragment : Fragment() {
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun toggleFabExpand(fab: FloatingActionButton) = with(fab) {
|
||||
isExpanded = !isExpanded
|
||||
isActivated = isExpanded
|
||||
animate().rotationBy(if (isExpanded) -45f else 45f).setDuration(100).start()
|
||||
}
|
||||
|
||||
private val actionModeCallback = object : ActionMode.Callback {
|
||||
|
||||
// Called when the action mode is created; startActionMode() was called
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
// Inflate a menu resource providing context menu items
|
||||
|
@ -282,6 +268,10 @@ class PasswordFragment : Fragment() {
|
|||
actionMode?.finish()
|
||||
}
|
||||
|
||||
fun createFolder() = requireStore().createFolder()
|
||||
|
||||
fun createPassword() = requireStore().createPassword()
|
||||
|
||||
interface OnFragmentInteractionListener {
|
||||
fun onFragmentInteraction(item: PasswordItem)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.ui.dialogs
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.zeapo.pwdstore.PasswordFragment
|
||||
import com.zeapo.pwdstore.R
|
||||
import com.zeapo.pwdstore.utils.resolveAttribute
|
||||
|
||||
class ItemCreationBottomSheet : BottomSheetDialogFragment() {
|
||||
private var behavior: BottomSheetBehavior<FrameLayout>? = null
|
||||
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (savedInstanceState != null) dismiss()
|
||||
return inflater.inflate(R.layout.item_create_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
val dialog = dialog as BottomSheetDialog? ?: return
|
||||
behavior = dialog.behavior
|
||||
behavior?.apply {
|
||||
state = BottomSheetBehavior.STATE_EXPANDED
|
||||
peekHeight = 0
|
||||
addBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_folder)?.setOnClickListener {
|
||||
(requireTargetFragment() as PasswordFragment).createFolder()
|
||||
dismiss()
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_password)?.setOnClickListener {
|
||||
(requireTargetFragment() as PasswordFragment).createPassword()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
|
||||
}
|
||||
view.background = gradientDrawable
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
behavior?.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun requireTargetFragment(): Fragment = requireNotNull(targetFragment) {
|
||||
"A target fragment must be set for $this"
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.widget.fab
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.transformation.ExpandableTransformationBehavior
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
* Taken from Mao Yufeng's excellent example at https://git.io/Jvml9, all credits to him for this.
|
||||
* It's hard to create per-file copyright rules for Spotless so I'm choosing to credit him here.
|
||||
*/
|
||||
class EmitExpandableTransformationBehavior @JvmOverloads constructor(
|
||||
context: Context? = null,
|
||||
attrs: AttributeSet? = null
|
||||
) : ExpandableTransformationBehavior(context, attrs) {
|
||||
|
||||
companion object {
|
||||
private const val EXPAND_DELAY = 60L
|
||||
private const val EXPAND_DURATION = 150L
|
||||
private const val COLLAPSE_DELAY = 60L
|
||||
private const val COLLAPSE_DURATION = 150L
|
||||
}
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
|
||||
return dependency is FloatingActionButton && child is ViewGroup
|
||||
}
|
||||
|
||||
override fun onCreateExpandedStateChangeAnimation(
|
||||
dependency: View,
|
||||
child: View,
|
||||
expanded: Boolean,
|
||||
isAnimating: Boolean
|
||||
): AnimatorSet {
|
||||
|
||||
if (child !is ViewGroup) {
|
||||
return AnimatorSet()
|
||||
}
|
||||
|
||||
val animations = ArrayList<Animator>()
|
||||
|
||||
if (expanded) {
|
||||
createExpandAnimation(child, isAnimating, animations)
|
||||
} else {
|
||||
createCollapseAnimation(child, animations)
|
||||
}
|
||||
|
||||
val set = AnimatorSet()
|
||||
set.playTogether(animations)
|
||||
set.addListener(
|
||||
onStart = {
|
||||
if (expanded) {
|
||||
child.isVisible = true
|
||||
}
|
||||
},
|
||||
onEnd = {
|
||||
if (!expanded) {
|
||||
child.isInvisible = true
|
||||
}
|
||||
}
|
||||
)
|
||||
return set
|
||||
}
|
||||
|
||||
private fun createExpandAnimation(
|
||||
child: ViewGroup,
|
||||
currentlyAnimating: Boolean,
|
||||
animations: MutableList<Animator>
|
||||
) {
|
||||
if (!currentlyAnimating) {
|
||||
child.children.forEach {
|
||||
it.alpha = 0f
|
||||
it.scaleX = 0.4f
|
||||
it.scaleY = 0.4f
|
||||
}
|
||||
}
|
||||
val delays = List(child.childCount) {
|
||||
it * EXPAND_DELAY
|
||||
}.reversed().asSequence()
|
||||
val scaleXHolder = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f)
|
||||
val scaleYHolder = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f)
|
||||
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 1f)
|
||||
val animators = child.children.zip(delays) { view, delay ->
|
||||
ObjectAnimator.ofPropertyValuesHolder(
|
||||
view,
|
||||
scaleXHolder,
|
||||
scaleYHolder,
|
||||
alphaHolder
|
||||
).apply {
|
||||
duration = EXPAND_DURATION
|
||||
startDelay = delay
|
||||
}
|
||||
}.toList()
|
||||
val animatorSet = AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
}
|
||||
animations.add(animatorSet)
|
||||
}
|
||||
|
||||
private fun createCollapseAnimation(
|
||||
child: ViewGroup,
|
||||
animations: MutableList<Animator>
|
||||
) {
|
||||
val delays = List(child.childCount) {
|
||||
it * COLLAPSE_DELAY
|
||||
}.asSequence()
|
||||
val scaleXHolder = PropertyValuesHolder.ofFloat(View.SCALE_X, 0.4f)
|
||||
val scaleYHolder = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.4f)
|
||||
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 0f)
|
||||
val animators = child.children.zip(delays) { view, delay ->
|
||||
ObjectAnimator.ofPropertyValuesHolder(
|
||||
view,
|
||||
scaleXHolder,
|
||||
scaleYHolder,
|
||||
alphaHolder
|
||||
).apply {
|
||||
duration = COLLAPSE_DURATION
|
||||
startDelay = delay
|
||||
}
|
||||
}.toList()
|
||||
val animatorSet = AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
}
|
||||
animations.add(animatorSet)
|
||||
}
|
||||
}
|
39
app/src/main/res/layout/item_create_sheet.xml
Normal file
39
app/src/main/res/layout/item_create_sheet.xml
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:id="@+id/create_folder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:text="@string/bottom_sheet_create_new_folder"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_margin="@dimen/normal_margin"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:icon="@drawable/ic_action_new_folder"
|
||||
app:iconPadding="@dimen/normal_margin"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:rippleColor="?attr/colorSecondary" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:id="@+id/create_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:text="@string/bottom_sheet_create_new_password"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_margin="@dimen/normal_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/create_folder"
|
||||
app:icon="@drawable/ic_action_new_password"
|
||||
app:iconPadding="@dimen/normal_margin"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:rippleColor="?attr/colorSecondary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -24,37 +24,6 @@
|
|||
tools:listitem="@layout/password_row_layout" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_anchor="@id/fab"
|
||||
app:layout_anchorGravity="top|center_horizontal"
|
||||
app:layout_behavior="com.zeapo.pwdstore.widget.fab.EmitExpandableTransformationBehavior"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/fab_margin"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_action_new_password" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_folder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/fab_margin"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_action_new_folder" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:src="@drawable/ic_add_white_48dp"
|
||||
|
|
|
@ -4,4 +4,6 @@
|
|||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="fab_compat_margin">16dp</dimen>
|
||||
<dimen name="fab_margin">8dp</dimen>
|
||||
<dimen name="normal_margin">8dp</dimen>
|
||||
<dimen name="bottom_sheet_item_height">56dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -360,4 +360,6 @@
|
|||
<string name="git_operation_unable_to_open_ssh_key_title">Unable to open the ssh-key</string>
|
||||
<string name="git_operation_unable_to_open_ssh_key_message">Please check that it was imported.</string>
|
||||
<string name="git_operation_wrong_passphrase">Wrong passphrase</string>
|
||||
<string name="bottom_sheet_create_new_folder">Create new folder</string>
|
||||
<string name="bottom_sheet_create_new_password">Create new password</string>
|
||||
</resources>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item>
|
||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.MaterialComponents.ActionBar</item>
|
||||
<item name="textInputStyle">@style/TextInputLayoutBase</item>
|
||||
<item name="bottomSheetDialogTheme">@style/BottomSheetDialogTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
|
||||
|
@ -33,6 +34,18 @@
|
|||
<item name="background">@color/primary_color</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomSheetDialogTheme" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:backgroundDimAmount">0.5</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
<item name="android:colorBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="NoBackgroundTheme" parent="@style/AppTheme">
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
|
|
Loading…
Reference in a new issue