悬浮窗的一种实现 | Android悬浮窗Window应用

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文以业务应用为出发点,从零开始抽象一个浮窗工具类,它用于在任意业务界面上展示悬浮窗。它可以同时管理多个浮窗,而且浮窗可以响应触摸事件,可拖拽,有贴边动画。

本文以业务应用为出发点,从零开始抽象一个浮窗工具类,它用于在任意业务界面上展示悬浮窗。它可以同时管理多个浮窗,而且浮窗可以响应触摸事件,可拖拽,有贴边动画。

效果如下:
float window

这是 Android Window 应用的第一篇,系列文章目录如下:

  1. 悬浮窗的一种实现 | Android悬浮窗Window应用
  2. 下沉式通知的一种实现 | Android悬浮窗Window应用

显示浮窗

原生ViewManager接口提供了向窗口添加并操纵View的方法:

public interface ViewManager{
    //'向窗口添加视图'
    public void addView(View view, ViewGroup.LayoutParams params);
    //'更新窗口中视图'
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    //'移除窗口中视图'
    public void removeView(View view);
}

使用这个接口显示窗口的模版代码如下:

//'解析布局文件为视图'
val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)
//'获取WindowManager系统服务'
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
//'构建窗口布局参数'
WindowManager.LayoutParams().apply {
    type = WindowManager.LayoutParams.TYPE_APPLICATION
    width = WindowManager.LayoutParams.WRAP_CONTENT
    height = WindowManager.LayoutParams.WRAP_CONTENT
    gravity = Gravity.START or Gravity.TOP
    x = 0
    y = 0
}.let { layoutParams->
    //'将视图添加到窗口'
    windowManager.addView(windowView, layoutParams)
}
  • 上述代码在当前界面的左上角显示R.id.window_view.xml中定义的布局。
  • 为避免重复,将这段代码抽象成一个函数,其中窗口视图内容和展示位置会随着需求而变,遂将其参数化:
object FloatWindow{
    private var context: Context? = null
    //'当前窗口参数'
    var windowInfo: WindowInfo? = null
    
    //'把和Window布局有关的参数打包成一个内部类'
    class WindowInfo(var view: View?) {
        var layoutParams: WindowManager.LayoutParams? = null
        //'窗口宽'
        var width: Int = 0
        //'窗口高'
        var height: Int = 0
        //'窗口中是否有视图'
        fun hasView() = view != null && layoutParams != null
        //'窗口中视图是否有父亲'
        fun hasParent() = hasView() && view?.parent != null
    }
    
    //'显示窗口'
    fun show(
        context: Context,
        windowInfo: WindowInfo?,
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value(),
    ) {
        if (windowInfo == null) { return }
        if (windowInfo.view == null) { return }
        this.windowInfo = windowInfo
        this.context = context
        //'创建窗口布局参数'
        windowInfo.layoutParams = createLayoutParam(x, y)
        //'显示窗口'
        if (!windowInfo.hasParent().value()) {
            val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }
    
    //'创建窗口布局参数'
    private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {
        if (context == null) { return WindowManager.LayoutParams() }
        return WindowManager.LayoutParams().apply {
            //'该类型不需要申请权限'
            type = WindowManager.LayoutParams.TYPE_APPLICATION
            format = PixelFormat.TRANSLUCENT
            flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
            gravity = Gravity.START or Gravity.TOP
            width = windowInfo?.width.value()
            height = windowInfo?.height.value()
            this.x = x
            this.y = y
        }
    }
    
    //'为空Int提供默认值'
    fun Int?.value() = this ?: 0
}
  • FloatWindow声明成了单例,目的是在 app 整个生命周期,任何界面都可以方便地显示浮窗。
  • 为了方便统一管理窗口的参数,抽象了内部类WindowInfo
  • 现在就可以像这样在屏幕左上角显示一个浮窗了:
val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)
WindowInfo(windowView).apply{
    width = 100
    height = 100
}.let{ windowInfo ->
    FloatWindow.show(context, windowInfo, 0, 0)
}

浮窗背景色

产品要求当浮窗显示时,屏幕变暗。设置WindowManager.LayoutParams.FLAG_DIM_BEHIND标签配合dimAmount就能轻松实现:

object FloatWindow{
    //当前窗口参数
    var windowInfo: WindowInfo? = null
    
    private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {
        if (context == null) { return WindowManager.LayoutParams() }

        return WindowManager.LayoutParams().apply {
            type = WindowManager.LayoutParams.TYPE_APPLICATION
            format = PixelFormat.TRANSLUCENT
            flags =
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                //'设置浮窗背景变暗'
                WindowManager.LayoutParams.FLAG_DIM_BEHIND
            //'设置默认变暗程度为0,即不变暗,1表示全黑'
            dimAmount = 0f
            gravity = Gravity.START or Gravity.TOP
            width = windowInfo?.width.value()
            height = windowInfo?.height.value()
            this.x = x
            this.y = y
        }
    }
    
