Add Floating Action Button Speed Dial to Android

This commit is contained in:
Florian Märkl 2019-10-05 22:33:56 +02:00
commit 5fab5b9f05
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
15 changed files with 553 additions and 20 deletions

View file

@ -13,7 +13,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".main.MainActivity">
<activity android:name=".main.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View file

@ -0,0 +1,102 @@
/*
* This file is part of Chiaki.
*
* Chiaki is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chiaki is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Chiaki. If not, see <https://www.gnu.org/licenses/>.
*/
package com.metallic.chiaki.main
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.animation.addListener
import androidx.core.view.children
import androidx.core.view.isGone
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 com.metallic.chiaki.R
class FloatingActionButtonBackgroundBehavior @JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null) : ExpandableTransformationBehavior(context, attrs)
{
companion object
{
private const val DURATION = 150L
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View)
= dependency is FloatingActionButton
override fun onCreateExpandedStateChangeAnimation(dependency: View, child: View, expanded: Boolean, isAnimating: Boolean): AnimatorSet
= AnimatorSet().also {
it.playTogether(listOf(
if(expanded)
createExpandAnimation(child, isAnimating)
else
createCollapseAnimation(child)
))
it.addListener(
onStart = {
if(expanded)
child.isVisible = true
},
onEnd = {
if(!expanded)
child.isGone = true
}
)
}
private fun createExpandAnimation(child: View, currentlyAnimating: Boolean): Animator
{
if(!currentlyAnimating)
child.alpha = 0f
val animator = ObjectAnimator.ofPropertyValuesHolder(
child,
PropertyValuesHolder.ofFloat(View.ALPHA, 1f)
).apply {
duration = DURATION
}
return AnimatorSet().apply {
playTogether(listOf(animator))
}
}
private fun createCollapseAnimation(child: View): Animator
{
val animator = ObjectAnimator.ofPropertyValuesHolder(
child,
PropertyValuesHolder.ofFloat(View.ALPHA, 0f)
).apply {
duration = DURATION
}
return AnimatorSet().apply {
playTogether(listOf(animator))
}
}
}

View file

@ -0,0 +1,128 @@
/*
* This file is part of Chiaki.
*
* Chiaki is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chiaki is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Chiaki. If not, see <https://www.gnu.org/licenses/>.
*/
package com.metallic.chiaki.main
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
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 com.metallic.chiaki.R
class FloatingActionButtonSpeedDialBehavior @JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null) : ExpandableTransformationBehavior(context, attrs)
{
companion object
{
private const val DELAY = 30L
private const val DURATION = 150L
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View)
= dependency is FloatingActionButton && child is ViewGroup
override fun onCreateExpandedStateChangeAnimation(dependency: View, child: View, expanded: Boolean, isAnimating: Boolean): AnimatorSet
= if(child !is ViewGroup)
AnimatorSet()
else
AnimatorSet().also {
it.playTogether(listOf(
if(expanded)
createExpandAnimation(child, isAnimating)
else
createCollapseAnimation(child)
))
it.addListener(
onStart = {
if(expanded)
child.isVisible = true
},
onEnd = {
if(!expanded)
child.isInvisible = true
}
)
}
private fun offset(resources: Resources) = resources.getDimension(R.dimen.floating_action_button_speed_dial_anim_offset)
private fun createExpandAnimation(child: ViewGroup, currentlyAnimating: Boolean): Animator
{
if(!currentlyAnimating)
{
child.children.forEach {
it.alpha = 0f
it.translationY = this.offset(child.resources)
}
}
val translationYHolder = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 1f)
val animators = child.children.mapIndexed { index, view ->
ObjectAnimator.ofPropertyValuesHolder(
view,
translationYHolder,
alphaHolder
).apply {
duration = DURATION
startDelay = (child.childCount - index - 1) * DELAY
interpolator = DecelerateInterpolator()
}
}.toList()
return AnimatorSet().apply {
playTogether(animators)
}
}
private fun createCollapseAnimation(child: ViewGroup): Animator
{
val translationYHolder = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, this.offset(child.resources))
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 0f)
val animators = child.children.mapIndexed { index, view ->
ObjectAnimator.ofPropertyValuesHolder(
view,
translationYHolder,
alphaHolder
).apply {
duration = DURATION
startDelay = index * DELAY
interpolator = AccelerateInterpolator()
}
}.toList()
return AnimatorSet().apply {
playTogether(animators)
}
}
}

