用kotlin+compose开发通知弹幕APP(二)

一、弹幕实现理论分析

1、弹幕View应该如何设计

我们的设想,是在任意界面,通知消息来了,我们弹幕就可以出现。

所以承载弹幕的View是不可能放在Activity里面的,不然退出APP就没有弹幕了。

需要再手机上全场景覆盖,那符合我们要求的就是悬浮窗。悬浮窗可以脱离app,在手机桌面和其他应用的最上层显示,这更加符合我们的要求。所以我们就选定,悬浮窗作为弹幕的载体。

View的位置想好后,接下来就是弹幕应该如何设计布局。

首先我们可以将悬浮窗View宽度设置为match_parent,也就是与屏幕等宽。高度部分,为了避免通知多的时候,把整个手机屏幕铺满了的弹幕。我就把高度控制屏幕上方部分吧。

这样就可以将弹幕的范围,控制在手机屏幕上半区。弹幕多的时候,也不会因满屏的弹幕,从而影响手机正常使用。

2、弹幕效果如何设计

我计划将悬浮窗View的布局大概规划成7行。类似于写信的横格纸,我只预留出7行位置供TextView从右往左滑。

弹幕本身就是一个TextView文字,我们需要做的就是,有新系统通知时,将通知正文,创建成一个TextView对象,然后通过addView,将其作为在悬浮窗View的子控件。

TextView创建好后,给它从0 - 6里分配一个随机数,然后把TextView放在随机数对应的行上。这样就能得到弹幕随机高度的效果。

最后将TextView放在悬浮窗View对应行的最右侧,再给TextView一个动画,从右往左滑动就行了。

大体思路就是这样。是不是看着比较麻烦?跟着一步一步走,不会太麻烦的。

开干吧!

二、 创建悬浮窗View

Android的悬浮窗,就还是使用WindowManager。通过WindowManager去绑定一个View。

所以我们需要自定义一个View。

我就还是先贴代码,代码具体逻辑解释,我就在注释里面体现了。

1、自定义悬浮窗窗口

class FloatWindows(context: Context): View(context) {
   //View对象创建所需的上下文
    private lateinit var mContext: Context
    private lateinit var wm: WindowManager
    //属性对象
    private val wmParams = WindowManager.LayoutParams()
    public lateinit var mContentView: View
    public var isShowing = false //是否正在显示



    init {
        wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager  //获取窗口管理器
        mContext = context
    }

    //这里是将悬浮窗内部的布局,采用传入layout文件的形式。
    public fun setLayout(layoutId: Int){
        mContentView = LayoutInflater.from(mContext).inflate(layoutId, null) //获取自定义视图
    }
    
    //展示悬浮窗
    @SuppressLint("WrongConstant")
    public fun show(gravity: Int){
        if (mContentView != null){
         //设置悬浮窗类型,安卓8以下用TYPE_SYSTEM_ALERT,8及8以上用TYPE_APPLICATION_OVERLAY
            wmParams.type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_SYSTEM_ALERT else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        }
        wmParams.format = PixelFormat.RGBA_8888
        //设置悬浮窗属性,这里大概意思是,不拦截点击事件。
        //比如手机处于桌面的时候,我们悬浮窗布局是覆盖手机上半部分的
        //这代表,手机由一部分APP图标是被悬浮窗覆盖住的
        //如果不给悬浮窗添加这个属性,则用户点击APP图标的时候,实际上都是点在了悬浮窗上。
        //点击事件被悬浮窗响应了,则桌面并没有收到点击APP的事件,那就出现用户点不了图标的情况
        //所以我们要声明悬浮窗不对点击事件做响应,让事件直接透传到桌面图标上。
        wmParams.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                or WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW)
        wmParams.alpha = 0.8f  //透明度
        wmParams.gravity = gravity
        //布局位置
        wmParams.x = 0
        wmParams.y = 0
        wmParams.width = WindowManager.LayoutParams.MATCH_PARENT
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT
        //给WindowManager添加悬浮窗布局,显示悬浮窗
        wm.addView(mContentView, wmParams)
        isShowing = true
    }
     
      //关闭悬浮窗
    public fun close(){
        if (mContentView != null){
            wm.removeView(mContentView)
            isShowing = false
        }
    }
}

2、自定义悬浮窗布局

我暂时没摸清楚,在WindowManager里,对compose的用法,所以悬浮窗这里还是用XML布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!--这里自定义绘制了弹幕View -->
    <com.liuzi.mynotice.view.BarrageView
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:id="@+id/barrage"/>

