自定义Toast工具类AToast:打造优雅的Android提示体验
自定义Toast工具类AToast:打造优雅的Android提示体验
概述
在Android开发中,Toast是常用的用户提示工具,但系统自带的Toast功能有限,样式单调。今天我们来介绍一款功能强大的自定义Toast工具类——AToast,它不仅支持多种动画效果,还能完全自定义布局,为你的应用增添独特的视觉体验。
主要特性
1. 多种动画效果
AToast内置了四种精美的动画效果:
- BOTTOM_BOUNCE: 底部弹出,带超调回弹效果
- TOP_BOUNCE: 顶部弹出,带超调回弹效果
- CENTER_SCALE: 中心缩放,带弹性效果
- FADE_IN_OUT: 淡入淡出效果
2. 灵活的显示位置
支持任意位置显示,可以设置重力、X/Y偏移量,满足不同场景需求。
3. 内存安全
通过Context映射管理Toast实例,自动处理生命周期,避免内存泄漏。
4. 完全自定义
不仅可以使用默认样式,还能传入任意自定义View,实现完全个性化的Toast。
核心设计
单例模式与静态方法
class AToast private constructor() {
companion object {
fun show(context: Context, message: CharSequence, ...)
fun show(context: Context, view: View, config: ToastConfig)
fun dismiss(context: Context)
fun dismissAll()
}
}
这种设计使得调用非常简洁,无需创建实例即可使用。
配置化设计
通过ToastConfig数据类封装所有配置项:
data class ToastConfig(
val duration: Long = 2000L,
val gravity: Int = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL,
val xOffset: Int = 0,
val yOffset: Int = 100,
val animationType: AnimationType = AnimationType.BOTTOM_BOUNCE
)
使用示例
基本使用
// 显示默认样式的Toast
AToast.show(context, "操作成功")
// 显示顶部Toast,持续3秒
AToast.show(context, "新消息", 3000L, AToast.AnimationType.TOP_BOUNCE)
// 显示中心缩放Toast
AToast.show(context, "加载中", type = AToast.AnimationType.CENTER_SCALE)
自定义布局
val customView = LayoutInflater.from(context).inflate(R.layout.custom_toast, null)
val config = AToast.ToastConfig(
duration = 3000L,
gravity = Gravity.TOP or Gravity.RIGHT,
xOffset = 20,
yOffset = 100,
animationType = AToast.AnimationType.FADE_IN_OUT
)
AToast.show(context, customView, config)
生命周期管理
// 移除特定Context的所有Toast
override fun onDestroy() {
super.onDestroy()
AToast.dismiss(context)
}
// 移除所有Toast
fun clearAllToasts() {
AToast.dismissAll()
}
动画实现原理
底部弹跳动画
private fun applyBottomBounceAnimation(view: View, isEnter: Boolean, onEnd: (() -> Unit)?) {
val translationY = if (isEnter) {
view.translationY = view.height.toFloat()
ObjectAnimator.ofFloat(view, "translationY", view.height.toFloat(), 0f)
} else {
ObjectAnimator.ofFloat(view, "translationY", 0f, view.height.toFloat())
}
translationY.apply {
duration = if (isEnter) 500L else 300L
interpolator = if (isEnter) OvershootInterpolator(1.5f)
else AccelerateDecelerateInterpolator()
// ... 动画监听
}
}
中心缩放动画
使用AnimatorSet同时控制X和Y轴的缩放,配合BounceInterpolator实现弹性效果。
默认样式实现
ToastLayoutCreator负责创建默认的系统Toast样式:
- 半透明黑色圆角背景
- 白色文字带阴影
- 自适应宽度(最小30dp,最大300dp)
- 自动省略长文本
最佳实践
1. 统一管理
建议在基类Activity中统一管理Toast的显示和移除,避免Toast在不当的时机显示。
2. 动画选择
- 成功提示:使用
BOTTOM_BOUNCE或CENTER_SCALE - 错误提示:使用
FADE_IN_OUT - 重要通知:使用
TOP_BOUNCE吸引用户注意
3. 时长设置
- 短消息:1500-2000ms
- 中等长度:2000-3000ms
- 长消息:3000-4000ms
4. 内存优化
AToast使用WeakReference模式管理Toast实例,但仍需注意:
- 在Activity销毁时调用
dismiss() - 避免在频繁调用的地方创建大量Toast
扩展建议
如果需要进一步扩展功能,可以考虑:
- 队列管理: 实现Toast队列,避免多个Toast重叠
- 优先级系统: 为不同重要性的Toast设置优先级
- 图标支持: 在默认样式中支持图标显示
- 主题适配: 根据应用主题自动适配Toast样式
总结
AToast工具类通过简洁的API、丰富的动画效果和灵活的自定义能力,极大地提升了Android应用中Toast的用户体验。其良好的内存管理和生命周期处理,让开发者可以放心使用,专注于业务逻辑的实现。
无论是简单的文本提示,还是复杂的自定义布局,AToast都能完美胜任,是提升应用交互体验的绝佳选择。
希望这个工具类能为你的Android开发带来便利!如果有任何问题或建议,欢迎在评论区留言讨论。
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.BounceInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.toColorInt
/**
* 自定义Toast工具类
*/
class AToast private constructor() {
companion object {
// 使用弱引用包装器来避免内存泄漏
private val toastMap = mutableMapOf<Context, CustomToast?>()
/**
* 显示Toast
*/
fun show(context: Context,
message: CharSequence,
duration: Long = 2000L,
type: AnimationType = AnimationType.BOTTOM_BOUNCE) {
toastMap[context] = CustomToast(context).apply {
setText(message)
setDuration(duration)
when (type) {
AnimationType.BOTTOM_BOUNCE -> {
setAnimationType(AnimationType.BOTTOM_BOUNCE)
setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, 100)
}
AnimationType.TOP_BOUNCE -> {
setAnimationType(AnimationType.TOP_BOUNCE)
setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL, 0, 100)
}
AnimationType.CENTER_SCALE -> {
setAnimationType(AnimationType.CENTER_SCALE)
setGravity(Gravity.CENTER, 0, 0)
}
else -> {
setAnimationType(AnimationType.FADE_IN_OUT)
setGravity(Gravity.CENTER, 0, 0)
}
}
show()
}
}
/**
* 显示自定义布局Toast
*/
fun show(
context: Context,
view: View,
config: ToastConfig
) {
toastMap[context] = CustomToast(context).apply {
setCustomView(view)
setDuration(config.duration)
setAnimationType(config.animationType)
setGravity(config.gravity, config.xOffset, config.yOffset)
show()
}
}
/**
* 移除指定Context的所有Toast
*/
fun dismiss(context: Context) {
toastMap[context]?.dismiss()
toastMap.remove(context)
}
/**
* 移除所有Toast
*/
fun dismissAll() {
toastMap.values.forEach { it?.dismiss() }
toastMap.clear()
}
}
/**
* 非静态内部类,持有对Context的引用但通过生命周期管理
*/
class CustomToast(private val context: Context) {
private var windowManager: WindowManager? = null
private var toastView: View? = null
private val handler = Handler(Looper.getMainLooper())
private var removeRunnable: Runnable? = null
private var duration: Long = 2000L
private var gravity: Int = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
private var xOffset: Int = 0
private var yOffset: Int = 100
private var animationType: AnimationType = AnimationType.BOTTOM_BOUNCE
private val layoutParams = WindowManager.LayoutParams().apply {
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
format = android.graphics.PixelFormat.TRANSLUCENT
gravity = this@CustomToast.gravity
x = xOffset
y = yOffset
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
}
/**
* 设置文本
*/
fun setText(text: CharSequence): CustomToast {
toastView = ToastLayoutCreator.createCompleteToastView(context, text)
return this
}
/**
* 设置自定义视图
*/
fun setCustomView(view: View): CustomToast {
toastView = view
return this
}
/**
* 设置显示时长
*/
fun setDuration(duration: Long): CustomToast {
this.duration = duration
return this
}
/**
* 设置动画类型
*/
fun setAnimationType(type: AnimationType): CustomToast {
this.animationType = type
return this
}
/**
* 设置位置
*/
fun setGravity(gravity: Int, xOffset: Int, yOffset: Int): CustomToast {
this.gravity = gravity
this.xOffset = xOffset
this.yOffset = yOffset
layoutParams.gravity = gravity
layoutParams.x = xOffset
layoutParams.y = yOffset
return this
}
/**
* 显示Toast
*/
fun show() {
val view = toastView ?: return
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
// 应用进入动画
applyAnimation(view, animationType, true)
// 添加到窗口
windowManager?.addView(view, layoutParams)
// 设置定时移除
removeRunnable = Runnable {
applyAnimation(view, animationType, false) {
dismiss()
}
}
handler.postDelayed(removeRunnable!!, duration)
}
/**
* 移除Toast
*/
fun dismiss() {
removeRunnable?.let { handler.removeCallbacks(it) }
removeRunnable = null
toastView?.let { view ->
windowManager?.removeView(view)
toastView = null
}
windowManager = null
}
/**
* 应用动画
*/
private fun applyAnimation(
view: View,
animationType: AnimationType,
isEnter: Boolean,
onEnd: (() -> Unit)? = null
) {
when (animationType) {
AnimationType.BOTTOM_BOUNCE -> applyBottomBounceAnimation(view, isEnter, onEnd)
AnimationType.TOP_BOUNCE -> applyTopBounceAnimation(view, isEnter, onEnd)
AnimationType.CENTER_SCALE -> applyCenterScaleAnimation(view, isEnter, onEnd)
AnimationType.FADE_IN_OUT -> applyFadeAnimation(view, isEnter, onEnd)
}
}
private fun applyBottomBounceAnimation(view: View, isEnter: Boolean, onEnd: (() -> Unit)?) {
val translationY = if (isEnter) {
view.translationY = view.height.toFloat()
ObjectAnimator.ofFloat(view, "translationY", view.height.toFloat(), 0f)
} else {
ObjectAnimator.ofFloat(view, "translationY", 0f, view.height.toFloat())
}
translationY.apply {
duration = if (isEnter) 500L else 300L
interpolator =
if (isEnter) OvershootInterpolator(1.5f) else AccelerateDecelerateInterpolator()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onEnd?.invoke()
}
})
start()
}
}
private fun applyTopBounceAnimation(view: View, isEnter: Boolean, onEnd: (() -> Unit)?) {
val translationY = if (isEnter) {
view.translationY = -view.height.toFloat()
ObjectAnimator.ofFloat(view, "translationY", -view.height.toFloat(), 0f)
} else {
ObjectAnimator.ofFloat(view, "translationY", 0f, -view.height.toFloat())
}
translationY.apply {
duration = if (isEnter) 500L else 300L
interpolator =
if (isEnter) OvershootInterpolator(1.5f) else AccelerateDecelerateInterpolator()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onEnd?.invoke()
}
})
start()
}
}
private fun applyCenterScaleAnimation(view: View, isEnter: Boolean, onEnd: (() -> Unit)?) {
val scaleX = if (isEnter) {
view.scaleX = 0f
view.scaleY = 0f
ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f)
} else {
ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f)
}
val scaleY = if (isEnter) {
ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f)
} else {
ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f)
}
val animatorSet = android.animation.AnimatorSet()
animatorSet.playTogether(scaleX, scaleY)
animatorSet.apply {
duration = if (isEnter) 400L else 300L
interpolator =
if (isEnter) BounceInterpolator() else AccelerateDecelerateInterpolator()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onEnd?.invoke()
}
})
start()
}
}
private fun applyFadeAnimation(view: View, isEnter: Boolean, onEnd: (() -> Unit)?) {
val alpha = if (isEnter) {
view.alpha = 0f
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
} else {
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
}
alpha.apply {
duration = 300L
interpolator = AccelerateDecelerateInterpolator()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onEnd?.invoke()
}
})
start()
}
}
}
/**
* 动画类型
*/
enum class AnimationType {
BOTTOM_BOUNCE, // 底部弹出,回弹效果
TOP_BOUNCE, // 顶部弹出,回弹效果
CENTER_SCALE, // 中心缩放效果
FADE_IN_OUT, // 淡入淡出
}
/**
* Toast配置
*/
data class ToastConfig(
val duration: Long = 2000L,
val gravity: Int = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL,
val xOffset: Int = 0,
val yOffset: Int = 100,
val animationType: AnimationType = AnimationType.BOTTOM_BOUNCE
)
/**
* 动态创建系统Toast样式的布局
*/
object ToastLayoutCreator {
/**
* 创建Toast的TextView
*/
private fun createToastTextView(context: Context, message: CharSequence): TextView {
return TextView(context).apply {
text = message
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
weight = 1f
minWidth = dpToPx(context, 30)
maxWidth = dpToPx(context, 300)
ellipsize = TextUtils.TruncateAt.END
gravity = Gravity.CENTER_HORIZONTAL
}
// 设置文本外观
setDefaultToastTextAppearance()
// 设置文字颜色
setTextColor(Color.WHITE)
// 设置阴影
setShadowLayer(2.75f, 0f, 0f, "#BB000000".toColorInt())
// 设置内边距
val padding = dpToPx(context, 32)
setPadding(padding, padding / 2, padding, padding / 2)
}
}
/**
* 创建默认Toast背景
*/
private fun createDefaultToastBackground(context: Context): android.graphics.drawable.Drawable {
val radius = dpToPx(context, 100).toFloat()
return android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
cornerRadius = radius
setColor("#99000000".toColorInt()) // 半透明黑色
}
}
/**
* 设置默认Toast文本样式
*/
private fun TextView.setDefaultToastTextAppearance() {
textSize = 12f // SP
gravity = Gravity.CENTER
includeFontPadding = false
}
/**
* dp转px
*/
private fun dpToPx(context: Context, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
/**
* 创建完整的系统样式Toast视图
*/
fun createCompleteToastView(
context: Context,
message: CharSequence,
): View {
// 创建外层容器(模拟Toast的根布局)
val container = LinearLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (message.length < 15) {
background = createDefaultToastBackground(context)
}
orientation = LinearLayout.VERTICAL
setPadding(0, 0, 0, 0)
}
// 创建Toast内容布局
val toastView = createToastTextView(context, message)
// 添加到容器
container.addView(toastView)
return container
}
}
}
浙公网安备 33010602011771号