    //'供业务界面在需要的时候调整浮窗背景亮暗'
    fun setDimAmount(amount:Float){
        windowInfo?.layoutParams?.let { it.dimAmount = amount }
    }
}

设置浮窗点击事件

为浮窗设置点击事件等价于为浮窗视图设置点击事件,但如果直接对浮窗视图使用setOnClickListener()的话,浮窗的触摸事件就不会被响应,那拖拽就无法实现。所以只能从更底层的触摸事件着手:

object FloatWindow : View.OnTouchListener{ 
    //'显示窗口'
    fun show(
        context: Context,
        windowInfo: WindowInfo?,
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value(),
    ) {
        if (windowInfo == null) { return }
        if (windowInfo.view == null) { return }
        this.windowInfo = windowInfo
        this.context = context
        //'为浮窗视图设置触摸监听器'
        windowInfo.view?.setOnTouchListener(this)
        windowInfo.layoutParams = createLayoutParam(x, y)
        if (!windowInfo.hasParent().value()) {
            val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        return false
    }
}
  • onTouch(v: View, event: MotionEvent)中可以拿到更详细的触摸事件,比如ACTION_DOWNACTION_MOVEACTION_UP。这方便了拖拽的实现,但点击事件的捕获变得复杂,因为需要定义上述三个 ACTION 以怎样的序列出现时才判定为点击事件。幸好GestureDetector为我们做了这件事:
public class GestureDetector {
    public interface OnGestureListener {
        //'ACTION_DOWN事件'
        boolean onDown(MotionEvent e);
        //'单击事件'
        boolean onSingleTapUp(MotionEvent e);
        //'拖拽事件'
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        ...
    }
}

构建GestureDetector实例并将MotionEvent传递给它就能将触摸事件解析成感兴趣的上层事件:

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
    private var clickListener: WindowClickListener? = null
    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    
    //'为浮窗设置点击监听器'
    fun setClickListener(listener: WindowClickListener) {
        clickListener = listener
    }
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        //'将触摸事件传递给 GestureDetector 解析'
        gestureDetector.onTouchEvent(event)
        return true
    }
    
    //'记忆起始触摸点坐标'
    private fun onActionDown(event: MotionEvent) {
        lastTouchX = event.rawX.toInt()
        lastTouchY = event.rawY.toInt()
    }

    private class GestureListener : GestureDetector.OnGestureListener {
        //'记忆起始触摸点坐标'
        override fun onDown(e: MotionEvent): Boolean {
            onActionDown(e)
            return false
        }
        
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            //'点击事件发生时,调用监听器'
            return clickListener?.onWindowClick(windowInfo) ?: false
        }

        ...
    }
    
    //'浮窗点击监听器'
    interface WindowClickListener {
        fun onWindowClick(windowInfo: WindowInfo?): Boolean
    }
}

拖拽浮窗

ViewManager提供了updateViewLayout(View view, ViewGroup.LayoutParams params)用于更新浮窗位置,所以只需监听ACTION_MOVE事件并实时更新浮窗视图位置就可实现拖拽。ACTION_MOVE事件被GestureDetector解析成OnGestureListener.onScroll()回调:

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        //'将触摸事件传递给GestureDetector解析'
        gestureDetector.onTouchEvent(event)
        return true
    }

    private class GestureListener : GestureDetector.OnGestureListener {
        override fun onDown(e: MotionEvent): Boolean {
            onActionDown(e)
            return false
        }

        override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY:Float): Boolean {
            //'响应手指滚动事件'
            onActionMove(e2)
            return true
        }
    }
    
    private fun onActionMove(event: MotionEvent) {
        //'获取当前手指坐标'
        val currentX = event.rawX.toInt()
        val currentY = event.rawY.toInt()
        //'获取手指移动增量'
        val dx = currentX - lastTouchX
        val dy = currentY - lastTouchY
        //'将移动增量应用到窗口布局参数上'
        windowInfo?.layoutParams!!.x += dx
        windowInfo?.layoutParams!!.y += dy
        val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        var rightMost = screenWidth - windowInfo?.layoutParams!!.width
        var leftMost = 0
        val topMost = 0
        val bottomMost = screenHeight - windowInfo?.layoutParams!!.height - getNavigationBarHeight(context)
        //'将浮窗移动区域限制在屏幕内'
        if (windowInfo?.layoutParams!!.x < leftMost) {
            windowInfo?.layoutParams!!.x = leftMost
        }
        if (windowInfo?.layoutParams!!.x > rightMost) {
            windowInfo?.layoutParams!!.x = rightMost
        }
        if (windowInfo?.layoutParams!!.y < topMost) {
            windowInfo?.layoutParams!!.y = topMost
        }
        if (windowInfo?.layoutParams!!.y > bottomMost) {
            windowInfo?.layoutParams!!.y = bottomMost
        }
        //'更新浮窗位置'
        windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)
        lastTouchX = currentX
        lastTouchY = currentY
    }
}