</LinearLayout>

3、自定义弹幕View布局

class BarrageView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
    private lateinit var mContext: Context
    //弹幕布局分为七行
    private val mRowCount = 7
    //TextView的字体大小
    private val mTextSize = 15
    //每一条弹幕的字数上限
    private val mMaxLength = 22
    //存储7行TextView的layout属性的list
    private val mLayoutList = mutableListOf<RelativeLayout>()
    //BarrageView的宽度
    private var mWidth = 0
    //记录上一条弹幕所处的行数
    private var mLastPos1 = -1
    //记录上上一条弹幕所处的行数
    private var mLastPos2 = -1


    init {
        initView(context!!)
    }
    
    //初始化弹幕布局
    private fun initView(context: Context) {
        mContext = context
        //整个弹幕View为竖向线性布局
        orientation = LinearLayout.VERTICAL
        //弹幕View内部,布局为竖向的7行RelativeLayout,也就是弹幕的7条“跑道”
        //弹幕每条“跑道”,宽度都是MATCH_PARENT,高度写死为40dp
        for (i in (0 until mRowCount)) {
            val layout = RelativeLayout(mContext)
            //layout.setPadding(0, 100, 0, 0)
            val param =
                RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, dp2Px(context, 40.toFloat()))
            layout.layoutParams = param
            mLayoutList.add(layout)
            //将所有“跑道”添加入布局
            addView(layout)
        }
    }
    
    //给弹幕随机分配“跑道”
    private fun getPos(): Int {
        var pos = 0
        do {
            Random.nextInt(mRowCount).also { pos = it }
        } while (pos == mLastPos1 || pos == mLastPos2) //避开上一条和上上一条的“跑道”,让弹幕分布均匀一些
        mLastPos2 = mLastPos1 //更新记录的历史跑道位置
        mLastPos1 = pos
        return pos //返回分配的跑道编号
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mWidth = measuredWidth  //获取弹幕View的实际测量宽度值(MATCH_PARENT)
    }


    //给弹幕View添加弹幕
    //入参:comment:系统通知正文,pkg:通知所属APP的包名
    @RequiresApi(Build.VERSION_CODES.HONEYCOMB)
    public fun addComment(comment: String, pkg: String) {
        //获取新分配的跑道编号,通过编号,获取对应跑道的layout
        val layout = mLayoutList.get(getPos())
        //创建弹幕对象
        val tv_comment = getCommentView(comment, pkg)
        //弹幕未开始滑动前,先设置为不可见
        tv_comment.visibility = INVISIBLE
        //将弹幕对象塞入跑道
        layout.addView(tv_comment)
        
        tv_comment.post(Runnable {  
             // 获得弹幕的宽度,单位px
            val textWidth: Int = tv_comment.width
            tv_comment.visibility = VISIBLE

            //设置估值器,通过估值器动态计算弹幕的属性,并实时调节,让弹幕跑起来
            //入参为需要计算的启动数值和终点数值。
            //此时mWidth代表手机屏幕宽度值,-textWidth代表负的弹幕宽度
            val anim = ValueAnimator.ofInt(mWidth, -textWidth)
            //估值器动画的刷新监听
            anim.addUpdateListener { animation: ValueAnimator ->
                //获取当前弹幕对象属性
                val tv_params = tv_comment.layoutParams as RelativeLayout.LayoutParams
                //根据计算值,修改弹幕的leftMargin 
                tv_params.leftMargin = animation.getAnimatedValue() as Int
                //由于弹幕的leftMargin会被估值器实时修改
                //假设屏幕宽度100,弹幕宽度5,则弹幕的左侧Margin,会从100变成负5
                //这代表,弹幕会从屏幕最右侧出现,然后逐渐滑动到左边,直至划出屏幕外
                tv_comment.setLayoutParams(tv_params)

            }
            //设置动画的播放目标
            anim.setTarget(tv_comment)
            //setDuration设置弹幕的播放时长,时间越长,弹幕跑的越慢
            //这里的判断是对设备是否横屏做了判断
            //横屏的设备,屏幕太宽,弹幕在相同的播放时长下,会跑得很快
            //所以针对横屏,播放时长要长一点
            if (DeviceStateUtils.isScreenOrienatation(mContext)) {
                anim.setDuration(40000)
            }else{
                anim.setDuration(20000)
            }
            //设置估值器的动画属性为线性
            anim.interpolator = LinearInterpolator()
            //启动动画
            anim.start()
            //这里对估值器又做了一个监听,当动画结束的时候,就结束动作,清除弹幕对象
            anim.addListener { animator ->
                tv_comment.clearAnimation()
                removeView(tv_comment) //移除弹幕
                animation.cancel()
            }

        })


    }

    //我设计的弹幕布局,是一个app图标加一个通知正文
    //此方法为创建弹幕对象
    private fun getCommentView(content: String, pkg: String): LinearLayout {
        //ImageView,APP图标
        val icon = ImageView(mContext) 
        //TextView,通知正文
        val tv = TextView(mContext)  
        //线性横向布局,图标在前,文字在后
        val linearLayout = LinearLayout(mContext)  
        linearLayout.orientation = HORIZONTAL
        linearLayout.gravity = Gravity.CENTER
        //通过包名获取图片对象,设置进ImageView
        icon.setImageDrawable(mContext.getPackageManager().getApplicationIcon(pkg)) 
        icon.scaleType = ImageView.ScaleType.FIT_CENTER
        icon.adjustViewBounds = true
        icon.maxHeight = 100
        icon.maxWidth = 100
        tv.setPadding(30, 0, 0, 0)
        //如果通知文字过长,则进行截取,以省略号拼接结尾
        tv.text = if (content.length > mMaxLength) content.substring(0, mMaxLength)
            .plus("...") else content
        tv.setTextSize(mTextSize.toFloat())
        tv.setTextColor(Color.WHITE)
        //设置一点阴影,让弹幕在各种背景下都容易能看到
        tv.setShadowLayer(0.3f, 2f, 2f, Color.BLACK)
        tv.setSingleLine(true)
        val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        //将设置好的ImageView和TextView,放入线性布局,返回线性布局View。高度和宽度都是WRAP_CONTENT
        linearLayout.addView(icon)
        linearLayout.addView(tv)
        linearLayout.layoutParams = params
        return linearLayout
    }

    companion object {
        fun px2dp(context: Context, pxValue: Float): Int {
            val scale = context.resources.displayMetrics.density
            return (pxValue / scale + 0.5f).toInt()
        }

        fun dp2Px(context: Context, dp: Float): Int {
            val scale = context.resources.displayMetrics.density
            return Math.round(dp * scale);
        }
    }
}

