feat: replace Toaster with Flashbar

This commit is contained in:
Akash Yadav
2023-01-18 22:45:38 +05:30
parent cbec897275
commit 97d8f66f2d
66 changed files with 3431 additions and 538 deletions
+1
View File
@@ -0,0 +1 @@
/build
+21
View File
@@ -0,0 +1,21 @@
# Flashbar
Based on [Flashbar](https://github.com/aritraroy/Flashbar).
## License
```
Copyright 2016 aritraroy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
+33
View File
@@ -0,0 +1,33 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE 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.
*
* AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
namespace = "com.itsaky.androidide.flashbar"
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.annotation)
implementation(libs.androidx.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.google.material)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE 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.
~
~ AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<manifest />
@@ -0,0 +1,674 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE 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.
*
* AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.flashbar
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.widget.ImageView.ScaleType
import android.widget.ImageView.ScaleType.CENTER_CROP
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.LEFT
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.RIGHT
import com.itsaky.androidide.flashbar.anim.FlashAnim
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
private const val DEFAULT_SHADOW_STRENGTH = 4
private const val DEFAUT_ICON_SCALE = 1.0f
@Suppress("UNUSED", "MemberVisibilityCanBePrivate")
class Flashbar private constructor(private var builder: Builder) {
private lateinit var flashbarContainerView: FlashbarContainerView
private lateinit var flashbarView: FlashbarView
/** Shows a flashbar */
fun show() {
flashbarContainerView.show(builder.activity)
}
/** Dismisses a flashbar */
fun dismiss() {
flashbarContainerView.dismiss()
}
/**
* Returns true/false depending on whether the flashbar is showing or not This represents the
* partial appearance of the flashbar
*/
fun isShowing() = flashbarContainerView.isBarShowing()
/**
* Returns true/false depending on whether the flashbar has been completely shown or not This
* represents the complete appearance of the flashbar
*/
fun isShown() = flashbarContainerView.isBarShown()
private fun construct() {
flashbarContainerView = FlashbarContainerView(builder.activity)
flashbarContainerView.adjustOrientation(builder.activity)
flashbarContainerView.addParent(this)
flashbarView = FlashbarView(builder.activity)
flashbarView.init(builder.gravity, builder.castShadow, builder.shadowStrength)
flashbarView.adjustWitPositionAndOrientation(builder.activity, builder.gravity)
flashbarView.addParent(flashbarContainerView)
flashbarContainerView.attach(flashbarView)
initializeContainerDecor()
initializeBarDecor()
flashbarContainerView.construct()
}
private fun initializeContainerDecor() {
with(flashbarContainerView) {
setDuration(builder.duration)
setBarShowListener(builder.onBarShowListener)
setBarDismissListener(builder.onBarDismissListener)
setBarDismissOnTapOutside(builder.barDismissOnTapOutside)
setOnTapOutsideListener(builder.onTapOutsideListener)
setOverlay(builder.overlay)
setOverlayColor(builder.overlayColor)
setOverlayBlockable(builder.overlayBlockable)
setVibrationTargets(builder.vibrationTargets)
setIconAnim(builder.iconAnimBuilder)
setEnterAnim(builder.enterAnimBuilder!!)
setExitAnim(builder.exitAnimBuilder!!)
enableSwipeToDismiss(builder.enableSwipeToDismiss)
}
}
private fun initializeBarDecor() {
with(flashbarView) {
setBarBackgroundColor(builder.backgroundColor)
setBarBackgroundDrawable(builder.backgroundDrawable)
setBarTapListener(builder.onBarTapListener)
setTitle(builder.title)
setTitleSpanned(builder.titleSpanned)
setTitleTypeface(builder.titleTypeface)
setTitleSizeInPx(builder.titleSizeInPx)
setTitleSizeInSp(builder.titleSizeInSp)
setTitleColor(builder.titleColor)
setTitleAppearance(builder.titleAppearance)
setMessage(builder.message)
setMessageSpanned(builder.messageSpanned)
setMessageTypeface(builder.messageTypeface)
setMessageSizeInPx(builder.messageSizeInPx)
setMessageSizeInSp(builder.messageSizeInSp)
setMessageColor(builder.messageColor)
setMessageAppearance(builder.messageAppearance)
setPrimaryActionText(builder.primaryActionText)
setPrimaryActionTextSpanned(builder.primaryActionTextSpanned)
setPrimaryActionTextTypeface(builder.primaryActionTextTypeface)
setPrimaryActionTextSizeInPx(builder.primaryActionTextSizeInPx)
setPrimaryActionTextSizeInSp(builder.primaryActionTextSizeInSp)
setPrimaryActionTextColor(builder.primaryActionTextColor)
setPrimaryActionTextAppearance(builder.primaryActionTextAppearance)
setPrimaryActionTapListener(builder.onPrimaryActionTapListener)
setPositiveActionText(builder.positiveActionText)
setPositiveActionTextSpanned(builder.positiveActionTextSpanned)
setPositiveActionTextTypeface(builder.positiveActionTextTypeface)
setPositiveActionTextSizeInPx(builder.positiveActionTextSizeInPx)
setPositiveActionTextSizeInSp(builder.positiveActionTextSizeInSp)
setPositiveActionTextColor(builder.positiveActionTextColor)
setPositiveActionTextAppearance(builder.positiveActionTextAppearance)
setPositiveActionTapListener(builder.onPositiveActionTapListener)
setNegativeActionText(builder.negativeActionText)
setNegativeActionTextSpanned(builder.negativeActionTextSpanned)
setNegativeActionTextTypeface(builder.negativeActionTextTypeface)
setNegativeActionTextSizeInPx(builder.negativeActionTextSizeInPx)
setNegativeActionTextSizeInSp(builder.negativeActionTextSizeInSp)
setNegativeActionTextColor(builder.negativeActionTextColor)
setNegativeActionTextAppearance(builder.negativeActionTextAppearance)
setNegativeActionTapListener(builder.onNegativeActionTapListener)
showIcon(builder.showIcon)
showIconScale(builder.iconScale, builder.iconScaleType)
setIconDrawable(builder.iconDrawable)
setIconBitmap(builder.iconBitmap)
setIconColorFilter(builder.iconColorFilter, builder.iconColorFilterMode)
setProgressPosition(builder.progressPosition)
setProgressTint(builder.progressTint, builder.progressPosition)
}
}
class Builder(internal var activity: Activity) {
internal var gravity: Gravity = BOTTOM
internal var backgroundColor: Int? = null
internal var backgroundDrawable: Drawable? = null
internal var duration: Long = DURATION_INDEFINITE
internal var onBarTapListener: OnTapListener? = null
internal var onBarShowListener: OnBarShowListener? = null
internal var onBarDismissListener: OnBarDismissListener? = null
internal var barDismissOnTapOutside: Boolean = false
internal var onTapOutsideListener: OnTapListener? = null
internal var overlay: Boolean = false
internal var overlayColor: Int = ContextCompat.getColor(activity, R.color.modal)
internal var overlayBlockable: Boolean = false
internal var castShadow: Boolean = true
internal var shadowStrength: Int = DEFAULT_SHADOW_STRENGTH
internal var enableSwipeToDismiss: Boolean = false
internal var vibrationTargets: List<Vibration> = emptyList()
internal var title: String? = null
internal var titleSpanned: Spanned? = null
internal var titleTypeface: Typeface? = null
internal var titleSizeInPx: Float? = null
internal var titleSizeInSp: Float? = null
internal var titleColor: Int? = null
internal var titleAppearance: Int? = null
internal var message: String? = null
internal var messageSpanned: Spanned? = null
internal var messageTypeface: Typeface? = null
internal var messageSizeInPx: Float? = null
internal var messageSizeInSp: Float? = null
internal var messageColor: Int? = null
internal var messageAppearance: Int? = null
internal var primaryActionText: String? = null
internal var primaryActionTextSpanned: Spanned? = null
internal var primaryActionTextTypeface: Typeface? = null
internal var primaryActionTextSizeInPx: Float? = null
internal var primaryActionTextSizeInSp: Float? = null
internal var primaryActionTextColor: Int? = null
internal var primaryActionTextAppearance: Int? = null
internal var onPrimaryActionTapListener: OnActionTapListener? = null
internal var positiveActionText: String? = null
internal var positiveActionTextSpanned: Spanned? = null
internal var positiveActionTextTypeface: Typeface? = null
internal var positiveActionTextSizeInPx: Float? = null
internal var positiveActionTextSizeInSp: Float? = null
internal var positiveActionTextColor: Int? = null
internal var positiveActionTextAppearance: Int? = null
internal var onPositiveActionTapListener: OnActionTapListener? = null
internal var negativeActionText: String? = null
internal var negativeActionTextSpanned: Spanned? = null
internal var negativeActionTextTypeface: Typeface? = null
internal var negativeActionTextSizeInPx: Float? = null
internal var negativeActionTextSizeInSp: Float? = null
internal var negativeActionTextColor: Int? = null
internal var negativeActionTextAppearance: Int? = null
internal var onNegativeActionTapListener: OnActionTapListener? = null
internal var showIcon: Boolean = false
internal var iconScale: Float = DEFAUT_ICON_SCALE
internal var iconScaleType: ScaleType = CENTER_CROP
internal var iconDrawable: Drawable? = null
internal var iconBitmap: Bitmap? = null
internal var iconColorFilter: Int? = null
internal var iconColorFilterMode: PorterDuff.Mode? = null
internal var iconAnimBuilder: FlashAnimIconBuilder? = null
internal var progressPosition: ProgressPosition? = null
internal var progressTint: Int? = null
internal var enterAnimBuilder: FlashAnimBarBuilder? = null
internal var exitAnimBuilder: FlashAnimBarBuilder? = null
/**
* Specifies the gravity from where the flashbar will be shown (top/bottom) Default gravity is
* TOP
*/
fun gravity(gravity: Gravity) = apply { this.gravity = gravity }
/** Specifies the background drawable of the flashbar */
fun backgroundDrawable(drawable: Drawable) = apply { this.backgroundDrawable = drawable }
/** Specifies the background drawable resource of the flashbar */
fun backgroundDrawable(@DrawableRes drawableId: Int) = apply {
this.backgroundDrawable = ContextCompat.getDrawable(activity, drawableId)
}
/** Specifies the background color of the flashbar */
fun backgroundColor(@ColorInt color: Int) = apply { this.backgroundColor = color }
/** Specifies the background color resource of the flashbar */
fun backgroundColorRes(@ColorRes colorId: Int) = apply {
this.backgroundColor = ContextCompat.getColor(activity, colorId)
}
/** Sets listener to receive tap events on the surface of the bar */
fun listenBarTaps(listener: OnTapListener) = apply { this.onBarTapListener = listener }
/**
* Specifies the duration for which the flashbar will be visible By default, the duration is
* infinite
*/
fun duration(milliseconds: Long) = apply {
require(milliseconds > 0) { "Duration can not be negative or zero" }
this.duration = milliseconds
}
/** Sets listener to receive bar showing/shown events */
fun barShowListener(listener: OnBarShowListener) = apply { this.onBarShowListener = listener }
/** Sets listener to receive bar dismissing/dismissed events */
fun barDismissListener(listener: OnBarDismissListener) = apply {
this.onBarDismissListener = listener
}
/** Sets listener to receive tap events outside flashbar surface */
fun listenOutsideTaps(listener: OnTapListener) = apply { this.onTapOutsideListener = listener }
/** Dismisses the bar on being tapped outside */
fun dismissOnTapOutside() = apply { this.barDismissOnTapOutside = true }
/** Shows the modal overlay */
fun showOverlay() = apply { this.overlay = true }
/** Specifies modal overlay color */
fun overlayColor(@ColorInt color: Int) = apply { this.overlayColor = color }
/**
* Specifies modal overlay color resource Modal overlay is automatically shown if color is set
*/
fun overlayColorRes(@ColorRes colorId: Int) = apply {
this.overlayColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies if modal overlay is blockable and should comsume touch events */
fun overlayBlockable() = apply { this.overlayBlockable = true }
/** Specifies if the flashbar should cast shadow and the strength of it */
@JvmOverloads
fun castShadow(shadow: Boolean = true, strength: Int = DEFAULT_SHADOW_STRENGTH) = apply {
require(strength > 0) { "Shadow strength can not be negative or zero" }
this.castShadow = shadow
this.shadowStrength = strength
}
/** Specifies the enter animation of the flashbar */
fun enterAnimation(builder: FlashAnimBarBuilder) = apply { this.enterAnimBuilder = builder }
/** Specifies the exit animation of the flashbar */
fun exitAnimation(builder: FlashAnimBarBuilder) = apply { this.exitAnimBuilder = builder }
/** Enables swipe-to-dismiss for the flashbar */
fun enableSwipeToDismiss() = apply { this.enableSwipeToDismiss = true }
/** Specifies whether the device should vibrate during flashbar enter/exit/both */
fun vibrateOn(vararg vibrate: Vibration) = apply {
require(vibrate.isNotEmpty()) { "Vibration targets can not be empty" }
this.vibrationTargets = vibrate.toList()
}
/** Specifies the title string */
fun title(title: String) = apply { this.title = title }
/** Specifies the title string res */
fun title(@StringRes titleId: Int) = apply { this.title = activity.getString(titleId) }
/** Specifies the title span */
fun title(title: Spanned) = apply { this.titleSpanned = title }
/** Specifies the title typeface */
fun titleTypeface(typeface: Typeface) = apply { this.titleTypeface = typeface }
/** Specifies the title size (in pixels) */
fun titleSizeInPx(size: Float) = apply { this.titleSizeInPx = size }
/** Specifies the title size (in sp) */
fun titleSizeInSp(size: Float) = apply { this.titleSizeInSp = size }
/** Specifies the title color */
fun titleColor(@ColorInt color: Int) = apply { this.titleColor = color }
/** Specifies the title color resource */
fun titleColorRes(@ColorRes colorId: Int) = apply {
this.titleColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the title appearance */
fun titleAppearance(@StyleRes appearance: Int) = apply { this.titleAppearance = appearance }
/** Specifies the message string */
fun message(message: String) = apply { this.message = message }
/** Specifies the message string resource */
fun message(@StringRes messageId: Int) = apply { this.message = activity.getString(messageId) }
/** Specifies the message string span */
fun message(message: Spanned) = apply { this.messageSpanned = message }
/** Specifies the message typeface */
fun messageTypeface(typeface: Typeface) = apply { this.messageTypeface = typeface }
/** Specifies the message size (in pixels) */
fun messageSizeInPx(size: Float) = apply { this.messageSizeInPx = size }
/** Specifies the message size (in sp) */
fun messageSizeInSp(size: Float) = apply { this.messageSizeInSp = size }
/** Specifies the message color */
fun messageColor(@ColorInt color: Int) = apply { this.messageColor = color }
/** Specifies the message color resource */
fun messageColorRes(@ColorRes colorId: Int) = apply {
this.messageColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the message appearance */
fun messageAppearance(@StyleRes appearance: Int) = apply { this.messageAppearance = appearance }
/** Specifies the primary action text string */
fun primaryActionText(text: String) = apply {
require(progressPosition != RIGHT) { "Cannot show action button if right progress is set" }
this.primaryActionText = text
}
/** Specifies the primary action text string resource */
fun primaryActionText(@StringRes actionTextId: Int) = apply {
primaryActionText(activity.getString(actionTextId))
}
/** Specifies the primary action text string span */
fun primaryActionText(actionText: Spanned) = apply {
this.primaryActionTextSpanned = actionText
}
/** Specifies the primary action text typeface */
fun primaryActionTextTypeface(typeface: Typeface) = apply {
this.primaryActionTextTypeface = typeface
}
/** Specifies the primary action text size (in pixels) */
fun primaryActionTextSizeInPx(size: Float) = apply { this.primaryActionTextSizeInPx = size }
/** Specifies the primary action text size (in sp) */
fun primaryActionTextSizeInSp(size: Float) = apply { this.primaryActionTextSizeInSp = size }
/** Specifies the primary action text color */
fun primaryActionTextColor(@ColorInt color: Int) = apply { this.primaryActionTextColor = color }
/** Specifies the primary action text color resource */
fun primaryActionTextColorRes(@ColorRes colorId: Int) = apply {
this.primaryActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the primary action text appearance */
fun primaryActionTextAppearance(@StyleRes appearance: Int) = apply {
this.primaryActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the primary action */
fun primaryActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onPrimaryActionTapListener = onActionTapListener
}
/** Specifies the positive action text string */
fun positiveActionText(text: String) = apply { this.positiveActionText = text }
/** Specifies the positive action text string resource */
fun positiveActionText(@StringRes actionTextId: Int) = apply {
positiveActionText(activity.getString(actionTextId))
}
/** Specifies the positive action text string span */
fun positiveActionText(actionText: Spanned) = apply {
this.positiveActionTextSpanned = actionText
}
/** Specifies the positive action text typeface */
fun positiveActionTextTypeface(typeface: Typeface) = apply {
this.positiveActionTextTypeface = typeface
}
/** Specifies the positive action text size (in pixels) */
fun positiveActionTextSizeInPx(size: Float) = apply { this.positiveActionTextSizeInPx = size }
/** Specifies the positive action text size (in sp) */
fun positiveActionTextSizeInSp(size: Float) = apply { this.positiveActionTextSizeInSp = size }
/** Specifies the positive action text color */
fun positiveActionTextColor(@ColorInt color: Int) = apply {
this.positiveActionTextColor = color
}
/** Specifies the positive action text color resource */
fun positiveActionTextColorRes(@ColorRes colorId: Int) = apply {
this.positiveActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the positive action text appearance */
fun positiveActionTextAppearance(@StyleRes appearance: Int) = apply {
this.positiveActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the positive action */
fun positiveActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onPositiveActionTapListener = onActionTapListener
}
/** Specifies the negative action text string */
fun negativeActionText(text: String) = apply { this.negativeActionText = text }
/** Specifies the negative action text string resource */
fun negativeActionText(@StringRes actionTextId: Int) = apply {
negativeActionText(activity.getString(actionTextId))
}
/** Specifies the negative action text string span */
fun negativeActionText(actionText: Spanned) = apply {
this.negativeActionTextSpanned = actionText
}
/** Specifies the negative action text typeface */
fun negativeActionTextTypeface(typeface: Typeface) = apply {
this.negativeActionTextTypeface = typeface
}
/** Specifies the negative action text size (in pixels) */
fun negativeActionTextSizeInPx(size: Float) = apply { this.negativeActionTextSizeInPx = size }
/** Specifies the negative action text size (in sp) */
fun negativeActionTextSizeInSp(size: Float) = apply { this.negativeActionTextSizeInSp = size }
/** Specifies the negative action text color */
fun negativeActionTextColor(@ColorInt color: Int) = apply {
this.negativeActionTextColor = color
}
/** Specifies the negative action text color resource */
fun negativeActionTextColorRes(@ColorRes colorId: Int) = apply {
this.negativeActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the negative action text appearance */
fun negativeActionTextAppearance(@StyleRes appearance: Int) = apply {
this.negativeActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the negative action */
fun negativeActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onNegativeActionTapListener = onActionTapListener
}
/** Specifies if the icon should be shown. Also configures its scale factor and scale type */
@JvmOverloads
fun showIcon(scale: Float = DEFAUT_ICON_SCALE, scaleType: ScaleType = CENTER_CROP) = apply {
require(progressPosition != LEFT) { "Cannot show icon if left progress is set" }
require(scale > 0) { "Icon scale cannot be negative or zero" }
this.showIcon = true
this.iconScale = scale
this.iconScaleType = scaleType
}
/** Specifies the icon drawable */
fun icon(icon: Drawable) = apply { this.iconDrawable = icon }
/** Specifies the icon drawable resource */
fun icon(@DrawableRes iconId: Int) = apply {
this.iconDrawable = ContextCompat.getDrawable(activity, iconId)
}
/** Specifies the icon bitmap */
fun icon(bitmap: Bitmap) = apply { this.iconBitmap = bitmap }
/** Specifies the icon color filter and mode */
@JvmOverloads
fun iconColorFilter(@ColorInt color: Int, mode: PorterDuff.Mode? = null) = apply {
this.iconColorFilter = color
this.iconColorFilterMode = mode
}
/** Specifies the icon color filter resource and mode */
@JvmOverloads
fun iconColorFilterRes(@ColorRes colorId: Int, mode: PorterDuff.Mode? = null) = apply {
this.iconColorFilter = ContextCompat.getColor(activity, colorId)
this.iconColorFilterMode = mode
}
/** Specifies the icon builder */
fun iconAnimation(builder: FlashAnimIconBuilder) = apply { this.iconAnimBuilder = builder }
/** Specifies the gravity in which the indeterminate progress is shown (left/right) */
fun showProgress(position: ProgressPosition) = apply {
this.progressPosition = position
if (progressPosition == LEFT && showIcon) {
throw IllegalArgumentException("Cannot show progress at left if icon is already set")
}
if (progressPosition == RIGHT && primaryActionText != null) {
throw IllegalArgumentException(
"Cannot show progress at right if action button is already set"
)
}
}
/** Specifies the indeterminate progress tint */
fun progressTint(@ColorInt color: Int) = apply { this.progressTint = color }
/** Specifies the indeterminate progress tint resource */
fun progressTintRes(@ColorRes colorId: Int) = apply {
this.progressTint = ContextCompat.getColor(activity, colorId)
}
/** Builds a flashbar instance */
fun build(): Flashbar {
configureAnimation()
val flashbar = Flashbar(this)
flashbar.construct()
return flashbar
}
/** Shows the flashbar */
fun show() = build().show()
private fun configureAnimation() {
enterAnimBuilder =
if (enterAnimBuilder == null) {
when (gravity) {
TOP -> FlashAnim.with(activity).animateBar().enter().fromTop()
BOTTOM -> FlashAnim.with(activity).animateBar().enter().fromBottom()
}
} else {
when (gravity) {
TOP -> enterAnimBuilder!!.enter().fromTop()
BOTTOM -> enterAnimBuilder!!.enter().fromBottom()
}
}
exitAnimBuilder =
if (exitAnimBuilder == null) {
when (gravity) {
TOP -> FlashAnim.with(activity).animateBar().exit().fromTop()
BOTTOM -> FlashAnim.with(activity).animateBar().exit().fromBottom()
}
} else {
when (gravity) {
TOP -> exitAnimBuilder!!.exit().fromTop()
BOTTOM -> exitAnimBuilder!!.exit().fromBottom()
}
}
}
}
companion object {
const val DURATION_SHORT = 1000L
const val DURATION_LONG = 2500L
const val DURATION_INDEFINITE = -1L
}
enum class Gravity {
TOP,
BOTTOM
}
enum class DismissEvent {
TIMEOUT,
MANUAL,
TAP_OUTSIDE,
SWIPE
}
enum class Vibration {
SHOW,
DISMISS
}
enum class ProgressPosition {
LEFT,
RIGHT
}
interface OnActionTapListener {
fun onActionTapped(bar: Flashbar)
}
interface OnBarDismissListener {
fun onDismissing(bar: Flashbar, isSwiped: Boolean)
fun onDismissProgress(bar: Flashbar, progress: Float)
fun onDismissed(bar: Flashbar, event: DismissEvent)
}
interface OnTapListener {
fun onTap(flashbar: Flashbar)
}
interface OnBarShowListener {
fun onShowing(bar: Flashbar)
fun onShowProgress(bar: Flashbar, progress: Float)
fun onShown(bar: Flashbar)
}
}
@@ -0,0 +1,298 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE 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.
*
* AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.flashbar
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.HapticFeedbackConstants.VIRTUAL_KEY
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout
import com.itsaky.androidide.flashbar.Flashbar.Companion.DURATION_INDEFINITE
import com.itsaky.androidide.flashbar.Flashbar.DismissEvent
import com.itsaky.androidide.flashbar.Flashbar.DismissEvent.*
import com.itsaky.androidide.flashbar.Flashbar.Vibration.DISMISS
import com.itsaky.androidide.flashbar.SwipeDismissTouchListener.DismissCallbacks
import com.itsaky.androidide.flashbar.anim.FlashAnim
import com.itsaky.androidide.flashbar.Flashbar.OnBarDismissListener
import com.itsaky.androidide.flashbar.Flashbar.OnBarShowListener
import com.itsaky.androidide.flashbar.Flashbar.OnTapListener
import com.itsaky.androidide.flashbar.Flashbar.Vibration
import com.itsaky.androidide.flashbar.Flashbar.Vibration.SHOW
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.*
import com.itsaky.androidide.flashbar.util.afterMeasured
import com.itsaky.androidide.flashbar.util.getNavigationBarPosition
import com.itsaky.androidide.flashbar.util.getNavigationBarSizeInPx
import com.itsaky.androidide.flashbar.util.getRootView
/**
* Container withView matching the height and width of the parent to hold a FlashbarView.
* It will occupy the entire screens size but will be completely transparent. The
* FlashbarView inside is the only visible component in it.
*/
internal class FlashbarContainerView(context: Context)
: RelativeLayout(context), DismissCallbacks {
private val dismissRunnable = Runnable { dismissInternal(TIMEOUT) }
internal lateinit var parentFlashbar: Flashbar
private lateinit var flashbarView: FlashbarView
private lateinit var enterAnimBuilder: FlashAnimBarBuilder
private lateinit var exitAnimBuilder: FlashAnimBarBuilder
private lateinit var vibrationTargets: List<Vibration>
private var onBarShowListener: OnBarShowListener? = null
private var onBarDismissListener: OnBarDismissListener? = null
private var onTapOutsideListener: OnTapListener? = null
private var overlayColor: Int? = null
private var iconAnimBuilder: FlashAnimIconBuilder? = null
private var duration = DURATION_INDEFINITE
private var isBarShowing = false
private var isBarShown = false
private var isBarDismissing = false
private var barDismissOnTapOutside: Boolean = false
private var showOverlay: Boolean = false
private var overlayBlockable: Boolean = false
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
ACTION_DOWN -> {
val rect = Rect()
flashbarView.getHitRect(rect)
// Checks if the tap was outside the bar
if (!rect.contains(event.x.toInt(), event.y.toInt())) {
onTapOutsideListener?.onTap(parentFlashbar)
if (barDismissOnTapOutside) {
dismissInternal(TAP_OUTSIDE)
}
}
}
}
return super.onInterceptTouchEvent(event)
}
override fun onSwipe(isSwiping: Boolean) {
isBarDismissing = isSwiping
if (isSwiping) {
onBarDismissListener?.onDismissing(parentFlashbar, true)
}
}
override fun onDismiss(view: View) {
removeCallbacks(dismissRunnable)
(parent as? ViewGroup)?.removeView(this@FlashbarContainerView)
isBarShown = false
flashbarView.stopIconAnimation()
if (vibrationTargets.contains(DISMISS)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarDismissListener?.onDismissed(parentFlashbar, SWIPE)
}
internal fun attach(flashbarView: FlashbarView) {
this.flashbarView = flashbarView
}
internal fun construct() {
isHapticFeedbackEnabled = true
if (showOverlay) {
setBackgroundColor(overlayColor!!)
if (overlayBlockable) {
isClickable = true
isFocusable = true
}
}
addView(flashbarView)
}
internal fun addParent(flashbar: Flashbar) {
this.parentFlashbar = flashbar
}
internal fun adjustOrientation(activity: Activity) {
val flashbarContainerViewLp = LayoutParams(MATCH_PARENT, MATCH_PARENT)
val navigationBarPosition = activity.getNavigationBarPosition()
val navigationBarSize = activity.getNavigationBarSizeInPx()
when (navigationBarPosition) {
LEFT -> flashbarContainerViewLp.leftMargin = navigationBarSize
TOP -> flashbarContainerViewLp.topMargin = navigationBarSize
RIGHT -> flashbarContainerViewLp.rightMargin = navigationBarSize
BOTTOM -> flashbarContainerViewLp.bottomMargin = navigationBarSize
}
layoutParams = flashbarContainerViewLp
}
internal fun show(activity: Activity) {
if (isBarShowing || isBarShown) return
val activityRootView = activity.getRootView() ?: return
// Only add the withView to the parent once
if (this.parent == null) activityRootView.addView(this)
activityRootView.afterMeasured {
val enterAnim = enterAnimBuilder.withView(flashbarView).build()
enterAnim.start(object : FlashAnim.InternalAnimListener {
override fun onStart() {
isBarShowing = true
onBarShowListener?.onShowing(parentFlashbar)
}
override fun onUpdate(progress: Float) {
onBarShowListener?.onShowProgress(parentFlashbar, progress)
}
override fun onStop() {
isBarShowing = false
isBarShown = true
flashbarView.startIconAnimation(iconAnimBuilder)
if (vibrationTargets.contains(SHOW)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarShowListener?.onShown(parentFlashbar)
}
})
handleDismiss()
}
}
internal fun dismiss() {
dismissInternal(MANUAL)
}
internal fun isBarShowing() = isBarShowing
internal fun isBarShown() = isBarShown
internal fun setDuration(duration: Long) {
this.duration = duration
}
internal fun setBarShowListener(listener: OnBarShowListener?) {
this.onBarShowListener = listener
}
internal fun setBarDismissListener(listener: OnBarDismissListener?) {
this.onBarDismissListener = listener
}
internal fun setBarDismissOnTapOutside(dismiss: Boolean) {
this.barDismissOnTapOutside = dismiss
}
internal fun setOnTapOutsideListener(listener: OnTapListener?) {
this.onTapOutsideListener = listener
}
internal fun setOverlay(overlay: Boolean) {
this.showOverlay = overlay
}
internal fun setOverlayColor(color: Int) {
this.overlayColor = color
}
internal fun setOverlayBlockable(blockable: Boolean) {
this.overlayBlockable = blockable
}
internal fun setEnterAnim(builder: FlashAnimBarBuilder) {
this.enterAnimBuilder = builder
}
internal fun setExitAnim(builder: FlashAnimBarBuilder) {
this.exitAnimBuilder = builder
}
internal fun enableSwipeToDismiss(enable: Boolean) {
this.flashbarView.enableSwipeToDismiss(enable, this)
}
internal fun setVibrationTargets(targets: List<Vibration>) {
this.vibrationTargets = targets
}
internal fun setIconAnim(builder: FlashAnimIconBuilder?) {
this.iconAnimBuilder = builder
}
private fun handleDismiss() {
if (duration != DURATION_INDEFINITE) {
postDelayed(dismissRunnable, duration)
}
}
private fun dismissInternal(event: DismissEvent) {
if (isBarDismissing || isBarShowing || !isBarShown) {
return
}
removeCallbacks(dismissRunnable)
val exitAnim = exitAnimBuilder.withView(flashbarView).build()
exitAnim.start(object : FlashAnim.InternalAnimListener {
override fun onStart() {
isBarDismissing = true
onBarDismissListener?.onDismissing(parentFlashbar, false)
}
override fun onUpdate(progress: Float) {
onBarDismissListener?.onDismissProgress(parentFlashbar, progress)
}
override fun onStop() {
isBarDismissing = false
isBarShown = false
if (vibrationTargets.contains(DISMISS)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarDismissListener?.onDismissed(parentFlashbar, event)
post { (parent as? ViewGroup)?.removeView(this@FlashbarContainerView) }
}
})
}
}
@@ -0,0 +1,484 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE 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.
*
* AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.flashbar
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
import android.widget.TextView
import androidx.annotation.ColorInt
import com.itsaky.androidide.flashbar.Flashbar.Gravity
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.Flashbar.OnActionTapListener
import com.itsaky.androidide.flashbar.Flashbar.OnTapListener
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.LEFT
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.RIGHT
import com.itsaky.androidide.flashbar.SwipeDismissTouchListener.DismissCallbacks
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
import com.itsaky.androidide.flashbar.databinding.FlashBarViewBinding
import com.itsaky.androidide.flashbar.util.convertDpToPx
import com.itsaky.androidide.flashbar.util.getStatusBarHeightInPx
import com.itsaky.androidide.flashbar.view.FbButton
import com.itsaky.androidide.flashbar.view.FbProgress
import com.itsaky.androidide.flashbar.view.ShadowView
/**
* The actual Flashbar withView representation that can consist of the title, message, button, icon,
* etc. Its size is adaptive and depends solely on the amount of content present in it. It always
* matches the width of the screen.
*
* It can either be present at the top or at the bottom of the screen. It will always consume touch
* events and respond as necessary.
*/
internal class FlashbarView(context: Context) : LinearLayout(context) {
private val compensationMarginTop =
resources.getDimension(R.dimen.fb_top_compensation_margin).toInt()
private val compensationMarginBottom =
resources.getDimension(R.dimen.fb_bottom_compensation_margin).toInt()
private lateinit var binding: FlashBarViewBinding
private lateinit var parentFlashbarContainer: FlashbarContainerView
private lateinit var gravity: Gravity
private var isMarginCompensationApplied: Boolean = false
private val fbContent: LinearLayout
get() = this.binding.fbContent
private val fbRoot: LinearLayout
get() = this.binding.fbRoot
private val fbIcon: ImageView
get() = this.binding.fbIcon
private val fbLeftProgress: FbProgress
get() = this.binding.fbLeftProgress
private val fbRightProgress: FbProgress
get() = this.binding.fbRightProgress
private val fbMessage: TextView
get() = this.binding.fbMessage
private val fbPrimaryAction: FbButton
get() = this.binding.fbPrimaryAction
private val fbPositiveAction: FbButton
get() = this.binding.fbPositiveAction
private val fbNegativeAction: FbButton
get() = this.binding.fbNegativeAction
private val fbSecondaryActionContainer: LinearLayout
get() = this.binding.fbSecondaryActionContainer
private val fbTitle: TextView
get() = this.binding.fbTitle
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!isMarginCompensationApplied) {
isMarginCompensationApplied = true
val params = layoutParams as MarginLayoutParams
when (gravity) {
TOP -> params.topMargin = -compensationMarginTop
BOTTOM -> params.bottomMargin = -compensationMarginBottom
}
requestLayout()
}
}
internal fun init(gravity: Gravity, castShadow: Boolean, shadowStrength: Int) {
this.gravity = gravity
this.orientation = VERTICAL
// If the bar appears with the bottom, then the shadow needs to added to the top of it,
// Thus, before the inflation of the bar
if (castShadow && gravity == BOTTOM) {
castShadow(ShadowView.ShadowType.TOP, shadowStrength)
}
this.binding = FlashBarViewBinding.inflate(LayoutInflater.from(context), this, true)
// If the bar appears with the top, then the shadow needs to added to the bottom of it,
// Thus, after the inflation of the bar
if (castShadow && gravity == TOP) {
castShadow(ShadowView.ShadowType.BOTTOM, shadowStrength)
}
}
internal fun adjustWitPositionAndOrientation(activity: Activity, gravity: Gravity) {
val flashbarViewLp = RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val statusBarHeight = activity.getStatusBarHeightInPx()
val flashbarViewContentLp = fbContent.layoutParams as LayoutParams
when (gravity) {
TOP -> {
flashbarViewContentLp.topMargin = statusBarHeight.plus(compensationMarginTop / 2)
flashbarViewLp.addRule(ALIGN_PARENT_TOP)
}
BOTTOM -> {
flashbarViewContentLp.bottomMargin = compensationMarginBottom
flashbarViewLp.addRule(ALIGN_PARENT_BOTTOM)
}
}
fbContent.layoutParams = flashbarViewContentLp
layoutParams = flashbarViewLp
}
internal fun addParent(flashbarContainerView: FlashbarContainerView) {
this.parentFlashbarContainer = flashbarContainerView
}
internal fun setBarBackgroundDrawable(drawable: Drawable?) {
if (drawable == null) return
this.fbRoot.background = drawable
}
internal fun setBarBackgroundColor(@ColorInt color: Int?) {
if (color == null) return
this.fbRoot.setBackgroundColor(color)
}
internal fun setBarTapListener(listener: OnTapListener?) {
if (listener == null) return
this.fbRoot.setOnClickListener { listener.onTap(parentFlashbarContainer.parentFlashbar) }
}
internal fun setTitle(title: String?) {
if (TextUtils.isEmpty(title)) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
internal fun setTitleSpanned(title: Spanned?) {
if (title == null) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
internal fun setTitleTypeface(typeface: Typeface?) {
if (typeface == null) return
fbTitle.typeface = typeface
}
internal fun setTitleSizeInPx(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
internal fun setTitleSizeInSp(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
internal fun setTitleColor(color: Int?) {
if (color == null) return
fbTitle.setTextColor(color)
}
internal fun setTitleAppearance(titleAppearance: Int?) {
if (titleAppearance == null) return
this.fbTitle.setTextAppearance(titleAppearance)
}
internal fun setMessage(message: String?) {
if (TextUtils.isEmpty(message)) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
internal fun setMessageSpanned(message: Spanned?) {
if (message == null) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
internal fun setMessageTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbMessage.typeface = typeface
}
internal fun setMessageSizeInPx(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
internal fun setMessageSizeInSp(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
internal fun setMessageColor(color: Int?) {
if (color == null) return
this.fbMessage.setTextColor(color)
}
internal fun setMessageAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbMessage.setTextAppearance(messageAppearance)
}
internal fun setPrimaryActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
internal fun setPrimaryActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
internal fun setPrimaryActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPrimaryAction.typeface = typeface
}
internal fun setPrimaryActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
internal fun setPrimaryActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
internal fun setPrimaryActionTextColor(color: Int?) {
if (color == null) return
this.fbPrimaryAction.setTextColor(color)
}
internal fun setPrimaryActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPrimaryAction.setTextAppearance(messageAppearance)
}
internal fun setPrimaryActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPrimaryAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
internal fun setPositiveActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
internal fun setPositiveActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
internal fun setPositiveActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPositiveAction.typeface = typeface
}
internal fun setPositiveActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
internal fun setPositiveActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
internal fun setPositiveActionTextColor(color: Int?) {
if (color == null) return
this.fbPositiveAction.setTextColor(color)
}
internal fun setPositiveActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPositiveAction.setTextAppearance(messageAppearance)
}
internal fun setPositiveActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPositiveAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
internal fun setNegativeActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
internal fun setNegativeActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
internal fun setNegativeActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbNegativeAction.typeface = typeface
}
internal fun setNegativeActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
internal fun setNegativeActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
internal fun setNegativeActionTextColor(color: Int?) {
if (color == null) return
this.fbNegativeAction.setTextColor(color)
}
internal fun setNegativeActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbNegativeAction.setTextAppearance(messageAppearance)
}
internal fun setNegativeActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbNegativeAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
internal fun showIcon(showIcon: Boolean) {
this.fbIcon.visibility = if (showIcon) VISIBLE else GONE
}
internal fun showIconScale(scale: Float, scaleType: ImageView.ScaleType?) {
this.fbIcon.scaleX = scale
this.fbIcon.scaleY = scale
this.fbIcon.scaleType = scaleType
}
internal fun setIconDrawable(icon: Drawable?) {
if (icon == null) return
this.fbIcon.setImageDrawable(icon)
}
internal fun setIconBitmap(bitmap: Bitmap?) {
if (bitmap == null) return
this.fbIcon.setImageBitmap(bitmap)
}
internal fun setIconColorFilter(colorFilter: Int?, filterMode: PorterDuff.Mode?) {
if (colorFilter == null) return
if (filterMode == null) {
this.fbIcon.setColorFilter(colorFilter)
} else {
this.fbIcon.setColorFilter(colorFilter, filterMode)
}
}
internal fun startIconAnimation(animator: FlashAnimIconBuilder?) {
animator?.withView(fbIcon)?.build()?.start()
}
internal fun stopIconAnimation() {
fbIcon.clearAnimation()
}
@SuppressLint("ClickableViewAccessibility")
internal fun enableSwipeToDismiss(enable: Boolean, callbacks: DismissCallbacks) {
if (enable) {
fbRoot.setOnTouchListener(SwipeDismissTouchListener(this, callbacks))
}
}
internal fun setProgressPosition(position: ProgressPosition?) {
if (position == null) return
when (position) {
LEFT -> {
fbLeftProgress.visibility = VISIBLE
fbRightProgress.visibility = GONE
}
RIGHT -> {
fbLeftProgress.visibility = GONE
fbRightProgress.visibility = VISIBLE
}
}
}
internal fun setProgressTint(progressTint: Int?, position: ProgressPosition?) {
if (position == null || progressTint == null) return
val progressBar =
when (position) {
LEFT -> fbLeftProgress
RIGHT -> fbRightProgress
}
progressBar.setBarColor(progressTint)
}
private fun castShadow(shadowType: ShadowView.ShadowType, strength: Int) {
val params = RelativeLayout.LayoutParams(MATCH_PARENT, context.convertDpToPx(strength))
val shadow = ShadowView(context)
shadow.applyShadow(shadowType)
addView(shadow, params)
}
}
@@ -0,0 +1,200 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE 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.
*
* AndroidIDE 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 AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.flashbar
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewConfiguration
import kotlin.math.abs
internal class SwipeDismissTouchListener(
private val view: View,
private val callbacks: DismissCallbacks
) : View.OnTouchListener {
private val slop: Int
private val minFlingVelocity: Int
private val animationTime: Long
private var viewWidth = 1
private var downX: Float = 0.toFloat()
private var downY: Float = 0.toFloat()
private var swiping: Boolean = false
private var swipingSlop: Int = 0
private var velocityTracker: VelocityTracker? = null
private var translationX: Float = 0.toFloat()
init {
val vc = ViewConfiguration.get(view.context)
slop = vc.scaledTouchSlop
minFlingVelocity = vc.scaledMinimumFlingVelocity * 16
animationTime =
view.context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
motionEvent.offsetLocation(translationX, 0f)
if (viewWidth < 2) {
viewWidth = this.view.width
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
downX = motionEvent.rawX
downY = motionEvent.rawY
velocityTracker = VelocityTracker.obtain()
velocityTracker!!.addMovement(motionEvent)
return false
}
MotionEvent.ACTION_UP -> {
if (velocityTracker != null) {
val deltaX = motionEvent.rawX - downX
velocityTracker!!.addMovement(motionEvent)
velocityTracker!!.computeCurrentVelocity(1000)
val velocityX = velocityTracker!!.xVelocity
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityTracker!!.yVelocity)
var dismiss = false
var dismissRight = false
if (abs(deltaX) > viewWidth / 2 && swiping) {
dismiss = true
dismissRight = deltaX > 0
} else if (minFlingVelocity <= absVelocityX && absVelocityY < absVelocityX && swiping) {
dismiss = velocityX < 0 == deltaX < 0
dismissRight = velocityTracker!!.xVelocity > 0
}
if (dismiss) {
this.view
.animate()
.translationX(if (dismissRight) viewWidth.toFloat() else -viewWidth.toFloat())
.alpha(0f)
.setDuration(animationTime)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
performDismiss()
}
}
)
} else if (swiping) {
this.view
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationTime)
.setListener(null)
}
velocityTracker!!.recycle()
velocityTracker = null
translationX = 0f
downX = 0f
downY = 0f
swiping = false
callbacks.onSwipe(false)
}
}
MotionEvent.ACTION_CANCEL -> {
if (velocityTracker != null) {
this.view
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationTime)
.setListener(null)
velocityTracker!!.recycle()
velocityTracker = null
translationX = 0f
downX = 0f
downY = 0f
swiping = false
callbacks.onSwipe(false)
}
}
MotionEvent.ACTION_MOVE -> {
if (velocityTracker != null) {
velocityTracker!!.addMovement(motionEvent)
val deltaX = motionEvent.rawX - downX
val deltaY = motionEvent.rawY - downY
if (abs(deltaX) > slop && abs(deltaY) < abs(deltaX) / 2) {
swiping = true
callbacks.onSwipe(true)
swipingSlop = if (deltaX > 0) slop else -slop
this.view.parent.requestDisallowInterceptTouchEvent(true)
val cancelEvent = MotionEvent.obtain(motionEvent)
cancelEvent.action =
MotionEvent.ACTION_CANCEL or
(motionEvent.actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
this.view.onTouchEvent(cancelEvent)
cancelEvent.recycle()
}
if (swiping) {
translationX = deltaX
this.view.translationX = deltaX - swipingSlop
this.view.alpha = 0f.coerceAtLeast(1f.coerceAtMost(1f - 2f * abs(deltaX) / viewWidth))
return true
}
}
}
else -> {
view.performClick()
return false
}
}
return false
}
private fun performDismiss() {
val lp = view.layoutParams
val originalHeight = view.height
val animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(animationTime)
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
callbacks.onDismiss(view)
view.alpha = 1f
view.translationX = 0f
lp.height = originalHeight
view.layoutParams = lp
}
}
)
animator.addUpdateListener { valueAnimator ->
lp.height = valueAnimator.animatedValue as Int
view.layoutParams = lp
}
animator.start()
}
internal interface DismissCallbacks {
fun onSwipe(isSwiping: Boolean)
fun onDismiss(view: View)
}
}
@@ -0,0 +1,52 @@
package com.itsaky.androidide.flashbar.anim
import android.content.Context
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AccelerateInterpolator
import android.view.animation.AnimationUtils
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.CallSuper
import androidx.annotation.InterpolatorRes
import com.itsaky.androidide.flashbar.R
abstract class BaseFlashAnimBuilder(private val context: Context) {
private val DEFAULT_DURATION =
context.resources.getInteger(R.integer.default_animation_duration).toLong()
protected val DEFAULT_ALPHA_START = 0.2f
protected val DEFAULT_ALPHA_END = 1.0f
protected var view: View? = null
protected var duration = DEFAULT_DURATION
protected var interpolator: Interpolator? = null
protected var alpha: Boolean = false
@CallSuper
open fun duration(millis: Long) = apply {
require(duration >= 0) { "Duration must not be negative" }
this.duration = millis
}
@CallSuper open fun accelerate() = apply { this.interpolator = AccelerateInterpolator() }
@CallSuper open fun decelerate() = apply { this.interpolator = DecelerateInterpolator() }
@CallSuper
open fun accelerateDecelerate() = apply { this.interpolator = AccelerateDecelerateInterpolator() }
@CallSuper
open fun interpolator(interpolator: Interpolator) = apply { this.interpolator = interpolator }
@CallSuper
open fun interpolator(@InterpolatorRes id: Int) = apply {
this.interpolator = AnimationUtils.loadInterpolator(context, id)
}
@CallSuper open fun alpha() = apply { this.alpha = true }
@CallSuper internal open fun withView(view: View) = apply { this.view = view }
}
@@ -0,0 +1,56 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
class FlashAnim(private val compositeAnim: AnimatorSet) {
companion object {
@JvmStatic fun with(context: Context) = FlashAnimRetriever(context)
}
internal fun start(listener: InternalAnimListener? = null) {
if (listener != null) {
val primaryAnim = compositeAnim.childAnimations[0] as ObjectAnimator
primaryAnim.addListener(
object : Animator.AnimatorListener {
override fun onAnimationRepeat(animator: Animator) {}
override fun onAnimationEnd(animator: Animator) {
listener.onStop()
primaryAnim.removeAllListeners()
primaryAnim.removeAllUpdateListeners()
}
override fun onAnimationCancel(animator: Animator) {}
override fun onAnimationStart(animator: Animator) {
listener.onStart()
}
}
)
primaryAnim.addUpdateListener { listener.onUpdate(it.animatedFraction) }
}
compositeAnim.start()
}
internal interface InternalAnimListener {
fun onStart()
fun onUpdate(progress: Float)
fun onStop()
}
}
class FlashAnimRetriever(private val context: Context) {
/** Retrieves the builder for animating flashbar */
fun animateBar() = FlashAnimBarBuilder(context)
/** Retrieves the builder for animating the icon in the flashbar */
fun animateIcon() = FlashAnimIconBuilder(context)
}
@@ -0,0 +1,140 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.view.View
import android.view.animation.AnticipateInterpolator
import android.view.animation.Interpolator
import android.view.animation.OvershootInterpolator
import androidx.annotation.InterpolatorRes
import com.itsaky.androidide.flashbar.Flashbar
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Direction.LEFT
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Direction.RIGHT
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Type.ENTER
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Type.EXIT
class FlashAnimBarBuilder(context: Context) : BaseFlashAnimBuilder(context) {
private var type: Type? = null
private var gravity: Flashbar.Gravity? = null
private var direction: Direction? = null
/** Specifies the duration (in millis) for the animation */
override fun duration(millis: Long) = apply { super.duration(millis) }
/** Specifies accelerate interpolator for the animation */
override fun accelerate() = apply { super.accelerate() }
/** Specifies decelerate interpolator for the animation */
override fun decelerate() = apply { super.decelerate() }
/** Specifies accelerate-decelerate interpolator for the animation */
override fun accelerateDecelerate() = apply { super.accelerateDecelerate() }
/** Specifies custom interpolator for the animation */
override fun interpolator(interpolator: Interpolator) = apply { super.interpolator(interpolator) }
/** Specifies custom interpolator resource for the animation */
override fun interpolator(@InterpolatorRes id: Int) = apply { super.interpolator(id) }
/** Specifies that the animation should have alpha effect */
override fun alpha() = apply { super.alpha() }
/** Specifies that the bar should slide to/from the left */
fun slideFromLeft() = apply { this.direction = LEFT }
/** Specifies that the bar should slide to/from the right */
fun slideFromRight() = apply { this.direction = RIGHT }
/** Specifies overshoot interpolator for the animation */
fun overshoot() = apply { this.interpolator = OvershootInterpolator() }
/** Specifies overshoot-anticipate interpolator for the animation */
fun anticipateOvershoot() = apply { this.interpolator = AnticipateInterpolator() }
override fun withView(view: View) = apply { super.withView(view) }
internal fun enter() = apply { this.type = ENTER }
internal fun exit() = apply { this.type = EXIT }
internal fun fromTop() = apply { this.gravity = TOP }
internal fun fromBottom() = apply { this.gravity = BOTTOM }
internal fun build(): FlashAnim {
requireNotNull(view, { "Target view can not be null" })
val compositeAnim = AnimatorSet()
val animators = linkedSetOf<Animator>()
val translationAnim = ObjectAnimator()
// Slide from left/right animation is not specified, default top/bottom
// animation is applied
if (direction == null) {
translationAnim.setPropertyName("translationY")
when (type!!) {
ENTER ->
when (gravity!!) {
TOP -> translationAnim.setFloatValues(-view!!.height.toFloat(), 0f)
BOTTOM -> translationAnim.setFloatValues(view!!.height.toFloat(), 0f)
}
EXIT ->
when (gravity!!) {
TOP -> translationAnim.setFloatValues(0f, -view!!.height.toFloat())
BOTTOM -> translationAnim.setFloatValues(0f, view!!.height.toFloat())
}
}
} else {
translationAnim.setPropertyName("translationX")
when (type!!) {
ENTER ->
when (direction!!) {
LEFT -> translationAnim.setFloatValues(-view!!.width.toFloat(), 0f)
RIGHT -> translationAnim.setFloatValues(view!!.width.toFloat(), 0f)
}
EXIT ->
when (direction!!) {
LEFT -> translationAnim.setFloatValues(0f, -view!!.width.toFloat())
RIGHT -> translationAnim.setFloatValues(0f, view!!.width.toFloat())
}
}
}
translationAnim.target = view
animators.add(translationAnim)
if (alpha) {
val alphaAnim = ObjectAnimator()
alphaAnim.setPropertyName("alpha")
alphaAnim.target = view
when (type!!) {
ENTER -> alphaAnim.setFloatValues(DEFAULT_ALPHA_START, DEFAULT_ALPHA_END)
EXIT -> alphaAnim.setFloatValues(DEFAULT_ALPHA_END, DEFAULT_ALPHA_START)
}
animators.add(alphaAnim)
}
compositeAnim.playTogether(animators)
compositeAnim.duration = duration
compositeAnim.interpolator = interpolator
return FlashAnim(compositeAnim)
}
enum class Type {
ENTER,
EXIT
}
enum class Direction {
LEFT,
RIGHT
}
}
@@ -0,0 +1,90 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator.INFINITE
import android.animation.ValueAnimator.REVERSE
import android.content.Context
import android.view.View
import android.view.animation.Interpolator
import androidx.annotation.InterpolatorRes
class FlashAnimIconBuilder(context: Context) : BaseFlashAnimBuilder(context) {
private val DEFAULT_PULSE_START = 1.0f
private val DEFAULT_PULSE_END = 0.6f
private var pulse: Boolean = false
private var pulseStart = 0f
private var pulseEnd = 0f
/** Specifies the duration (in millis) for the animation */
override fun duration(millis: Long) = apply { super.duration(millis) }
/** Specifies accelerate interpolator for the animation */
override fun accelerate() = apply { super.accelerate() }
/** Specifies decelerate interpolator for the animation */
override fun decelerate() = apply { super.decelerate() }
/** Specifies accelerate-decelerate interpolator for the animation */
override fun accelerateDecelerate() = apply { super.accelerateDecelerate() }
/** Specifies custom interpolator for the animation */
override fun interpolator(interpolator: Interpolator) = apply { super.interpolator(interpolator) }
/** Specifies custom interpolator resource for the animation */
override fun interpolator(@InterpolatorRes id: Int) = apply { super.interpolator(id) }
/** Specifies that the animation should have alpha effect */
override fun alpha() = apply { super.alpha() }
/** Specifies that the animation should have pulse effect */
@JvmOverloads
fun pulse(start: Float = DEFAULT_PULSE_START, end: Float = DEFAULT_PULSE_END) = apply {
this.pulse = true
this.pulseStart = start
this.pulseEnd = end
}
override fun withView(view: View) = apply { super.withView(view) }
internal fun build(): FlashAnim {
requireNotNull(view, { "Target view can not be null" })
val compositeAnim = AnimatorSet()
val animators = linkedSetOf<Animator>()
if (pulse) {
val scaleAnim =
ObjectAnimator.ofPropertyValuesHolder(
view,
PropertyValuesHolder.ofFloat("scaleX", pulseStart, pulseEnd),
PropertyValuesHolder.ofFloat("scaleY", pulseStart, pulseEnd)
)
scaleAnim.repeatCount = INFINITE
scaleAnim.repeatMode = REVERSE
animators.add(scaleAnim)
}
if (alpha) {
val alpha = ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0.6f)
alpha.repeatCount = INFINITE
alpha.repeatMode = REVERSE
animators.add(alpha)
}
compositeAnim.playTogether(animators)
compositeAnim.duration = duration
compositeAnim.interpolator = interpolator
return FlashAnim(compositeAnim)
}
}
@@ -0,0 +1,125 @@
package com.itsaky.androidide.flashbar.util
import android.app.Activity
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.os.Build
import android.util.DisplayMetrics
import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_180
import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.Window
import android.view.WindowManager
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.BOTTOM
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.LEFT
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.RIGHT
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.TOP
import kotlin.math.roundToInt
internal fun Activity.getStatusBarHeightInPx(): Int {
val rectangle = Rect()
window.decorView.getWindowVisibleDisplayFrame(rectangle)
val statusBarHeight = rectangle.top
val contentViewTop = window.findViewById<View>(Window.ID_ANDROID_CONTENT).top
return if (contentViewTop == 0) { // Actionbar is not present
statusBarHeight
} else {
contentViewTop - statusBarHeight
}
}
internal fun Activity.getNavigationBarPosition(): NavigationBarPosition {
val display =
if (Build.VERSION.SDK_INT >= 30) {
display
} else {
@Suppress("DEPRECATION") windowManager.defaultDisplay
}
return when (display?.rotation) {
ROTATION_0 -> BOTTOM
ROTATION_90 -> RIGHT
ROTATION_180 -> TOP
ROTATION_270 -> LEFT
else -> BOTTOM
}
}
internal fun Activity.getNavigationBarSizeInPx(): Int {
val realScreenSize = getRealScreenSize()
val appUsableScreenSize = getAppUsableScreenSize()
val navigationBarPosition = getNavigationBarPosition()
return if (navigationBarPosition == LEFT || navigationBarPosition == RIGHT) {
realScreenSize.x - appUsableScreenSize.x
} else {
realScreenSize.y - appUsableScreenSize.y
}
}
internal fun Activity?.getRootView(): ViewGroup? {
if (this == null || window == null) {
return null
}
return window.decorView as ViewGroup
}
internal fun Context.convertDpToPx(dp: Int): Int {
return (dp * (resources.displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
}
internal fun Context.convertPxToDp(px: Int): Int {
return (px / (resources.displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
}
private fun Activity.getRealScreenSize(): Point {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val defaultDisplay =
if (Build.VERSION.SDK_INT >= 30) {
display
} else {
@Suppress("DEPRECATION") windowManager.defaultDisplay
}
val size = Point()
defaultDisplay?.getRealSize(size)
return size
}
private fun Activity.getAppUsableScreenSize(): Point {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val defaultDisplay = windowManager.defaultDisplay
val size = Point()
defaultDisplay.getSize(size)
return size
}
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
} else {
viewTreeObserver.removeGlobalOnLayoutListener(this)
}
f()
}
}
}
)
}
internal enum class NavigationBarPosition {
BOTTOM,
RIGHT,
LEFT,
TOP
}
@@ -0,0 +1,17 @@
package com.itsaky.androidide.flashbar.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.itsaky.androidide.flashbar.R
internal class FbButton : AppCompatTextView {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.FbButtonStyle)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
}
@@ -0,0 +1,581 @@
package com.itsaky.androidide.flashbar.view
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.os.Build
import android.os.Build.VERSION
import android.os.Parcel
import android.os.Parcelable
import android.os.SystemClock
import android.provider.Settings
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import com.itsaky.androidide.flashbar.R
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.roundToInt
/** Forked from Todd Davies' Progress Wheel https://github.com/Todd-Davies/ProgressWheel */
class FbProgress : View {
private val barLength = 16
private val barMaxLength = 270
private val pauseGrowingTime: Long = 200
private var circleRadius = 28
private var barWidth = 4
private var rimWidth = 4
private var fillRadius = false
private var timeStartGrowing = 0.0
private var barSpinCycleTime = 460.0
private var barExtraLength = 0f
private var barGrowingFromFront = true
private var pausedTimeWithoutGrowing: Long = 0
private var barColor = -0x56000000
private var rimColor = 0x00FFFFFF
private val barPaint = Paint()
private val rimPaint = Paint()
private var circleBounds = RectF()
private var spinSpeed = 230.0f
private var lastTimeAnimated: Long = 0
private var linearProgress: Boolean = false
private var progress = 0.0f
private var targetProgress = 0.0f
private var isSpinning = false
private var shouldAnimate: Boolean = false
private var callback: ProgressCallback? = null
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
parseAttributes(context.obtainStyledAttributes(attrs, R.styleable.FbProgress))
setAnimationEnabled()
}
constructor(context: Context) : super(context) {
setAnimationEnabled()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val viewWidth = circleRadius + this.paddingLeft + this.paddingRight
val viewHeight = circleRadius + this.paddingTop + this.paddingBottom
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width: Int =
when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> viewWidth.coerceAtMost(widthSize)
else -> viewWidth
}
val height: Int =
when {
heightMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.EXACTLY -> heightSize
heightMode == MeasureSpec.AT_MOST -> viewHeight.coerceAtMost(heightSize)
else -> viewHeight
}
setMeasuredDimension(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
super.onSizeChanged(w, h, oldW, oldH)
setupBounds(w, h)
setupPaints()
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(circleBounds, 360f, 360f, false, rimPaint)
var mustInvalidate = false
if (!shouldAnimate) {
return
}
if (isSpinning) {
mustInvalidate = true
val deltaTime = SystemClock.uptimeMillis() - lastTimeAnimated
val deltaNormalized = deltaTime * spinSpeed / 1000.0f
updateBarLength(deltaTime)
progress += deltaNormalized
if (progress > 360) {
progress -= 360f
runCallback(-1.0f)
}
lastTimeAnimated = SystemClock.uptimeMillis()
var from = progress - 90
var length = barLength + barExtraLength
if (isInEditMode) {
from = 0f
length = 135f
}
canvas.drawArc(circleBounds, from, length, false, barPaint)
} else {
val oldProgress = progress
if (progress != targetProgress) {
mustInvalidate = true
val deltaTime = (SystemClock.uptimeMillis() - lastTimeAnimated).toFloat() / 1000
val deltaNormalized = deltaTime * spinSpeed
progress = (progress + deltaNormalized).coerceAtMost(targetProgress)
lastTimeAnimated = SystemClock.uptimeMillis()
}
if (oldProgress != progress) {
runCallback()
}
var offset = 0.0f
var progress = this.progress
if (!linearProgress) {
val factor = 2.0f
offset =
(1.0f - (1.0f - this.progress / 360.0f).toDouble().pow((2.0f * factor).toDouble()))
.toFloat() * 360.0f
progress =
(1.0f - (1.0f - this.progress / 360.0f).toDouble().pow(factor.toDouble())).toFloat() *
360.0f
}
if (isInEditMode) {
progress = 360f
}
canvas.drawArc(circleBounds, offset - 90, progress, false, barPaint)
}
if (mustInvalidate) {
invalidate()
}
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility == VISIBLE) {
lastTimeAnimated = SystemClock.uptimeMillis()
}
}
public override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val ss = WheelSavedState(superState)
ss.mProgress = this.progress
ss.mTargetProgress = this.targetProgress
ss.isSpinning = this.isSpinning
ss.spinSpeed = this.spinSpeed
ss.barWidth = this.barWidth
ss.barColor = this.barColor
ss.rimWidth = this.rimWidth
ss.rimColor = this.rimColor
ss.circleRadius = this.circleRadius
ss.linearProgress = this.linearProgress
ss.fillRadius = this.fillRadius
return ss
}
public override fun onRestoreInstanceState(state: Parcelable) {
if (state !is WheelSavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.progress = state.mProgress
this.targetProgress = state.mTargetProgress
this.isSpinning = state.isSpinning
this.spinSpeed = state.spinSpeed
this.barWidth = state.barWidth
this.barColor = state.barColor
this.rimWidth = state.rimWidth
this.rimColor = state.rimColor
this.circleRadius = state.circleRadius
this.linearProgress = state.linearProgress
this.fillRadius = state.fillRadius
this.lastTimeAnimated = SystemClock.uptimeMillis()
}
fun resetCount() {
progress = 0.0f
targetProgress = 0.0f
invalidate()
}
fun stopSpinning() {
isSpinning = false
progress = 0.0f
targetProgress = 0.0f
invalidate()
}
fun spin() {
lastTimeAnimated = SystemClock.uptimeMillis()
isSpinning = true
invalidate()
}
fun setInstantProgress(update: Float) {
var progressUpdate = update
if (isSpinning) {
progress = 0.0f
isSpinning = false
}
if (progressUpdate > 1.0f) {
progressUpdate -= 1.0f
} else if (progressUpdate < 0) {
progressUpdate = 0f
}
if (progressUpdate == targetProgress) {
return
}
targetProgress = (progressUpdate * 360.0f).coerceAtMost(360.0f)
progress = targetProgress
lastTimeAnimated = SystemClock.uptimeMillis()
invalidate()
}
fun getProgress(): Float {
return if (isSpinning) -1f else progress / 360.0f
}
fun setProgress(update: Float) {
var progress = update
if (isSpinning) {
this.progress = 0.0f
isSpinning = false
runCallback()
}
if (progress > 1.0f) {
progress -= 1.0f
} else if (progress < 0) {
progress = 0f
}
if (progress == targetProgress) {
return
}
if (this.progress == targetProgress) {
lastTimeAnimated = SystemClock.uptimeMillis()
}
targetProgress = (progress * 360.0f).coerceAtMost(360.0f)
invalidate()
}
fun setLinearProgress(isLinear: Boolean) {
linearProgress = isLinear
if (!isSpinning) {
invalidate()
}
}
fun getCircleRadius() = circleRadius
fun setCircleRadius(circleRadius: Int) {
this.circleRadius = circleRadius
if (!isSpinning) {
invalidate()
}
}
fun getBarWidth() = barWidth
fun setBarWidth(barWidth: Int) {
this.barWidth = barWidth
if (!isSpinning) {
invalidate()
}
}
fun getBarColor() = barColor
fun setBarColor(barColor: Int) {
this.barColor = barColor
setupPaints()
if (!isSpinning) {
invalidate()
}
}
fun getRimColor(): Int {
return rimColor
}
fun setRimColor(rimColor: Int) {
this.rimColor = rimColor
setupPaints()
if (!isSpinning) {
invalidate()
}
}
fun getSpinSpeed() = spinSpeed / 360.0f
fun setSpinSpeed(spinSpeed: Float) {
this.spinSpeed = spinSpeed * 360.0f
}
fun getRimWidth() = rimWidth
fun setRimWidth(rimWidth: Int) {
this.rimWidth = rimWidth
if (!isSpinning) {
invalidate()
}
}
fun setCallback(progressCallback: ProgressCallback) {
callback = progressCallback
if (!isSpinning) {
runCallback()
}
}
private fun setAnimationEnabled() {
val currentApiVersion = VERSION.SDK_INT
val animationValue =
if (currentApiVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
} else {
Settings.System.getFloat(
context.contentResolver,
Settings.System.ANIMATOR_DURATION_SCALE,
1f
)
}
shouldAnimate = animationValue != 0f
}
private fun setupBounds(layoutWidth: Int, layoutHeight: Int) {
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val paddingLeft = paddingLeft
val paddingRight = paddingRight
if (!fillRadius) {
val minValue =
(layoutWidth - paddingLeft - paddingRight).coerceAtMost(
layoutHeight - paddingBottom - paddingTop
)
val circleDiameter = minValue.coerceAtMost(circleRadius * 2 - barWidth * 2)
val xOffset = (layoutWidth - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft
val yOffset = (layoutHeight - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop
circleBounds =
RectF(
(xOffset + barWidth).toFloat(),
(yOffset + barWidth).toFloat(),
(xOffset + circleDiameter - barWidth).toFloat(),
(yOffset + circleDiameter - barWidth).toFloat()
)
} else {
circleBounds =
RectF(
(paddingLeft + barWidth).toFloat(),
(paddingTop + barWidth).toFloat(),
(layoutWidth - paddingRight - barWidth).toFloat(),
(layoutHeight - paddingBottom - barWidth).toFloat()
)
}
}
private fun parseAttributes(a: TypedArray) {
val metrics = context.resources.displayMetrics
barWidth =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, barWidth.toFloat(), metrics).toInt()
rimWidth =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rimWidth.toFloat(), metrics).toInt()
circleRadius =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleRadius.toFloat(), metrics)
.toInt()
circleRadius =
a.getDimension(R.styleable.FbProgress_fbp_circleRadius, circleRadius.toFloat()).toInt()
fillRadius = a.getBoolean(R.styleable.FbProgress_fbp_fillRadius, false)
barWidth = a.getDimension(R.styleable.FbProgress_fbp_barWidth, barWidth.toFloat()).toInt()
rimWidth = a.getDimension(R.styleable.FbProgress_fbp_rimWidth, rimWidth.toFloat()).toInt()
val baseSpinSpeed = a.getFloat(R.styleable.FbProgress_fbp_spinSpeed, spinSpeed / 360.0f)
spinSpeed = baseSpinSpeed * 360
barSpinCycleTime =
a.getInt(R.styleable.FbProgress_fbp_barSpinCycleTime, barSpinCycleTime.toInt()).toDouble()
barColor = a.getColor(R.styleable.FbProgress_fbp_barColor, barColor)
rimColor = a.getColor(R.styleable.FbProgress_fbp_rimColor, rimColor)
linearProgress = a.getBoolean(R.styleable.FbProgress_fbp_linearProgress, false)
if (a.getBoolean(R.styleable.FbProgress_fbp_progressIndeterminate, false)) {
spin()
}
a.recycle()
}
private fun setupPaints() {
barPaint.color = barColor
barPaint.isAntiAlias = true
barPaint.style = Paint.Style.STROKE
barPaint.strokeWidth = barWidth.toFloat()
rimPaint.color = rimColor
rimPaint.isAntiAlias = true
rimPaint.style = Paint.Style.STROKE
rimPaint.strokeWidth = rimWidth.toFloat()
}
private fun updateBarLength(deltaTimeInMilliSeconds: Long) {
if (pausedTimeWithoutGrowing >= pauseGrowingTime) {
timeStartGrowing += deltaTimeInMilliSeconds.toDouble()
if (timeStartGrowing > barSpinCycleTime) {
timeStartGrowing -= barSpinCycleTime
pausedTimeWithoutGrowing = 0
barGrowingFromFront = !barGrowingFromFront
}
val distance = cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI).toFloat() / 2 + 0.5f
val destLength = (barMaxLength - barLength).toFloat()
if (barGrowingFromFront) {
barExtraLength = distance * destLength
} else {
val newLength = destLength * (1 - distance)
progress += barExtraLength - newLength
barExtraLength = newLength
}
} else {
pausedTimeWithoutGrowing += deltaTimeInMilliSeconds
}
}
private fun runCallback(value: Float) {
if (callback != null) {
callback!!.onProgressUpdate(value)
}
}
private fun runCallback() {
if (callback != null) {
val normalizedProgress = (progress * 100 / 360.0f).roundToInt().toFloat() / 100
callback!!.onProgressUpdate(normalizedProgress)
}
}
interface ProgressCallback {
fun onProgressUpdate(progress: Float)
}
internal class WheelSavedState : View.BaseSavedState {
var mProgress: Float = 0.toFloat()
var mTargetProgress: Float = 0.toFloat()
var isSpinning: Boolean = false
var spinSpeed: Float = 0.toFloat()
var barWidth: Int = 0
var barColor: Int = 0
var rimWidth: Int = 0
var rimColor: Int = 0
var circleRadius: Int = 0
var linearProgress: Boolean = false
var fillRadius: Boolean = false
constructor(superState: Parcelable?) : super(superState)
private constructor(`in`: Parcel) : super(`in`) {
this.mProgress = `in`.readFloat()
this.mTargetProgress = `in`.readFloat()
this.isSpinning = `in`.readByte().toInt() != 0
this.spinSpeed = `in`.readFloat()
this.barWidth = `in`.readInt()
this.barColor = `in`.readInt()
this.rimWidth = `in`.readInt()
this.rimColor = `in`.readInt()
this.circleRadius = `in`.readInt()
this.linearProgress = `in`.readByte().toInt() != 0
this.fillRadius = `in`.readByte().toInt() != 0
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeFloat(this.mProgress)
out.writeFloat(this.mTargetProgress)
out.writeByte((if (isSpinning) 1 else 0).toByte())
out.writeFloat(this.spinSpeed)
out.writeInt(this.barWidth)
out.writeInt(this.barColor)
out.writeInt(this.rimWidth)
out.writeInt(this.rimColor)
out.writeInt(this.circleRadius)
out.writeByte((if (linearProgress) 1 else 0).toByte())
out.writeByte((if (fillRadius) 1 else 0).toByte())
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<WheelSavedState> =
object : Parcelable.Creator<WheelSavedState> {
override fun createFromParcel(`in`: Parcel): WheelSavedState {
return WheelSavedState(`in`)
}
override fun newArray(size: Int): Array<WheelSavedState> {
return newArray(size)
}
}
}
}
}
@@ -0,0 +1,33 @@
package com.itsaky.androidide.flashbar.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import com.itsaky.androidide.flashbar.R
import com.itsaky.androidide.flashbar.view.ShadowView.ShadowType.BOTTOM
import com.itsaky.androidide.flashbar.view.ShadowView.ShadowType.TOP
internal class ShadowView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
internal fun applyShadow(type: ShadowType) {
when (type) {
TOP -> setShadow(R.drawable.shadow_top)
BOTTOM -> setShadow(R.drawable.shadow_bottom)
}
}
private fun setShadow(@DrawableRes id: Int) {
val shadow = ContextCompat.getDrawable(context, id)
this.background = shadow
}
enum class ShadowType {
TOP,
BOTTOM
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/translucent_black" android:state_pressed="true" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="@color/shadow_color"
android:startColor="@android:color/transparent" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="-90"
android:endColor="@color/shadow_color"
android:startColor="@android:color/transparent" />
</shape>
@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fbRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/warm_grey"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:padding="@dimen/fb_content_padding_all">
<LinearLayout
android:id="@+id/fbContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<com.itsaky.androidide.flashbar.view.FbProgress
android:id="@+id/fbLeftProgress"
style="@style/FbCircularIndeterminateProgressStyle"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/fb_margin_progress"
android:layout_marginRight="@dimen/fb_margin_progress"
android:visibility="gone" />
<ImageView
android:id="@+id/fbIcon"
android:layout_width="@dimen/fb_icon_size"
android:layout_height="@dimen/fb_icon_size"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/fb_margin_right_icon"
android:layout_marginRight="@dimen/fb_margin_right_icon"
android:src="@drawable/ic_info"
android:visibility="gone"
app:tint="@color/white"
tools:visibility="visible" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="8"
android:orientation="vertical">
<TextView
android:id="@+id/fbTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/fb_title_message_margin"
android:textAppearance="@style/FbTitleTextStyle"
android:visibility="gone"
tools:text="Title"
tools:visibility="visible" />
<TextView
android:id="@+id/fbMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/fb_title_message_margin"
android:justificationMode="inter_word"
android:textAppearance="@style/FbMessageStyle"
android:visibility="gone"
tools:text="A quick brown fox jumps of the lazy dog."
tools:visibility="visible" />
</LinearLayout>
<com.itsaky.androidide.flashbar.view.FbButton
android:id="@+id/fbPrimaryAction"
style="@style/FbButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/FbButtonTextStyle"
android:visibility="gone"
tools:text="CLICK"
tools:visibility="visible" />
<com.itsaky.androidide.flashbar.view.FbProgress
android:id="@+id/fbRightProgress"
style="@style/FbCircularIndeterminateProgressStyle"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/fb_margin_progress"
android:layout_marginLeft="@dimen/fb_margin_progress"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:id="@+id/fbSecondaryActionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/fb_margin_secondary_action_container"
android:gravity="end"
android:visibility="gone">
<com.itsaky.androidide.flashbar.view.FbButton
android:id="@+id/fbPositiveAction"
style="@style/FbButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/FbButtonTextStyle"
android:visibility="gone"
tools:text="OK"
tools:visibility="gone" />
<com.itsaky.androidide.flashbar.view.FbButton
android:id="@+id/fbNegativeAction"
style="@style/FbButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fb_margin_secondary_action"
android:layout_marginLeft="12dp"
android:textAppearance="@style/FbButtonTextStyle"
android:visibility="gone"
tools:text="CANCEL"
tools:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FbProgress">
<attr name="fbp_progressIndeterminate" format="boolean" />
<attr name="fbp_barColor" format="color" />
<attr name="fbp_rimColor" format="color" />
<attr name="fbp_rimWidth" format="dimension" />
<attr name="fbp_spinSpeed" format="float" />
<attr name="fbp_barSpinCycleTime" format="integer" />
<attr name="fbp_circleRadius" format="dimension" />
<attr name="fbp_fillRadius" format="boolean" />
<attr name="fbp_barWidth" format="dimension" />
<attr name="fbp_linearProgress" format="boolean" />
</declare-styleable>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="warm_grey">#8e8e8e</color>
<color name="white">#fff</color>
<color name="shadow_color">#94444444</color>
<color name="translucent_black">#22000000</color>
<color name="modal">#80272727</color>
<color name="slate_black">#FF333333</color>
<color name="yellow">#FFEB3B</color>
</resources>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fb_content_padding_all">16dp</dimen>
<dimen name="fb_title_text_size">16sp</dimen>
<dimen name="fb_message_text_size">14sp</dimen>
<dimen name="fb_title_message_margin">2dp</dimen>
<dimen name="fb_icon_size">28dp</dimen>
<dimen name="fb_margin_right_icon">16dp</dimen>
<dimen name="fb_button_text_size">16sp</dimen>
<dimen name="fb_top_compensation_margin">16dp</dimen>
<dimen name="fb_bottom_compensation_margin">30dp</dimen>
<dimen name="fb_progress_size">20dp</dimen>
<dimen name="fb_margin_secondary_action">12dp</dimen>
<dimen name="fb_margin_secondary_action_container">12dp</dimen>
<dimen name="fb_margin_progress">20dp</dimen>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="default_animation_duration">250</integer>
</resources>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FbTitleTextStyle" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/white</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/fb_title_text_size</item>
</style>
<style name="FbMessageStyle" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/white</item>
<item name="android:textSize">@dimen/fb_message_text_size</item>
</style>
<style name="FbButtonTextStyle" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/white</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/fb_button_text_size</item>
</style>
<style name="FbButtonStyle">
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">8dp</item>
<item name="android:paddingTop">4dp</item>
<item name="android:paddingBottom">4dp</item>
<item name="android:background">@drawable/fb_button_background_selector</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
</style>
<style name="FbCircularIndeterminateProgressStyle">
<item name="fbp_barColor">@color/white</item>
<item name="fbp_barWidth">2dp</item>
<item name="fbp_progressIndeterminate">true</item>
</style>
</resources>