├── .gitignore ├── README.md ├── art └── preview.gif ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bitvale │ │ └── fabdialogdemo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── bitvale │ │ │ └── fabdialogdemo │ │ │ ├── DataProvider.kt │ │ │ ├── DemoAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ └── extensions.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── android_icon.xml │ │ ├── filter_icon.xml │ │ ├── ic_launcher_background.xml │ │ └── splash_screen.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── filter_view.xml │ │ └── lang_item.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── bitvale │ └── fabdialogdemo │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── skycodetech │ │ └── fabdialog │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── bitvale │ │ │ └── fabdialog │ │ │ ├── common │ │ │ ├── BitmapUtil.kt │ │ │ ├── Constant.kt │ │ │ └── extensions.kt │ │ │ └── widget │ │ │ ├── FabDialog.kt │ │ │ ├── FabDialogOutlineProvider.kt │ │ │ └── FabDialogSettings.kt │ └── res │ │ ├── layout │ │ └── include_dialog_layout.xml │ │ ├── values-sw600dp │ │ └── dimen.xml │ │ ├── values-sw720dp-land │ │ └── dimen.xml │ │ ├── values-sw720dp │ │ └── dimen.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── skycodetech │ └── fabdialog │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | .idea 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FabDialog 2 | ================= 3 | 4 | sample 5 | 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | [![Platform](https://img.shields.io/badge/platform-android-green.svg)](http://developer.android.com/index.html) 8 | [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) 9 | 10 | This is an Android project allowing to animate a custom Floating Action Button into a custom Dialog. 11 | 12 | USAGE 13 | ----- 14 | 15 | Just add FabDialog view in your layout XML and FabDialog library in your project via Gradle: 16 | 17 | ```gradle 18 | dependencies { 19 | implementation 'com.bitvale:fabdialog:1.0.1' 20 | } 21 | ``` 22 | 23 | XML 24 | ----- 25 | 26 | ```xml 27 | 36 | ``` 37 | 38 | You must use the following properties in your XML to change your FabDialog. 39 | 40 | 41 | ##### Properties: 42 | 43 | * `app:fabBackgroundColor` (color) -> default ?attr/colorAccent 44 | * `app:dialogBackgroundColor` (color) -> default ?attr/colorBackgroundFloating 45 | * `app:dialogCornerRadius` (dimension) -> default 8dp 46 | * `app:dimBackgroundEnabled` (boolean) -> default true 47 | * `app:dimBackgroundColor` (color) -> default BLACK with transparency (#99000000) 48 | * `app:closeOnTouchOutside` (boolean) -> default true 49 | 50 | Kotlin 51 | ----- 52 | 53 | ```kotlin 54 | with(dialog_fab) { 55 | setTitle(R.string.dialog_title) 56 | setMessage(R.string.dialog_message) 57 | setDialogIcon(R.drawable.android_icon) 58 | setFabIcon(R.drawable.android_icon) 59 | setFabBackgroundColor(ContextCompat.getColor(context, R.color.fabColor)) 60 | setDialogBackgroundColor(ContextCompat.getColor(context, R.color.dialogColor)) 61 | setPositiveButton(R.string.positive_btn) { // some action } 62 | setNegativeButton(R.string.negative_btn) { dialog_fab.collapseDialog() } 63 | setOnClickListener { dialog_fab.expandDialog() } 64 | setListener(this@MainActivity) 65 | } 66 | ``` 67 | 68 | LICENCE 69 | ----- 70 | 71 | FabDialog by [Alexander Kolpakov](https://play.google.com/store/apps/dev?id=7044571013168957413) is licensed under a [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). -------------------------------------------------------------------------------- /art/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/art/preview.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.2.50' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.0-alpha03' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 27 9 | defaultConfig { 10 | applicationId "com.bitvale.fabdialogdemo" 11 | minSdkVersion 21 12 | targetSdkVersion 27 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | androidExtensions { 25 | experimental = true 26 | } 27 | } 28 | 29 | ext { 30 | supportLibVersion = '27.1.1' 31 | firebaseVersion = '16.0.0' 32 | retrofitLibVersion = '2.2.0' 33 | daggerVersion = '2.11' 34 | architectureComponentsVersion = '1.0.0-rc1' 35 | } 36 | 37 | 38 | dependencies { 39 | implementation fileTree(dir: 'libs', include: ['*.jar']) 40 | 41 | /*--Kotlin--*/ 42 | implementation"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 43 | 44 | /*--Support--*/ 45 | implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha1' 46 | implementation 'com.android.support:design:27.1.1' 47 | 48 | implementation project(':library') 49 | } 50 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /demo/src/androidTest/java/com/bitvale/fabdialogdemo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.bitvale.fabdialog", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/src/main/java/com/bitvale/fabdialogdemo/DataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | /** 4 | * Created by Alexander Kolpakov on 17.07.2018 5 | */ 6 | object DataProvider { 7 | 8 | fun getData(): List { 9 | val data = ArrayList() 10 | data.add(Lang(0, "Kotlin", "2018-03-22 10:57:41", 232)) 11 | data.add(Lang(1, "Android", "2017-01-16 10:57:41", 132)) 12 | data.add(Lang(2, "Python", "2018-04-17 10:57:41", 156)) 13 | data.add(Lang(3, "C++", "2018-06-15 10:57:41", 89)) 14 | data.add(Lang(4, "C#", "2018-02-16 10:57:41", 100)) 15 | data.add(Lang(5, "JavaScript", "2018-05-19 10:57:41", 120)) 16 | data.add(Lang(6, "Html", "2018-05-21 10:57:41", 185)) 17 | data.add(Lang(7, "PHP", "2018-04-16 10:57:41", 257)) 18 | data.add(Lang(8, "Ruby", "2018-04-17 10:57:41", 189)) 19 | data.add(Lang(9, "Swift", "2018-07-16 10:57:41", 145)) 20 | data.add(Lang(10, "Java", "2017-05-24 10:57:41", 178)) 21 | return data 22 | } 23 | 24 | data class Lang(val id: Int, val name: String, val date: String, val questions: Int) 25 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/bitvale/fabdialogdemo/DemoAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.bitvale.fabdialogdemo.DataProvider.Lang 7 | import kotlinx.android.extensions.LayoutContainer 8 | import kotlinx.android.synthetic.main.lang_item.* 9 | 10 | /** 11 | * Created by Alexander Kolpakov on 17.07.2018 12 | */ 13 | class DemoAdapter(var dataSet: List) : RecyclerView.Adapter() { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DemoViewHolder(parent) 16 | 17 | override fun getItemCount() = dataSet.size 18 | 19 | override fun onBindViewHolder(holder: DemoViewHolder, position: Int) = holder.bind(dataSet[position]) 20 | 21 | class DemoViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(parent.inflate(R.layout.lang_item)), 22 | LayoutContainer { 23 | 24 | override val containerView: View? 25 | get() = itemView 26 | 27 | val colors: IntArray = itemView.context.resources.getIntArray(R.array.colors) 28 | 29 | fun bind(item: Lang) { 30 | tv_name.text = item.name 31 | tv_questions.text = containerView?.context?.getString(R.string.questions, item.questions) 32 | img_background.setBackgroundColor(colors[item.id]) 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/bitvale/fabdialogdemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | import android.os.Bundle 4 | import android.support.v4.content.ContextCompat 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.util.DiffUtil 7 | import android.support.v7.widget.LinearLayoutManager 8 | import android.widget.SeekBar 9 | import android.widget.TextView 10 | import com.bitvale.fabdialog.widget.FabDialog 11 | import com.bitvale.fabdialogdemo.DataProvider.Lang 12 | import kotlinx.android.synthetic.main.activity_main.* 13 | 14 | /** 15 | * Created by Alexander Kolpakov on 16.07.2018 16 | */ 17 | class MainActivity : AppCompatActivity(), FabDialog.FabDialogListener { 18 | 19 | private var stateFlag = false 20 | private lateinit var dataSet: List 21 | private lateinit var recyclerAdapter: DemoAdapter 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | setTheme(R.style.AppTheme) 25 | super.onCreate(savedInstanceState) 26 | setContentView(R.layout.activity_main) 27 | dataSet = DataProvider.getData() 28 | initRecycler() 29 | savedInstanceState?.let { 30 | stateFlag = !it.getBoolean("filter") 31 | } 32 | initFabDialog() 33 | } 34 | 35 | private fun initRecycler() { 36 | with(recycler) { 37 | layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false) 38 | recyclerAdapter = DemoAdapter(dataSet) 39 | adapter = recyclerAdapter 40 | } 41 | } 42 | 43 | private fun initFabDialog() { 44 | if (stateFlag) { 45 | initFilter() 46 | } else { 47 | initDefault() 48 | } 49 | stateFlag = !stateFlag 50 | } 51 | 52 | private fun initDefault() { 53 | with(fab_dialog) { 54 | setUseDefaultView() 55 | setTitle(R.string.dialog_title) 56 | setMessage(R.string.dialog_message) 57 | setDialogIcon(R.drawable.android_icon) 58 | setFabBackgroundColor(ContextCompat.getColor(context, R.color.fabColor)) 59 | setPositiveButton(R.string.positive_btn) { 60 | this.collapseDialog() 61 | initFabDialog() 62 | } 63 | setFabIcon(R.drawable.android_icon) 64 | setNegativeButton(R.string.negative_btn) { this.collapseDialog() } 65 | setOnClickListener { this.expandDialog() } 66 | setListener(this@MainActivity) 67 | } 68 | } 69 | 70 | private fun initFilter() { 71 | var questionCount = 50 72 | with(fab_dialog) { 73 | setTitle(R.string.filter_dialog_title) 74 | setDialogIcon(R.drawable.filter_icon) 75 | setFabBackgroundColor(ContextCompat.getColor(context, R.color.filterFabColor)) 76 | setFabIcon(R.drawable.filter_icon) 77 | setPositiveButton(R.string.positive_btn) { 78 | updateRecyclerData(recyclerAdapter.dataSet, getFilteredDataSet(questionCount)) 79 | this.collapseDialog() 80 | initFabDialog() 81 | } 82 | setContentView(R.layout.filter_view) 83 | 84 | val seekBar = findDialogViewById(R.id.seek_bar) 85 | val seekBarProgress = findDialogViewById(R.id.tv_sb_progress) 86 | seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 87 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { 88 | questionCount = progress 89 | if (questionCount == 0) questionCount = 50 90 | seekBarProgress.text = questionCount.toString() 91 | } 92 | 93 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 94 | } 95 | 96 | override fun onStopTrackingTouch(seekBar: SeekBar?) { 97 | } 98 | }) 99 | seekBar.max = 250 100 | seekBar.progress = 50 101 | } 102 | } 103 | 104 | private fun updateRecyclerData(oldData: List, newData: List) { 105 | recyclerAdapter.dataSet = newData 106 | calculateDiff(oldData, newData) 107 | } 108 | 109 | private fun calculateDiff(oldData: List, newData: List) { 110 | DiffUtil.calculateDiff(object : DiffUtil.Callback() { 111 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = 112 | oldData[oldItemPosition].name == newData[newItemPosition].name 113 | 114 | override fun getOldListSize() = oldData.size 115 | 116 | override fun getNewListSize() = newData.size 117 | 118 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = 119 | oldData[oldItemPosition] == newData[newItemPosition] 120 | }).dispatchUpdatesTo(recycler.adapter) 121 | } 122 | 123 | private fun getFilteredDataSet(number: Int) = dataSet.filter { it.questions >= number } 124 | 125 | override fun onSaveInstanceState(outState: Bundle) { 126 | super.onSaveInstanceState(outState) 127 | outState.putBoolean("filter", stateFlag) 128 | } 129 | 130 | override fun onCollapsed() { 131 | // Do something 132 | } 133 | 134 | override fun onExpanded() { 135 | // Do something 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /demo/src/main/java/com/bitvale/fabdialogdemo/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | import android.support.annotation.LayoutRes 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | /** 9 | * Created by Alexander Kolpakov on 17.07.2018 10 | */ 11 | 12 | fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false): View { 13 | return LayoutInflater.from(context).inflate(layoutId, this, attachToRoot) 14 | } -------------------------------------------------------------------------------- /demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/android_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/filter_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 33 | 34 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/filter_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 24 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/lang_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 30 | 31 | 43 | 44 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4a525a 4 | #3b4147 5 | #7585f2 6 | #5768de 7 | #4a525a 8 | #df3d54 9 | 10 | 11 | #827f93 12 | #20bf55 13 | #3b9aee 14 | #f4a261 15 | #1ed2bb 16 | #778da9 17 | #c467f4 18 | #9bc53d 19 | #9f9fed 20 | #fc6177 21 | #bcbcbc 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 180dp 3 | 24dp 4 | 56dp 5 | 6 | 8dp 7 | 16dp 8 | 24dp 9 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FabDialog 3 | 4 | %d questions 5 | 6 | Hi! I am a FabDialog! 7 | Apply filter 8 | Tap OK and try to reopen me again. 9 | OK 10 | Cancel 11 | Select number of questions 12 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/src/test/java/com/bitvale/fabdialogdemo/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialogdemo 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvale/FabDialog/9b1bad769bb067ef2e8f258adf34293ce308b71e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 27 7 | defaultConfig { 8 | minSdkVersion 21 9 | targetSdkVersion 27 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 27 | implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha1' 28 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/skycodetech/fabdialog/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.skycodetech.fabdialog; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.skycodetech.fabdialog.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/common/BitmapUtil.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.common 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.graphics.drawable.Drawable 7 | import android.graphics.drawable.VectorDrawable 8 | import android.widget.ImageView 9 | 10 | 11 | object BitmapUtil { 12 | 13 | /** 14 | * Get bitmap from ImageView drawable 15 | */ 16 | fun getBitmapFromImageView(imageView: ImageView): Bitmap? { 17 | val drawable = imageView.drawable 18 | return if (drawable == null) null 19 | else when (drawable::class) { 20 | VectorDrawable::class -> getBitmapFromVector(drawable as VectorDrawable) 21 | BitmapDrawable::class -> (drawable as BitmapDrawable).bitmap 22 | else -> throw ClassCastException(drawable.toString() + " is not supported!") 23 | } 24 | } 25 | 26 | fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? { 27 | return if (drawable == null) null 28 | else when (drawable::class) { 29 | VectorDrawable::class -> getBitmapFromVector(drawable as VectorDrawable) 30 | BitmapDrawable::class -> (drawable as BitmapDrawable).bitmap 31 | else -> throw ClassCastException(drawable.toString() + " is not supported!") 32 | } 33 | } 34 | 35 | private fun getBitmapFromVector(vectorDrawable: VectorDrawable): Bitmap { 36 | val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, 37 | vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888) 38 | val canvas = Canvas(bitmap) 39 | vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) 40 | vectorDrawable.draw(canvas) 41 | return bitmap 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/common/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.common 2 | 3 | /** 4 | * Created by Alexander Kolpakov on 05.07.2018 5 | */ 6 | object Constant { 7 | const val COLLAPSED = 0 8 | const val PROGRESSING = 1 9 | const val EXPANDED = 2 10 | 11 | const val EXPAND_DURATION = 200L 12 | const val COLLAPSE_DURATION = 150L 13 | const val EXPAND_MOTION_DURATION = 300L 14 | const val COLLAPSE_MOTION_DURATION = 300L 15 | const val COLOR_ANIMATION_DURATION = 300L 16 | const val ELEVATION_ANIMATION_DURATION = 200L 17 | 18 | const val FAB_POSITION_PROPERTY = "fubPosition" 19 | const val DIALOG_SIZE_PROPERTY = "dialogSize" 20 | 21 | const val KEY_IS_EXPANDED = "key_is_expanded" 22 | const val KEY_FAB_STATE = "key_fab_state" 23 | } 24 | 25 | -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/common/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.common 2 | 3 | import android.support.annotation.DimenRes 4 | import android.view.View 5 | 6 | /** 7 | * Created by Alexander Kolpakov on 16.07.2018 8 | */ 9 | 10 | fun View.getFloatDimen(@DimenRes res: Int) = context.resources.getDimension(res) 11 | 12 | fun View.getIntDimen(@DimenRes res: Int) = context.resources.getDimensionPixelOffset(res) -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/widget/FabDialog.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.widget 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.ObjectAnimator 6 | import android.animation.ValueAnimator 7 | import android.annotation.SuppressLint 8 | import android.content.Context 9 | import android.graphics.* 10 | import android.graphics.drawable.Drawable 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.os.Parcelable 14 | import android.support.annotation.* 15 | import android.support.constraint.ConstraintLayout 16 | import android.support.v4.content.ContextCompat 17 | import android.util.AttributeSet 18 | import android.util.Log 19 | import android.view.LayoutInflater 20 | import android.view.MotionEvent 21 | import android.view.View 22 | import android.view.View.OnTouchListener 23 | import android.view.ViewGroup 24 | import android.view.animation.DecelerateInterpolator 25 | import android.widget.Button 26 | import android.widget.ImageView 27 | import android.widget.ScrollView 28 | import android.widget.TextView 29 | import com.bitvale.fabdialog.R 30 | import com.bitvale.fabdialog.common.BitmapUtil 31 | import com.bitvale.fabdialog.common.Constant.COLLAPSED 32 | import com.bitvale.fabdialog.common.Constant.COLLAPSE_DURATION 33 | import com.bitvale.fabdialog.common.Constant.COLLAPSE_MOTION_DURATION 34 | import com.bitvale.fabdialog.common.Constant.COLOR_ANIMATION_DURATION 35 | import com.bitvale.fabdialog.common.Constant.DIALOG_SIZE_PROPERTY 36 | import com.bitvale.fabdialog.common.Constant.ELEVATION_ANIMATION_DURATION 37 | import com.bitvale.fabdialog.common.Constant.EXPANDED 38 | import com.bitvale.fabdialog.common.Constant.EXPAND_DURATION 39 | import com.bitvale.fabdialog.common.Constant.EXPAND_MOTION_DURATION 40 | import com.bitvale.fabdialog.common.Constant.FAB_POSITION_PROPERTY 41 | import com.bitvale.fabdialog.common.Constant.KEY_FAB_STATE 42 | import com.bitvale.fabdialog.common.Constant.KEY_IS_EXPANDED 43 | import com.bitvale.fabdialog.common.Constant.PROGRESSING 44 | import com.bitvale.fabdialog.common.getFloatDimen 45 | import com.bitvale.fabdialog.common.getIntDimen 46 | 47 | /** 48 | * Created by Alexander Kolpakov on 05.07.2018 49 | */ 50 | class FabDialog : ConstraintLayout { 51 | 52 | interface FabDialogListener { 53 | fun onCollapsed() 54 | fun onExpanded() 55 | } 56 | 57 | private var state = COLLAPSED 58 | 59 | private val settings = FabDialogSettings() 60 | private var defElevation = 0f 61 | private val paint = Paint(Paint.ANTI_ALIAS_FLAG) 62 | private val circlePath = Path() 63 | private var fubIconBitmap: Bitmap? = null 64 | 65 | private var rectF: RectF = RectF(0f, 0f, 0f, 0f) 66 | private var helperRectF: RectF = RectF(0f, 0f, 0f, 0f) 67 | 68 | private var radius = 0f 69 | 70 | private var expandPath = Path() 71 | private var collapsePath = Path() 72 | 73 | private var dialogHeight = 0 74 | private var dialogWidth = 0 75 | 76 | private var drawFabIcon = true 77 | 78 | private var dimView: View? = null 79 | private var contentView: View? = null 80 | 81 | @Dimension 82 | private var dialogCornerRadius = 0f 83 | @ColorInt 84 | private var dialogBackgroundColor = 0 85 | @ColorInt 86 | private var fabBackgroundColor = 0 87 | @BoolRes 88 | private var dimBackgroundEnabled = true 89 | @ColorInt 90 | private var dimBackgroundColor = 0 91 | @BoolRes 92 | private var closeOnTouchOutside = true 93 | 94 | private var fabDialogListener: FabDialogListener? = null 95 | 96 | fun setListener(listener: FabDialogListener) { 97 | this.fabDialogListener = listener 98 | } 99 | 100 | constructor(context: Context) : this(context, null) 101 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 102 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 103 | 104 | setCustomDialogView(R.layout.include_dialog_layout) 105 | 106 | attrs?.let { retrieveAttributes(attrs, defStyleAttr) } 107 | 108 | defElevation = getFloatDimen(R.dimen.elevation) 109 | elevation = defElevation 110 | 111 | setBackgroundColor(Color.TRANSPARENT) 112 | isClickable = true 113 | initOnTouchListener() 114 | } 115 | 116 | private fun retrieveAttributes(attrs: AttributeSet, defStyleAttr: Int) { 117 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FabDialog, defStyleAttr, R.style.FabDialog) 118 | 119 | dialogBackgroundColor = typedArray.getColor(R.styleable.FabDialog_dialogBackgroundColor, 0) 120 | fabBackgroundColor = typedArray.getColor(R.styleable.FabDialog_fabBackgroundColor, 0) 121 | dialogCornerRadius = typedArray.getDimension(R.styleable.FabDialog_dialogCornerRadius, 0f) 122 | dimBackgroundColor = typedArray.getColor(R.styleable.FabDialog_dimBackgroundColor, 0) 123 | dimBackgroundEnabled = typedArray.getBoolean(R.styleable.FabDialog_dimBackgroundEnabled, true) 124 | closeOnTouchOutside = typedArray.getBoolean(R.styleable.FabDialog_closeOnTouchOutside, true) 125 | 126 | paint.color = fabBackgroundColor 127 | 128 | val src = typedArray.getDrawable(R.styleable.FabDialog_fabIcon) 129 | fubIconBitmap = BitmapUtil.getBitmapFromDrawable(src) 130 | 131 | typedArray.recycle() 132 | } 133 | 134 | private fun calculateDialogSize() { 135 | val windowVerticalPadding = getIntDimen(R.dimen.dialog_vertical_margin) 136 | val windowHorizontalPadding = getIntDimen(R.dimen.dialog_horizontal_margin) 137 | val windowHeight = resources.displayMetrics.heightPixels 138 | val windowWidth = resources.displayMetrics.widthPixels 139 | 140 | val maxDialogWidth = getIntDimen(R.dimen.dialog_max_width) 141 | val calculatedWidth = windowWidth - windowHorizontalPadding * 2 142 | dialogWidth = Math.min(calculatedWidth, maxDialogWidth) 143 | 144 | val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(dialogWidth, View.MeasureSpec.AT_MOST) 145 | val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 146 | contentView?.measure(widthMeasureSpec, heightMeasureSpec) 147 | 148 | val calculatedHeight = contentView?.measuredHeight as Int 149 | 150 | maxHeight = windowHeight - windowVerticalPadding * 2 151 | dialogHeight = Math.min(calculatedHeight, maxHeight) 152 | 153 | dialogHeight = if (dialogHeight % 2 == 0) dialogHeight else dialogHeight + 1 // prevent incorrect outline shadow with odd height 154 | } 155 | 156 | /** 157 | * Expand dialog with animation. 158 | */ 159 | fun expandDialog() { 160 | initPath() 161 | startExpandMotion() 162 | } 163 | 164 | /** 165 | * Collapse dialog with animation. 166 | */ 167 | fun collapseDialog() { 168 | collapse() 169 | } 170 | 171 | private fun startExpandMotion() { 172 | ObjectAnimator.ofMultiFloat(this, FAB_POSITION_PROPERTY, expandPath).apply { 173 | duration = EXPAND_MOTION_DURATION 174 | interpolator = DecelerateInterpolator() 175 | addUpdateListener { 176 | invalidate() 177 | } 178 | addListener(object : AnimatorListenerAdapter() { 179 | override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { 180 | state = PROGRESSING 181 | animateElevation(elevation, 0f) 182 | } 183 | 184 | override fun onAnimationEnd(animation: Animator?) { 185 | expand() 186 | } 187 | }) 188 | start() 189 | } 190 | } 191 | 192 | private fun expand() { 193 | val width = width.toFloat() 194 | val height = height.toFloat() 195 | val toX = dialogWidth / 2 - width / 2 196 | val toY = dialogHeight / 2 - height / 2 197 | val toRadius = radius - dialogCornerRadius 198 | val animatedValues = arrayOf(floatArrayOf(0f, 0f, 0f), floatArrayOf(toX, toY, toRadius)) 199 | 200 | ObjectAnimator.ofMultiFloat(this, DIALOG_SIZE_PROPERTY, animatedValues).apply { 201 | duration = EXPAND_DURATION 202 | interpolator = DecelerateInterpolator() 203 | 204 | addUpdateListener { 205 | invalidate() 206 | } 207 | 208 | addListener(object : AnimatorListenerAdapter() { 209 | override fun onAnimationStart(animation: Animator?) { 210 | showDimBackground() 211 | animateBackgroundColor(fabBackgroundColor, dialogBackgroundColor) 212 | settings.currentWidth = dialogWidth.toFloat() 213 | settings.currentHeight = dialogHeight.toFloat() 214 | 215 | drawFabIcon = false 216 | 217 | val r = toX + width 218 | val b = toY + height 219 | 220 | layoutParams.width = dialogWidth 221 | layoutParams.height = dialogHeight 222 | 223 | rectF.set(toX, toY, r, b) 224 | helperRectF.set(toX, toY, r, b) 225 | 226 | setupCoordinates() 227 | requestLayout() 228 | } 229 | 230 | override fun onAnimationEnd(animation: Animator?) { 231 | contentView?.visibility = View.VISIBLE 232 | setupPadding() 233 | updateOutline() 234 | animateElevation(0f, defElevation) 235 | state = EXPANDED 236 | fabDialogListener?.onExpanded() 237 | } 238 | }) 239 | start() 240 | } 241 | } 242 | 243 | private fun collapse() { 244 | val width = settings.width.toFloat() 245 | val height = settings.height.toFloat() 246 | val toX = -(dialogWidth / 2 - width / 2) 247 | val toY = -(dialogHeight / 2 - height / 2) 248 | val toRadius = dialogCornerRadius - settings.radius 249 | val animatedValues = arrayOf(floatArrayOf(0f, 0f, 0f), floatArrayOf(toX, toY, toRadius)) 250 | 251 | ObjectAnimator.ofMultiFloat(this, DIALOG_SIZE_PROPERTY, animatedValues) 252 | .apply { 253 | duration = COLLAPSE_DURATION 254 | interpolator = DecelerateInterpolator() 255 | addUpdateListener { 256 | invalidate() 257 | } 258 | addListener(object : AnimatorListenerAdapter() { 259 | override fun onAnimationStart(animation: Animator?) { 260 | state = PROGRESSING 261 | removeDimBackground() 262 | settings.currentWidth = width 263 | settings.currentHeight = height 264 | elevation = 0f 265 | contentView?.visibility = View.INVISIBLE 266 | drawFabIcon = true 267 | helperRectF.set(rectF) 268 | } 269 | 270 | override fun onAnimationEnd(animation: Animator?) { 271 | layoutParams.width = width.toInt() 272 | layoutParams.height = height.toInt() 273 | rectF.set(0f, 0f, width, height) 274 | setupCoordinates() 275 | requestLayout() 276 | setupPadding() 277 | updateOutline() 278 | startCollapseMotion() 279 | } 280 | }) 281 | start() 282 | } 283 | } 284 | 285 | private fun startCollapseMotion() { 286 | ObjectAnimator.ofMultiFloat(this, FAB_POSITION_PROPERTY, collapsePath).apply { 287 | duration = COLLAPSE_MOTION_DURATION 288 | interpolator = DecelerateInterpolator() 289 | addUpdateListener { 290 | invalidate() 291 | } 292 | addListener(object : AnimatorListenerAdapter() { 293 | override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { 294 | animateElevation(0f, defElevation) 295 | } 296 | 297 | override fun onAnimationEnd(animation: Animator?) { 298 | fabDialogListener?.onCollapsed() 299 | animateBackgroundColor(dialogBackgroundColor, fabBackgroundColor) 300 | state = COLLAPSED 301 | fabDialogListener?.onCollapsed() 302 | } 303 | }) 304 | start() 305 | } 306 | } 307 | 308 | @SuppressLint("ObjectAnimatorBinding") 309 | @SuppressWarnings("unused") 310 | private fun setFubPosition(x: Float, y: Float) { 311 | this.x = x 312 | this.y = y 313 | } 314 | 315 | @SuppressLint("ObjectAnimatorBinding") 316 | @SuppressWarnings("unused") 317 | private fun setDialogSize(x: Float, y: Float, radius: Float) { 318 | val left = helperRectF.left - x 319 | val right = helperRectF.right + x 320 | val top = helperRectF.top - y 321 | val bottom = helperRectF.bottom + y 322 | this.radius = Math.abs(settings.radius - radius) 323 | rectF.set(left, top, right, bottom) 324 | } 325 | 326 | private fun animateBackgroundColor(@ColorInt fromColor: Int, @ColorInt toColor: Int) { 327 | if (dialogBackgroundColor != fabBackgroundColor) { 328 | ValueAnimator.ofArgb(fromColor, toColor).apply { 329 | duration = COLOR_ANIMATION_DURATION 330 | addUpdateListener { 331 | paint.color = animatedValue as Int 332 | invalidate() 333 | } 334 | start() 335 | } 336 | } 337 | } 338 | 339 | private fun showDimBackground() { 340 | if (!dimBackgroundEnabled && !closeOnTouchOutside) return 341 | if (dimView == null) dimView = View(context) 342 | val parent = parent as ViewGroup 343 | val lp = ViewGroup.LayoutParams(settings.maxWidth, settings.maxHeight) 344 | if (dimBackgroundEnabled) dimView?.setBackgroundColor(dimBackgroundColor) 345 | if (closeOnTouchOutside) dimView?.setOnClickListener { collapseDialog() } 346 | dimView?.layoutParams = lp 347 | parent.addView(dimView) 348 | } 349 | 350 | private fun removeDimBackground() { 351 | (parent as ViewGroup).removeView(dimView) 352 | } 353 | 354 | private fun setupCoordinates() { 355 | addOnLayoutChangeListener(object : OnLayoutChangeListener { 356 | override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { 357 | removeOnLayoutChangeListener(this) 358 | val screenCenterX = settings.maxWidth / 2f 359 | val screenCenterY = settings.parentHeight / 2.2f 360 | x = screenCenterX - settings.currentWidth / 2 361 | y = screenCenterY - settings.currentHeight / 2 362 | } 363 | }) 364 | } 365 | 366 | private fun setupPadding() { 367 | val padding = if (state == COLLAPSED) getIntDimen(R.dimen.fab_padding) else 0 368 | setPadding(padding, padding, padding, padding) 369 | } 370 | 371 | private fun updateOutline() { 372 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 373 | val outlineWidth = settings.currentWidth.toInt() 374 | val outlineHeight = settings.currentHeight.toInt() 375 | if (outlineProvider !is FabDialogOutlineProvider) { 376 | outlineProvider = FabDialogOutlineProvider(outlineWidth, outlineHeight, radius) 377 | } else { 378 | (outlineProvider as FabDialogOutlineProvider).apply { 379 | currentWidth = outlineWidth 380 | currentHeight = outlineHeight 381 | currentRadius = radius 382 | } 383 | invalidateOutline() 384 | } 385 | } 386 | } 387 | 388 | private fun initPath() { 389 | expandPath.reset() 390 | val screenCenterX = settings.maxWidth / 2f 391 | val screenCenterY = settings.parentHeight / 2.2f 392 | expandPath.moveTo(x, y) 393 | var x1 = screenCenterX - width / 2 394 | var y1 = y 395 | var x2 = screenCenterX - width / 2 396 | var y2 = screenCenterY - height / 2 397 | expandPath.quadTo(x1, y1, x2, y2) 398 | 399 | collapsePath.reset() 400 | collapsePath.moveTo(screenCenterX - width / 2, screenCenterY - height / 2) 401 | x1 = x 402 | y1 = screenCenterY - height / 2 403 | x2 = settings.x 404 | y2 = settings.y 405 | collapsePath.quadTo(x1, y1, x2, y2) 406 | } 407 | 408 | private fun animateElevation(from: Float, to: Float, delay: Long = ELEVATION_ANIMATION_DURATION) { 409 | ValueAnimator.ofFloat(from, to).apply { 410 | addUpdateListener { elevation = it.animatedValue as Float } 411 | duration = delay 412 | }.start() 413 | } 414 | 415 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 416 | super.onSizeChanged(w, h, oldw, oldh) 417 | if (!settings.isInitialized) { 418 | rectF.set(0f, 0f, w.toFloat(), h.toFloat()) 419 | settings.initialize(this) 420 | radius = settings.radius 421 | updateOutline() 422 | } 423 | } 424 | 425 | override fun onDraw(canvas: Canvas?) { 426 | circlePath.addRoundRect(rectF, radius, radius, Path.Direction.CCW) 427 | canvas?.drawPath(circlePath, paint) 428 | if (drawFabIcon) { 429 | drawBitmap(canvas) 430 | } 431 | circlePath.reset() 432 | } 433 | 434 | private fun drawBitmap(canvas: Canvas?) { 435 | fubIconBitmap?.let { 436 | val w = settings.bmpRect.width() 437 | val h = settings.bmpRect.height() 438 | settings.bmpRect.left = rectF.centerX().toInt() - w / 2 439 | settings.bmpRect.top = rectF.centerY().toInt() - h / 2 440 | settings.bmpRect.right = rectF.centerX().toInt() + w / 2 441 | settings.bmpRect.bottom = rectF.centerY().toInt() + h / 2 442 | canvas?.drawBitmap(fubIconBitmap, null, settings.bmpRect, null) 443 | } 444 | } 445 | 446 | private fun initOnTouchListener() { 447 | setOnTouchListener(OnTouchListener { v, event -> 448 | if (state != COLLAPSED) return@OnTouchListener true 449 | 450 | when (event?.action) { 451 | MotionEvent.ACTION_DOWN -> { 452 | v.scaleX = 0.99f 453 | v.scaleY = 0.99f 454 | v?.z = elevation / 2 455 | } 456 | MotionEvent.ACTION_UP -> { 457 | v.scaleX = 1.0f 458 | v.scaleY = 1.0f 459 | v?.z = elevation 460 | } 461 | } 462 | false 463 | }) 464 | } 465 | 466 | /** 467 | * Set the title to display on default dialog layout. 468 | */ 469 | fun setTitle(title: String) { 470 | contentView?.findViewById(R.id.tv_dialog_title)?.text = title 471 | } 472 | 473 | /** 474 | * Set the title to display using the given resource id. 475 | */ 476 | fun setTitle(@StringRes titleId: Int) { 477 | val title = context.getString(titleId) 478 | setTitle(title) 479 | } 480 | 481 | /** 482 | * Set the message to display on default dialog layout. 483 | */ 484 | fun setMessage(message: String) { 485 | contentView?.findViewById(R.id.tv_dialog_msg)?.text = message 486 | calculateDialogSize() 487 | } 488 | 489 | /** 490 | * Set the message to display using the given resource id. 491 | */ 492 | fun setMessage(@StringRes messageId: Int) { 493 | val msg = context.getString(messageId) 494 | setMessage(msg) 495 | } 496 | 497 | /** 498 | * Set the icon to display on default dialog layout. 499 | * 500 | * * @param icon The drawable of the icon 501 | */ 502 | fun setDialogIcon(icon: Drawable?) { 503 | getIconImageView()?.setImageDrawable(icon) 504 | } 505 | 506 | /** 507 | * Set the icon to display on default dialog layout. 508 | * 509 | * @param icon The drawable resource id of the icon 510 | */ 511 | fun setDialogIcon(@DrawableRes icon: Int) { 512 | getIconImageView()?.setImageResource(icon) 513 | } 514 | 515 | private fun getIconImageView(): ImageView? { 516 | val imageView = contentView?.findViewById(R.id.img_icon) 517 | imageView?.visibility = View.VISIBLE 518 | return imageView 519 | } 520 | 521 | /** 522 | * Set the icon to display on fab. 523 | * 524 | * * @param icon The drawable of the icon 525 | */ 526 | fun setFabIcon(icon: Drawable?) { 527 | fubIconBitmap = BitmapUtil.getBitmapFromDrawable(icon) 528 | if (state == COLLAPSED) invalidate() 529 | } 530 | 531 | /** 532 | * Set the icon to display on fab. 533 | * 534 | * @param icon The drawable resource id of the icon 535 | */ 536 | fun setFabIcon(@DrawableRes icon: Int) { 537 | val drawable = ContextCompat.getDrawable(context, icon) 538 | setFabIcon(drawable) 539 | } 540 | 541 | /** 542 | * Set an action to be invoked when the positive button of the dialog is pressed. 543 | * 544 | * @param textId The resource id of the text to display in the positive button 545 | * @param action The action to be invoked. 546 | */ 547 | fun setPositiveButton(@StringRes textId: Int, action: () -> Unit) { 548 | setPositiveButton(context.getText(textId), action) 549 | } 550 | 551 | /** 552 | * Set an action to be invoked when the negative button of the dialog is pressed. 553 | * 554 | * @param textId The resource id of the text to display in the negative button 555 | * @param action The action to be invoked. 556 | */ 557 | fun setNegativeButton(@StringRes textId: Int, action: () -> Unit) { 558 | setNegativeButton(context.getText(textId), action) 559 | } 560 | 561 | /** 562 | * Set an action to be invoked when the positive button of the dialog is pressed. 563 | * 564 | *   565 | * 566 | * For working with custom layout just provide a button with id: 567 | * ``` 568 | * android:id="@+id/btn_positive" 569 | * ``` 570 | * @param text The text to display in thepositive button 571 | * @param action The action to be invoked. 572 | */ 573 | fun setPositiveButton(text: CharSequence, action: () -> Unit) { 574 | val button = findViewById(R.id.btn_positive) 575 | button?.text = text 576 | button?.setOnClickListener { action() } 577 | } 578 | 579 | /** 580 | * Set an action to be invoked when the negative button of the dialog is pressed. 581 | * 582 | *   583 | * 584 | * For working with custom layout just provide a button with id: 585 | * ``` 586 | * android:id="@+id/btn_negative" 587 | * ``` 588 | * @param text The text to display in the negative button 589 | * @param action The action to be invoked. 590 | */ 591 | fun setNegativeButton(text: CharSequence, action: () -> Unit) { 592 | val button = findViewById(R.id.btn_negative) 593 | button?.text = text 594 | button?.setOnClickListener { action() } 595 | } 596 | 597 | /** 598 | * Set the dialog content from a layout resource. The resource will be 599 | * 600 | * inflated and added to dialog top-view. This method has no effect if called 601 | * 602 | * after [expandDialog] 603 | * 604 | * @param layoutResID Resource ID to be inflated. 605 | */ 606 | fun setContentView(@LayoutRes layoutResID: Int) { 607 | if (state == EXPANDED) return 608 | val scrollView = findViewById(R.id.sv_msg) 609 | scrollView.visibility = View.GONE 610 | val customPanel = findViewById(R.id.custom_panel) 611 | customPanel.removeAllViews() 612 | LayoutInflater.from(context).inflate(layoutResID, customPanel) 613 | calculateDialogSize() 614 | } 615 | 616 | /** 617 | * Set the dialog view from a layout resource. The resource will be 618 | * 619 | * inflated and added to dialog as a top-view and replace default view 620 | * 621 | * with title, buttons and message. This method has no effect if called 622 | * 623 | * after [expandDialog] 624 | * 625 | *   626 | * 627 | * IMPORTANT: the top-level view of the inflated layout resource must be with id: 628 | * ``` 629 | * android:id="@+id/dialog_content" 630 | * ``` 631 | * @param layoutResID Resource ID to be inflated. 632 | */ 633 | fun setCustomDialogView(@LayoutRes layoutResID: Int) { 634 | if (state == EXPANDED) return 635 | removeAllViews() 636 | LayoutInflater.from(context).inflate(layoutResID, this) 637 | contentView = findViewById(R.id.dialog_content) 638 | calculateDialogSize() 639 | contentView?.visibility = View.INVISIBLE 640 | } 641 | 642 | /** 643 | * Set the default dialog layout resource as the top view. 644 | */ 645 | fun setUseDefaultView() = setCustomDialogView(R.layout.include_dialog_layout) 646 | 647 | /** 648 | * Finds a view that was identified by the `android:id` XML attribute 649 | * 650 | * @param id the ID to search for 651 | * @return a view with given ID if found, or `null` otherwise 652 | */ 653 | @Nullable 654 | fun findDialogViewById(@IdRes id: Int): T { 655 | return findViewById(id) 656 | } 657 | 658 | /** 659 | * Sets the background color for dialog. 660 | * 661 | * @param color the color of the background 662 | */ 663 | fun setDialogBackgroundColor(@ColorInt color: Int) { 664 | dialogBackgroundColor = color 665 | } 666 | 667 | /** 668 | * Sets the background color for fab. 669 | * 670 | * @param color the color of the background 671 | */ 672 | fun setFabBackgroundColor(@ColorInt color: Int) { 673 | fabBackgroundColor = color 674 | if (state == COLLAPSED) { 675 | paint.color = color 676 | invalidate() 677 | } 678 | } 679 | 680 | /** 681 | * Sets the background color for dialog dim background. 682 | * 683 | * @param color the color of the background 684 | */ 685 | fun setDimBackgroundColor(@ColorInt color: Int) { 686 | dimBackgroundColor = color 687 | } 688 | 689 | /** 690 | * Sets whether this dialog should be show dim background. Default is true. 691 | * 692 | * @param isEnabled Whether the dialog should be show dim background. 693 | */ 694 | fun setDimBackgroundEnabled(isEnabled: Boolean) { 695 | dimBackgroundEnabled = isEnabled 696 | } 697 | 698 | /** 699 | * Sets the dialog corner radius. 700 | * 701 | * @param radius for dialog corner 702 | */ 703 | fun setDialogCornerRadius(@Dimension radius: Float) { 704 | dialogCornerRadius = radius 705 | } 706 | 707 | /** 708 | * Sets whether this dialog is canceled when touched outside the window's 709 | * bounds. If setting to true, the dialog is set to be cancelable if not 710 | * already set. 711 | * 712 | * @param cancel Whether the dialog should be canceled when touched outside 713 | * the window. 714 | */ 715 | fun setCanceledOnTouchOutside(cancel: Boolean) { 716 | closeOnTouchOutside = cancel 717 | } 718 | 719 | /** 720 | * @return Whether the dialog is currently expanded. 721 | */ 722 | fun isExpanded() = state == EXPANDED 723 | 724 | override fun onSaveInstanceState(): Parcelable { 725 | super.onSaveInstanceState() 726 | return Bundle().apply { 727 | putBoolean(KEY_IS_EXPANDED, state == EXPANDED) 728 | putParcelable(KEY_FAB_STATE, super.onSaveInstanceState()) 729 | } 730 | } 731 | 732 | override fun onRestoreInstanceState(state: Parcelable?) { 733 | if (state is Bundle) { 734 | super.onRestoreInstanceState(state.getParcelable(KEY_FAB_STATE)) 735 | val isExpanded = state.getBoolean(KEY_IS_EXPANDED) 736 | if (isExpanded) { 737 | (parent as ViewGroup).addOnLayoutChangeListener(object : OnLayoutChangeListener { 738 | override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { 739 | v?.removeOnLayoutChangeListener(this) 740 | forceExpand() 741 | } 742 | }) 743 | } 744 | } 745 | } 746 | 747 | private fun forceExpand() { 748 | initPath() 749 | 750 | state = EXPANDED 751 | 752 | rectF.set(0f, 0f, dialogWidth.toFloat(), dialogHeight.toFloat()) 753 | radius = dialogCornerRadius 754 | paint.color = dialogBackgroundColor 755 | drawFabIcon = false 756 | 757 | settings.currentWidth = dialogWidth.toFloat() 758 | settings.currentHeight = dialogHeight.toFloat() 759 | layoutParams.width = dialogWidth 760 | layoutParams.height = dialogHeight 761 | 762 | contentView?.visibility = View.VISIBLE 763 | 764 | setupPadding() 765 | updateOutline() 766 | showDimBackground() 767 | setupCoordinates() 768 | requestLayout() 769 | } 770 | } -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/widget/FabDialogOutlineProvider.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.widget 2 | 3 | import android.graphics.Outline 4 | import android.view.View 5 | import android.view.ViewOutlineProvider 6 | 7 | /** 8 | * Created by Alexander Kolpakov on 15.07.2018 9 | */ 10 | class FabDialogOutlineProvider(width: Int, height: Int, radius: Float) : ViewOutlineProvider() { 11 | 12 | var currentWidth = width 13 | var currentHeight = height 14 | var currentRadius = radius 15 | 16 | override fun getOutline(view: View, outline: Outline) { 17 | outline.setRoundRect(0, 0, currentWidth, currentHeight, currentRadius) 18 | } 19 | } -------------------------------------------------------------------------------- /library/src/main/java/com/bitvale/fabdialog/widget/FabDialogSettings.kt: -------------------------------------------------------------------------------- 1 | package com.bitvale.fabdialog.widget 2 | 3 | import android.graphics.PointF 4 | import android.graphics.Rect 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | 9 | /** 10 | * Created by Alexander Kolpakov on 06.07.2018 11 | */ 12 | class FabDialogSettings { 13 | 14 | var x = 0f 15 | var y = 0f 16 | var translationX = 0f 17 | var translationY = 0f 18 | var width = 0 19 | var height = 0 20 | var maxWidth = 0 21 | var maxHeight = 0 22 | var radius = 0f 23 | var animRadius = 0f 24 | var minDimension = 0f 25 | var center = PointF(0f, 0f) 26 | var isInitialized = false 27 | val bmpRect = Rect(0, 0, 0, 0) 28 | var currentWidth = 0f 29 | var currentHeight = 0f 30 | var parentHeight = 0 31 | 32 | fun initialize(view: View) { 33 | x = view.x 34 | y = view.y 35 | translationX = view.translationX 36 | translationY = view.translationY 37 | width = view.width 38 | height = view.height 39 | center.x = width / 2f 40 | center.y = height / 2f 41 | minDimension = Math.min(width.toFloat(), height.toFloat()) 42 | radius = minDimension / 2f 43 | animRadius = radius 44 | bmpRect.left = view.paddingLeft 45 | bmpRect.top = view.paddingTop 46 | bmpRect.right = view.right - view.left - view.paddingRight 47 | bmpRect.bottom = view.bottom - view.top - view.paddingBottom 48 | maxWidth = view.resources.displayMetrics.widthPixels 49 | maxHeight = view.resources.displayMetrics.heightPixels 50 | isInitialized = true 51 | parentHeight = (view.parent as ViewGroup).height 52 | currentHeight = height.toFloat() 53 | currentWidth = width.toFloat() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /library/src/main/res/layout/include_dialog_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 25 | 26 | 35 | 36 | 46 | 47 | 48 | 56 | 57 | 65 | 66 | 67 | 68 | 71 | 72 | 81 | 82 |