4、判断设备横屏

class DeviceStateUtils {
    //  companion object内的声明方法,即代表静态方法
    companion object{
        fun isScreenOrienatation(mContext: Context): Boolean {
            val mConfiguration = mContext.resources.configuration
            return mConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE
        }
    }
}

5、弹幕启用

弹幕开启,咱们可以直接放在activity启动的时候。

步骤很简单,创建FloatWindow对象初始化,然后调用show方法就可以了。代码如下:

            private var mFloatWindows: FloatWindows? = null

            if (mFloatWindows == null) {
                mFloatWindows = FloatWindows(mContext)
                mFloatWindows?.setLayout(R.layout.float_window)
                mBarrageView = mFloatWindows?.mContentView?.findViewById(R.id.barrage)
            }
            mFloatWindows?.let {
                if (!it.isShowing) {
                    it.show(Gravity.LEFT or Gravity.TOP)
                    mFloatShow = true
                    mBarrageView?.addComment(
                        "通知弹幕功能开启成功,这是一条测试弹幕",
                        "com.liuzi.mynotice"
                    )
                }
            }

初始化完成好了,那后面怎么把通知消息和弹幕串起来呢?

那还不简单吗,我们回到MyNotificationService,把获取系统通知的时候,直接调用一下弹幕mBarrageView?.addComment就可以了!