View file

@ -24,6 +24,7 @@ import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
@ -34,6 +35,7 @@ import com.metallic.chiaki.common.ext.viewModelFactory
import com.metallic.chiaki.settings.SettingsActivity
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_test_start.*
class MainActivity : AppCompatActivity()
{
@ -51,13 +53,15 @@ class MainActivity : AppCompatActivity()
title = ""
setSupportActionBar(toolbar)
addButton.setOnClickListener {
Intent(this, TestStartActivity::class.java).also {
it.putExtra(TestStartActivity.EXTRA_REVEAL_X, addButton.x + addButton.width * 0.5f)
it.putExtra(TestStartActivity.EXTRA_REVEAL_Y, addButton.y + addButton.height * 0.5f)
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
}
floatingActionButton.setOnClickListener {
expandFloatingActionButton(!floatingActionButton.isExpanded)
}
floatingActionButtonDialBackground.setOnClickListener {
expandFloatingActionButton(false)
}
addManualButton.setOnClickListener { addManualConsole() }
addManualLabelButton.setOnClickListener { addManualConsole() }
viewModel = ViewModelProviders
.of(this, viewModelFactory { MainViewModel(getDatabase(this)) })
@ -73,24 +77,40 @@ class MainActivity : AppCompatActivity()
})
}
private fun expandFloatingActionButton(expand: Boolean)
{
floatingActionButton.isExpanded = expand
floatingActionButton.isActivated = floatingActionButton.isExpanded
}
override fun onDestroy()
{
super.onDestroy()
disposable.dispose()
}
override fun onResume()
override fun onStart()
{
super.onResume()
super.onStart()
viewModel.discoveryManager.resume()
}
override fun onPause()
override fun onStop()
{
super.onPause()
super.onStop()
viewModel.discoveryManager.pause()
}
override fun onBackPressed()
{
if(floatingActionButton.isExpanded)
{
expandFloatingActionButton(false)
return
}
super.onBackPressed()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean
{
menuInflater.inflate(R.menu.main, menu)
@ -125,4 +145,17 @@ class MainActivity : AppCompatActivity()
else -> super.onOptionsItemSelected(item)
}
private fun addManualConsole()
{
val parent = addManualButton.parent as View
val parentParent = parent.parent as View
val x = addManualButton.x + parent.x + parentParent.x + addManualButton.width * 0.5f
val y = addManualButton.y + parent.y + parentParent.y + addManualButton.height * 0.5f
Intent(this, TestStartActivity::class.java).also {
it.putExtra(TestStartActivity.EXTRA_REVEAL_X, x)
it.putExtra(TestStartActivity.EXTRA_REVEAL_Y, y)
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
}
}
}

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_add_close">
<target
android:name="add">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="45"/>
</aapt:attr>
</target>
<target
android:name="add_path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:propertyName="fillAlpha"
android:valueFrom="1"
android:valueTo="0"/>
</aapt:attr>
</target>
<target
android:name="close">
<aapt:attr name="android:animation">
<objectAnimator
android:startOffset="50"
android:duration="100"
android:propertyName="rotation"
android:valueFrom="-45"
android:valueTo="0"/>
</aapt:attr>
</target>
<target
android:name="close_path">
<aapt:attr name="android:animation">
<objectAnimator
android:startOffset="50"
android:duration="100"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"/>
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_add_close">
<target
android:name="close">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="-45"/>
</aapt:attr>
</target>
<target
android:name="close_path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:propertyName="fillAlpha"
android:valueFrom="1"
android:valueTo="0"/>
</aapt:attr>
</target>
<target
android:name="add">
<aapt:attr name="android:animation">
<objectAnimator
android:startOffset="50"
android:duration="100"
android:propertyName="rotation"
android:valueFrom="45"
android:valueTo="0"/>
</aapt:attr>
</target>
<target
android:name="add_path">
<aapt:attr name="android:animation">
<objectAnimator
android:startOffset="50"
android:duration="100"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"/>
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<group
android:name="add"
android:pivotX="12.0"
android:pivotY="12.0">
<path
android:name="add_path"
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</group>
<group
android:name="close"
android:pivotX="12.0"
android:pivotY="12.0">
<path
android:name="close_path"
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/close"
android:state_activated="true"
android:drawable="@drawable/ic_close">
</item>
<item
android:id="@+id/add"
android:drawable="@drawable/ic_add">
</item>
<transition
android:drawable="@drawable/avd_add_to_close"
android:fromId="@id/add"
android:toId="@id/close">
</transition>
<transition
android:drawable="@drawable/avd_close_to_add"
android:fromId="@id/close"
android:toId="@id/add"/>
</animated-selector>