浮窗自动贴边

新的需求来了,拖拽浮窗松手后,需要自动贴边。

把贴边理解成一个水平位移动画。在松手时求出动画起点和终点横坐标,利用动画值不断更新浮窗位置::

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    //'贴边动画'
    private var weltAnimator: ValueAnimator? = null
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        //'将触摸事件传递给GestureDetector解析'
        gestureDetector.onTouchEvent(event)
        //'处理ACTION_UP事件'
        val action = event.action
        when (action) {
            MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)
            else -> {
            }
        }
        return true
    }
    
    private fun onActionUp(event: MotionEvent, screenWidth: Int, width: Int) {
        if (!windowInfo?.hasView().value()) { return }
        //'记录抬手横坐标'
        val upX = event.rawX.toInt()
        //'贴边动画终点横坐标'
        val endX = if (upX > screenWidth / 2) {
            screenWidth - width
        } else {
            0
        }

        //'构建贴边动画'
        if (weltAnimator == null) {
            weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply {
                interpolator = LinearInterpolator()
                duration = 300
                addUpdateListener { animation ->
                    val x = animation.animatedValue as Int
                    if (windowInfo?.layoutParams != null) {
                        windowInfo?.layoutParams!!.x = x
                    }
                    val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
                    //'更新窗口位置'
                    if (windowInfo?.hasParent().value()) {
                        windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)
                    }
                }
            }
        }
        weltAnimator?.setIntValues(windowInfo?.layoutParams!!.x, endX)
        weltAnimator?.start()
    }
    
    //为空Boolean提供默认值
    fun Boolean?.value() = this ?: false
}
  • GestureDetector解析后ACTION_UP事件被吞掉了,所以只能在onTouch()中截获它。
  • 根据抬手横坐标和屏幕中点横坐标的大小关系,来决定浮窗贴向左边还是右边。

管理多个浮窗

若 app 的不同业务界面同时需要显示浮窗:进入 界面A 时显示 浮窗A,然后它被拖拽到右下角,退出 界面A 进入 界面B,显示浮窗B,当再次进入 界面A 时,期望还原上次离开时的浮窗A的位置。

当前FloatWindow中用windowInfo成员存储单个浮窗参数,为了同时管理多个浮窗,需要将所有浮窗参数保存在Map结构中用 tag 区分:

object FloatWindow : View.OnTouchListener {
    //'浮窗参数容器'
    private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap()
    //'当前浮窗参数'
    var windowInfo: WindowInfo? = null
    
    //'显示浮窗'
    fun show(
        context: Context,
        //'浮窗标签'
        tag: String,
        //'若不提供浮窗参数则从参数容器中获取该tag上次保存的参数'
        windowInfo: WindowInfo? = windowInfoMap[tag],
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value()
    ) {
        if (windowInfo == null) { return }
        if (windowInfo.view == null) { return }
        //'更新当前浮窗参数'
        this.windowInfo = windowInfo
        //'将浮窗参数存入容器'
        windowInfoMap[tag] = windowInfo
        windowInfo.view?.setOnTouchListener(this)
        this.context = context
        windowInfo.layoutParams = createLayoutParam(x, y)
        if (!windowInfo.hasParent().value()) {
            val windowManager =this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }
}

在显示浮窗时,增加tag标签参数用以唯一标识浮窗,并且为windowInfo提供默认参数,当恢复原有浮窗时,可以不提供windowInfo参数,FloatWindow就会去windowInfoMap中根据给定tag寻找对应windowInfo

监听浮窗界外点击事件

新的需求来了,点击浮窗时,贴边的浮窗像抽屉一样展示,点击浮窗以外区域时,抽屉收起。

刚开始接到这个新需求时,没什么思路。转念一想PopupWindow有一个setOutsideTouchable()

public class PopupWindow {
    /**
     * <p>Controls whether the pop-up will be informed of touch events outside
     * of its window. 
     *
     * @param touchable true if the popup should receive outside
     * touch events, false otherwise
     */
    public void setOutsideTouchable(boolean touchable) {
        mOutsideTouchable = touchable;
    }
}

该函数用于设置是否允许 window 边界外的触摸事件传递给 window。跟踪mOutsideTouchable变量应该就能找到更多线索:

public class PopupWindow {
    private int computeFlags(int curFlags) {
        curFlags &= ~(
                WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
        ...
        //'如果界外可触摸,则将FLAG_WATCH_OUTSIDE_TOUCH赋值给flag'
        if (mOutsideTouchable) {
            curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        }
        ...
    }
}

继续往上跟踪computeFlags()调用的地方:

public class PopupWindow {
    protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        p.gravity = computeGravity();
        //'计算窗口布局参数flag属性并赋值'
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;
        p.token = token;
        ...
    }
}

createPopupLayoutParams()会在窗口显示的时候被调用:

public class PopupWindow {
    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) { return; }
        TransitionManager.endTransitions(mDecorView);
        detachFromAnchor();
        mIsShowing = true;
        mIsDropdown = false;
        mGravity = gravity;

