build: reorganize and reconfigure modules to resolve dependency resolution errors

Gradle, for some reason, does not the path of modules to identify them uniquely. Instead, the 'build id' is used to do so, which consists of the maven group and artifact ID. So, even if two modules are located in separate directories, but have the name group and artifact ID, including both of them in other modules may result in dependency resolution errors.
This commit is contained in:
Akash Yadav
2024-05-29 13:11:44 +05:30
parent c0c185cfb7
commit 1efd6a04d5
2457 changed files with 1069 additions and 709 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.
```
+43
View File
@@ -0,0 +1,43 @@
/*
* 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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
}
groupConfig {
groupIdSuffix.set("utils")
}
android {
namespace = "${BuildConfig.packageName}.flashbar"
}
dependencies {
implementation(libs.common.utilcode)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.annotation)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.google.material)
implementation(projects.core.resources)
}
+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,666 @@
/*
* 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
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)
flashbarContainerView.fitsSystemWindows = true
flashbarView = FlashbarView(builder.activity)
flashbarView.init(builder.gravity)
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 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 == DURATION_INDEFINITE || milliseconds > 0) { "Duration can only be $DURATION_INDEFINITE or > 0" }
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 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
}
fun 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,312 @@
/*
* 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 earlyDismissalRequested = 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)
if (earlyDismissalRequested) {
dismiss()
earlyDismissalRequested = false
}
}
})
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 (isBarShowing) {
// Flashbar is currently being shown
// but a dismissal has been requested
// take this into account and dismiss flashbar afterwards
earlyDismissalRequested = true
return
}
if (isBarDismissing || !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,488 @@
/*
* 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.content.res.ColorStateList
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 androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicator
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.getStatusBarHeightInPx
/**
* 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.
*/
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: CircularProgressIndicator
get() = this.binding.fbLeftProgress
private val fbRightProgress: CircularProgressIndicator
get() = this.binding.fbRightProgress
private val fbMessage: TextView
get() = this.binding.fbMessage
private val fbPrimaryAction: MaterialButton
get() = this.binding.fbPrimaryAction
private val fbPositiveAction: MaterialButton
get() = this.binding.fbPositiveAction
private val fbNegativeAction: MaterialButton
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) {
this.gravity = gravity
this.orientation = VERTICAL
this.binding = FlashBarViewBinding.inflate(LayoutInflater.from(context), this, true)
this.binding.root.apply {
// add margin to the card view so that the card elevation is visible
val dp16 = context.resources.getDimensionPixelSize(R.dimen.fb_card_elevation)
val (topIncr, bottomIncr) = when (gravity) {
TOP -> 0 to dp16
BOTTOM -> dp16 to 0
}
updateLayoutParams<MarginLayoutParams> {
setMargins(
/* left = */ marginLeft,
/* top = */ marginTop + topIncr,
/* right = */ marginRight,
/* bottom = */ marginBottom + bottomIncr
)
}
}
}
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
}
fun setBarBackgroundDrawable(drawable: Drawable?) {
if (drawable == null) return
this.fbRoot.background = drawable
}
fun setBarBackgroundColor(@ColorInt color: Int?) {
if (color == null) return
this.fbRoot.setBackgroundColor(color)
}
fun setBarTapListener(listener: OnTapListener?) {
if (listener == null) return
this.fbRoot.setOnClickListener { listener.onTap(parentFlashbarContainer.parentFlashbar) }
}
fun setTitle(title: String?) {
if (TextUtils.isEmpty(title)) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
fun setTitleSpanned(title: Spanned?) {
if (title == null) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
fun setTitleTypeface(typeface: Typeface?) {
if (typeface == null) return
fbTitle.typeface = typeface
}
fun setTitleSizeInPx(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setTitleSizeInSp(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setTitleColor(color: Int?) {
if (color == null) return
fbTitle.setTextColor(color)
}
fun setTitleAppearance(titleAppearance: Int?) {
if (titleAppearance == null) return
this.fbTitle.setTextAppearance(titleAppearance)
}
fun setMessage(message: String?) {
if (TextUtils.isEmpty(message)) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
fun setMessageSpanned(message: Spanned?) {
if (message == null) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
fun setMessageTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbMessage.typeface = typeface
}
fun setMessageSizeInPx(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setMessageSizeInSp(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setMessageColor(color: Int?) {
if (color == null) return
this.fbMessage.setTextColor(color)
}
fun setMessageAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbMessage.setTextAppearance(messageAppearance)
}
fun setPrimaryActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
fun setPrimaryActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
fun setPrimaryActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPrimaryAction.typeface = typeface
}
fun setPrimaryActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setPrimaryActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setPrimaryActionTextColor(color: Int?) {
if (color == null) return
this.fbPrimaryAction.setTextColor(color)
}
fun setPrimaryActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPrimaryAction.setTextAppearance(messageAppearance)
}
fun setPrimaryActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPrimaryAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
fun setPositiveActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
fun setPositiveActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
fun setPositiveActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPositiveAction.typeface = typeface
}
fun setPositiveActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setPositiveActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setPositiveActionTextColor(color: Int?) {
if (color == null) return
this.fbPositiveAction.setTextColor(color)
}
fun setPositiveActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPositiveAction.setTextAppearance(messageAppearance)
}
fun setPositiveActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPositiveAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
fun setNegativeActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
fun setNegativeActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
fun setNegativeActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbNegativeAction.typeface = typeface
}
fun setNegativeActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setNegativeActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setNegativeActionTextColor(color: Int?) {
if (color == null) return
this.fbNegativeAction.setTextColor(color)
}
fun setNegativeActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbNegativeAction.setTextAppearance(messageAppearance)
}
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
}
fun setIconDrawable(icon: Drawable?) {
if (icon == null) return
this.fbIcon.setImageDrawable(icon)
}
fun setIconBitmap(bitmap: Bitmap?) {
if (bitmap == null) return
this.fbIcon.setImageBitmap(bitmap)
}
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))
}
}
fun setProgressPosition(position: ProgressPosition?) {
if (position == null) return
when (position) {
LEFT -> {
fbLeftProgress.visibility = VISIBLE
fbRightProgress.visibility = GONE
}
RIGHT -> {
fbLeftProgress.visibility = GONE
fbRightProgress.visibility = VISIBLE
}
}
}
fun setProgressTint(progressTint: Int?, position: ProgressPosition?) {
if (position == null || progressTint == null) return
val progressBar =
when (position) {
LEFT -> fbLeftProgress
RIGHT -> fbRightProgress
}
progressBar.progressTintList = ColorStateList.valueOf(progressTint)
}
}
@@ -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,121 @@
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) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
}
)
}
internal enum class NavigationBarPosition {
BOTTOM,
RIGHT,
LEFT,
TOP
}
@@ -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>
@@ -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,142 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:fitsSystemWindows="true"
android:focusable="true"
app:cardBackgroundColor="?colorPrimaryContainer"
app:cardElevation="@dimen/fb_card_elevation"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom">
<LinearLayout
android:id="@+id/fbRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/fb_content_padding_all"
android:orientation="vertical">
<LinearLayout
android:id="@+id/fbContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fbLeftProgress"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_marginEnd="@dimen/fb_margin_progress"
android:layout_marginRight="@dimen/fb_margin_progress"
android:layout_gravity="center_vertical"
android:indeterminate="true"
android:visibility="gone" />
<ImageView
android:id="@+id/fbIcon"
android:layout_width="@dimen/fb_icon_size"
android:layout_height="@dimen/fb_icon_size"
android:layout_marginEnd="@dimen/fb_margin_right_icon"
android:layout_marginRight="@dimen/fb_margin_right_icon"
android:layout_gravity="center_vertical"
android:importantForAccessibility="no"
android:src="@drawable/ic_info"
android:visibility="gone"
app:tint="?colorOnPrimaryContainer"
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:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/fb_title_message_margin"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?colorOnPrimaryContainer"
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/TextAppearance.Material3.BodyMedium"
android:visibility="gone"
tools:text="A quick brown fox jumps of the lazy dog."
tools:visibility="visible" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/fbPrimaryAction"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:visibility="gone"
tools:text="CLICK"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fbRightProgress"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_marginStart="@dimen/fb_margin_progress"
android:layout_marginLeft="@dimen/fb_margin_progress"
android:layout_gravity="center_vertical"
android:indeterminate="true"
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.google.android.material.button.MaterialButton
android:id="@+id/fbNegativeAction"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="CANCEL"
tools:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fbPositiveAction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fb_margin_secondary_action"
android:layout_marginLeft="@dimen/fb_margin_secondary_action"
android:visibility="gone"
tools:text="OK"
tools:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -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,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fb_card_elevation">8dp</dimen>
<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>