mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: replace Toaster with Flashbar
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
+298
@@ -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)
|
||||
}
|
||||
}
|
||||
+200
@@ -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)
|
||||
}
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
+140
@@ -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
|
||||
}
|
||||
}
|
||||
+90
@@ -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)
|
||||
}
|
||||
}
|
||||
+125
@@ -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>
|
||||
Reference in New Issue
Block a user