        //'构建窗口布局参数'
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);
        p.x = x;
        p.y = y;
        invokePopup(p);
    }
}

想在源码中继续搜索,但到FLAG_WATCH_OUTSIDE_TOUCH,线索就断了。现在只知道为了让界外点击事件传递给 window,必须为布局参数设置FLAG_WATCH_OUTSIDE_TOUCH。但事件响应逻辑应该写在哪里?

当调用PopupWindow.setOutsideTouchable(true),在窗口界外点击后,窗口会消失。这必然是调用了dismiss(),沿着dismiss()的调用链往上找一定能找到界外点击事件的响应逻辑:

public class PopupWindow {
    //'窗口根视图'
    private class PopupDecorView extends FrameLayout {
    
        //'窗口根视图触摸事件'
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();

            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            //'如果发生了界外触摸事件则解散窗口'
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
    }
}

所以只需要在窗口根视图的触摸事件回调中捕获ACTION_OUTSIDE即可:

object FloatWindow : View.OnTouchListener {
    //'界外触摸事件回调'
    private var onTouchOutside: (() -> Unit)? = null
    
    //'设置是否响应界外点击事件'
    fun setOutsideTouchable(enable: Boolean, onTouchOutside: (() -> Unit)? = null) {
        windowInfo?.layoutParams?.let { layoutParams ->
            layoutParams.flags = layoutParams.flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
            this.onTouchOutside = onTouchOutside
        }
    }
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        //'界外触摸事件处理'
        if (event.action == MotionEvent.ACTION_OUTSIDE) {
            onTouchOutside?.invoke()
            return true
        }

        //'点击和拖拽事件处理'
        gestureDetector.onTouchEvent(event).takeIf { !it }?.also {
            //there is no ACTION_UP event in GestureDetector
            val action = event.action
            when (action) {
                MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)
                else -> {
                }
            }
        }
        return true
    }
}

talk is cheap, show me the code

实例代码隐藏了不重要的细节,完整代码可点击上面链接。

目录
相关文章
|
2月前
|
IDE Java 开发工具
深入探索安卓应用开发:从环境搭建到第一个"Hello, World!"应用
本文将引导读者完成安卓应用开发的初步入门,包括安装必要的开发工具、配置开发环境、创建第一个简单的安卓项目,以及解释其背后的一些基本概念。通过一步步的指导和解释,本文旨在为安卓开发新手提供一个清晰、易懂的起点,帮助读者顺利地迈出安卓开发的第一步。
224 65
|
2月前
|
存储 Java Android开发
探索安卓应用开发:构建你的第一个"Hello World"应用
【9月更文挑战第24天】在本文中,我们将踏上一段激动人心的旅程,深入安卓应用开发的奥秘。通过一个简单而经典的“Hello World”项目,我们将解锁安卓应用开发的基础概念和步骤。无论你是编程新手还是希望扩展技能的老手,这篇文章都将为你提供一次实操体验。从搭建开发环境到运行你的应用,每一步都清晰易懂,确保你能顺利地迈出安卓开发的第一步。让我们开始吧,探索如何将一行简单的代码转变为一个功能齐全的安卓应用!
|
17天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
17天前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
21天前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。
|
23天前
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
37 2
|
27天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
48 5
|
27天前
|
移动开发 Dart 搜索推荐
打造个性化安卓应用:从零开始的Flutter之旅
【10月更文挑战第20天】本文将引导你开启Flutter开发之旅,通过简单易懂的语言和步骤,让你了解如何从零开始构建一个安卓应用。我们将一起探索Flutter的魅力,实现快速开发,并见证代码示例如何生动地转化为用户界面。无论你是编程新手还是希望扩展技能的开发者,这篇文章都将为你提供价值。
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
47 4
|
1月前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
51 2
下一篇
无影云桌面