//重写消息监听的方法
    override fun onNotificationPosted(sbn: StatusBarNotification?) {
        //super.onNotificationPosted(sbn)
        try {
            if (sbn!!.notification.tickerText != null) {
            
                //本次新增逻辑
                if (mFloatShow) {
                    mBarrageView?.addComment(
                        sbn.notification.extras.getString(
                            Notification.EXTRA_TEXT
                        ).toString(),
                        sbn.packageName
                    )
                }
                
                val notice = toNotice(sbn)
                GlobalScope.launch(Dispatchers.IO) {
                    saveNotice(notice)
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "保存失败:$e")
        }
    }

三、 别忘了悬浮窗所需要的权限

1、AndroidMenifest声明权限

	<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

2、动态权限

悬浮窗需要系统授权,允许应用“显示在其他应用上层”。

这个权限无法直接弹窗让用户授权,只能让用户去系统设置里面给app打开。

这部分代码我就没写了,可以自行去系统设置里面为app授权。

四、总结

本次我们算是完成最核心的弹幕内容,相关逻辑说明,在注释里面都解释的差不多了。看看大家还有没有疑问。如果大家有什么疑问或建议的话,欢迎大家在评论区交流。

需要注意的一点是,无论是悬浮窗,还是自定义弹幕View,这部分我们都在和View打交道。所以Context对象十分重要,大家在调用弹幕的时候,一定要注意Context的传递和使用。既不要出现context空指针导致应用崩溃,也不要因为context引用问题造成内存泄漏。

对了,我们还遗漏了一个开启关闭弹幕的button没有实现,如果后面大家评论区疑问比较多的话,那我再续一篇,把大家疑问做一个探讨,顺便把button的对应功能的代码实现补上。

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin是一种静态类型的编程语言,具有JVM的可移植性和Java的互操作性。Spring Boot是一个用于创建独立的、基于Spring框架的Java应用程序的框架,它提供了快速开发应用程序所需的所有功能。JavaFX是一个用于创建丰富客户端应用程序的框架,它提供了丰富的UI组件和布局管理器。 要使用Kotlin Spring Boot和JavaFX开发桌面应用程序,需要完成以下步骤: 1. 创建一个Kotlin Spring Boot项目。可以使用Spring Initializr创建项目,选择Kotlin和Spring Web依赖项。 2. 添加JavaFX依赖项。可以在pom.xml文件中添加以下依赖项: ``` <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>16</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>16</version> </dependency> ``` 3. 创建一个JavaFX应用程序类。可以使用JavaFX的Application类作为应用程序的入口点。在这个类中,可以创建UI组件,处理事件和管理应用程序的状态。以下是一个简单的JavaFX应用程序类的示例: ```kotlin import javafx.application.Application import javafx.fxml.FXMLLoader import javafx.scene.Parent import javafx.scene.Scene import javafx.stage.Stage class MyApplication : Application() { override fun start(primaryStage: Stage?) { val root: Parent = FXMLLoader.load(javaClass.getResource("/fxml/main.fxml")) primaryStage?.title = "My Application" primaryStage?.scene = Scene(root) primaryStage?.show() } companion object { @JvmStatic fun main(args: Array<String>) { launch(MyApplication::class.java, *args) } } } ``` 4. 创建FXML布局文件。FXML是一种XML格式的文件,用于定义UI组件和布局。可以使用Scene Builder或手动创建FXML文件。以下是一个简单的FXML布局文件的示例: ```xml <?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.Button?> <?import javafx.scene.layout.AnchorPane?> <AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="root" prefHeight="400" prefWidth="600"> <Button fx:id="button" text="Click me" layoutX="250" layoutY="180" /> </AnchorPane> ``` 5. 在JavaFX应用程序类中加载FXML布局文件。可以使用FXMLLoader类加载FXML布局文件,并将其添加到应用程序的场景图中。以下是一个示例: ```kotlin val root: Parent = FXMLLoader.load(javaClass.getResource("/fxml/main.fxml")) primaryStage?.title = "My Application" primaryStage?.scene = Scene(root) primaryStage?.show() ``` 6. 处理UI事件。可以在JavaFX应用程序类中添加事件处理程序,以响应UI组件的事件。以下是一个处理按钮单击事件的示例: ```kotlin button.setOnAction { event -> println("Button clicked!") } ``` 7. 使用Spring Boot管理应用程序的状态。可以使用Spring Boot的依赖注入和管理功能来管理应用程序的状态和依赖关系。可以在Spring Boot的配置类中定义bean,然后在JavaFX应用程序类中使用它们。以下是一个简单的Spring Boot配置类的示例: ```kotlin @Configuration class AppConfig { @Bean fun myService(): MyService { return MyService() } } ``` 8. 在JavaFX应用程序类中使用Spring Boot的依赖注入功能。可以在JavaFX应用程序类的构造函数中注入Spring Boot管理的bean。以下是一个示例: ```kotlin class MyApplication : Application() { @Autowired lateinit var myService: MyService // ... } ``` 这就是使用Kotlin Spring Boot和JavaFX开发桌面应用程序的基本步骤。当然,还有很多其他的细节和技术,可以根据需要进行学习和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值