View file

@ -3,15 +3,8 @@
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.floatingactionbutton.FloatingActionButton
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_add"
android:layout_margin="16dp"
android:layout_gravity="bottom|end" />
android:layout_height="match_parent"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hostsRecyclerView"
@ -22,6 +15,95 @@
android:clipChildren="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<FrameLayout
android:id="@+id/floatingActionButtonDialBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#44000000"
android:visibility="gone"
android:clickable="true"
app:layout_behavior=".main.FloatingActionButtonBackgroundBehavior"/>
<LinearLayout
android:id="@+id/floatingActionButtonDial"
android:layout_gravity="top|end"
android:visibility="invisible"
app:layout_anchor="@id/floatingActionButton"
app:layout_anchorGravity="top|end"
android:orientation="vertical"
android:layout_marginRight="20dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
app:layout_behavior=".main.FloatingActionButtonSpeedDialBehavior"
android:gravity="right">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/registerLabelButton"
style="@style/AppTheme.FloatingActionButtonSpeedDial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/action_register"
android:textStyle="bold"
app:rippleColor="@color/floating_action_button_speed_dial_tint"
app:cornerRadius="8dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/registerButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_register"
app:fabSize="mini"
style="@style/AppTheme.FloatingActionButtonSpeedDial" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/addManualLabelButton"
style="@style/AppTheme.FloatingActionButtonSpeedDial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/action_add_manual"
android:textStyle="bold"
app:rippleColor="@color/floating_action_button_speed_dial_tint"
app:cornerRadius="8dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/addManualButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_add"
app:fabSize="mini"
style="@style/AppTheme.FloatingActionButtonSpeedDial" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/selector_add_fab"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
app:fabSize="normal" />
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">

View file

@ -7,6 +7,9 @@
<color name="stream_text">@android:color/white</color>
<color name="stream_background">@android:color/black</color>
<color name="floating_action_button_speed_dial_background">#fafafa</color>
<color name="floating_action_button_speed_dial_tint">#333333</color>
<color name="control_primary">#22ffffff</color>
<color name="control_pressed">#88ffffff</color>
</resources>

View file

@ -3,4 +3,5 @@
<dimen name="control_face_button_size">48dp</dimen>
<dimen name="control_analog_stick_radius">64dp</dimen>
<dimen name="control_analog_stick_handle_radius">16dp</dimen>
<dimen name="floating_action_button_speed_dial_anim_offset">48dp</dimen>
</resources>

View file

@ -12,4 +12,6 @@
<string name="action_discover">Discover Consoles Automatically</string>
<string name="display_host_host">Address: %s</string>
<string name="display_host_id">ID: %s</string>
<string name="action_register">Register Console</string>
<string name="action_add_manual">Add Console Manually</string>
</resources>

View file

@ -12,6 +12,12 @@
<item name="android:windowActivityTransitions">true</item>
</style>
<style name="AppTheme.FloatingActionButtonSpeedDial">
<item name="tint">@color/floating_action_button_speed_dial_tint</item>
<item name="android:backgroundTint">@color/floating_action_button_speed_dial_background</item>
<item name="android:textColor">@color/floating_action_button_speed_dial_tint</item>
</style>
<style name="MageTheme" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>