Android 自定义PopupWindow,实现下拉框

news2024/9/30 23:32:09

1、效果图

2、前言

1、页面由 MagicIndicator + ViewPager2 + Fragment 实现;

2、下拉框是基于WindowManager实现;

3、我使用PopupWindow实现下拉框时,发现一个问题,PopupWindow 在窗口显示的情况下,无法直接从外部修改布局,必须先dismiss

PopupWindow源码:              

public void setContentView(View contentView) {
    if (isShowing()) {
        return;
    }

    ... ...
}


public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    if (isShowing() || !hasContentView()) {
        return;
    }
    ... ...
}

4、如果先dismiss再添加,属于重新创建布局,切换生硬,会出现闪烁,影响用户体验,就像这样;

那就没办法了,自己实现;

观摩PopupWindow源码发现它是基于windowManager实现的,照葫芦画瓢,自定义一个

3、自定义下拉框

AffiliatedBottomWindow

package com.example.myapplication.common;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;

import androidx.annotation.NonNull;

import com.example.myapplication.util.BarUtils;
import com.example.myapplication.util.CustomizeUtils;
import com.example.myapplication.util.ScreenUtils;


/**
 * 挂靠在某个view下面的悬浮窗
 * <p>
 * 和PopupWindow一样都是基于WindowManager实现的;
 * <p>
 *
 * 和PopupWindow区别:
 * PopupWindow 在窗口显示的情况下,无法直接从外部修改布局,必须先dismiss,
 * 再重新创建,切换会出现闪烁,用户体验差;
 * <p>
 * PopupWindow源码:
 * public void setContentView(View contentView) {
 *      if (isShowing()) {
 *          return;
 *      }
 *      ... ...
 * }
 *
 * <p>
 * 为了避免这种闪烁,基于WindowManager写了这个 AffiliatedBottomWindow 底部挂靠悬浮框;
 * <p>
 *
 *
 * 注意:单例,使用完成后一定要清空,不然无法创建实例 if (instance == null) {}
 *     public void clear() {
 *         instance = null;
 *     }
 */
public class AffiliatedBottomWindow {

    private final Context context;

    private static CustomizeUtils.AntiShake antiShake;

    private WindowManager windowManager;

    @SuppressLint("StaticFieldLeak")
    private static AffiliatedBottomWindow instance;

    // 根view
    @SuppressLint("StaticFieldLeak")
    private static ViewGroup rootView;

    // 根view中的子view
    private ViewGroup rootChildView;

    // true:使用 根view中的子view 作为内容布局的容器
    private boolean useRootChildView = false;

    // 显示/隐藏 过渡动画时长
    public static int animatorDuration = 200;

    private WindowManager.LayoutParams windowLayoutParams;

    private AffiliatedBottomWindow(Context context) {
        this.context = context;
        createWindowManager();
        antiShake = new CustomizeUtils.AntiShake(500);
    }

    public WindowManager getWindowManager() {
        return windowManager;
    }

    public void setWindowManager(WindowManager windowManager) {
        this.windowManager = windowManager;
    }

    public AffiliatedBottomWindow getInstance() {
        return instance;
    }

    public WindowManager.LayoutParams getWindowLayoutParams() {
        return windowLayoutParams;
    }

    public void setWindowLayoutParams(WindowManager.LayoutParams windowLayoutParams) {
        this.windowLayoutParams = windowLayoutParams;
    }

    public ViewGroup getRootView() {
        return rootView;
    }

    public void setRootView(ViewGroup rootView) {
        AffiliatedBottomWindow.rootView = rootView;

        // 初始化隐藏布局
        AffiliatedBottomWindow.rootView.setVisibility(View.GONE);
    }

    public boolean isUseRootChildView() {
        return useRootChildView;
    }

    public void setUseRootChildView(boolean useRootChildView) {
        this.useRootChildView = useRootChildView;
    }

    // 显示 和 隐藏 过渡透明度动画
    public static void alphaAnimation(boolean show) {
        if (antiShake.isFastClick()) {
            ValueAnimator animator;
            if (show) {
                if (rootView.getVisibility() == View.VISIBLE) {
                    return;
                }
                rootView.setAlpha(0);
                rootView.setVisibility(View.VISIBLE);

                //显示
                animator = ValueAnimator.ofFloat(0f, 1f);

                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                        float progress = (float) animation.getAnimatedValue();
                        rootView.setAlpha(progress);
                    }
                });
                animator.setDuration(animatorDuration);
                animator.start();
            } else {
                if (rootView.getVisibility() == View.GONE) {
                    return;
                }

                //隐藏
                animator = ValueAnimator.ofFloat(1f, 0f);

                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                        float progress = (float) animation.getAnimatedValue();
                        rootView.setAlpha(progress);
                    }
                });

                animator.addListener(new AnimatorListenerAdapter() {

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        rootView.setVisibility(View.GONE);
                    }

                });
                animator.setDuration(animatorDuration);
                animator.start();
            }
        }

    }

    /**
     * 内容布局插入到 根View中
     *
     * @param context
     * @param rootView       根布局
     * @param affiliatedView 挂靠的View,悬浮框会出现在这个View下面
     */
    public static AffiliatedBottomWindow createInstance(Context context,
                                                        ViewGroup rootView,
                                                        View affiliatedView) {
        if (instance == null) {
            instance = new AffiliatedBottomWindow(context);

            // 先设置根view
            instance.setRootView(rootView);

            // 再设置挂靠view,顺序不能乱
            instance.setAffiliatedView(affiliatedView);
        }
        return instance;
    }

    /**
     * 内容布局插入到 根View中的 子View中
     *
     * @param context
     * @param rootView          根布局
     * @param affiliatedView    挂靠的View,悬浮框会出现在这个View下面
     * @param rootChildView     根view中的子view
     * @param useChildContainer 是否将 根view中的子view 作为内容布局的容器
     */
    public static AffiliatedBottomWindow createInstance(Context context,
                                                        ViewGroup rootView,
                                                        View affiliatedView,
                                                        ViewGroup rootChildView,
                                                        boolean useChildContainer) {
        if (instance == null) {
            instance = new AffiliatedBottomWindow(context);
            instance.setUseRootChildView(useChildContainer);
            instance.setRootChildView(rootChildView);

            // 先设置根view
            instance.setRootView(rootView);

            // 再设置挂靠view,顺序不能乱
            instance.setAffiliatedView(affiliatedView);

        }
        return instance;
    }

    public void setAffiliatedView(View affiliatedView) {
        // 设置悬浮框宽/高
        windowLayoutParams.width = affiliatedView.getWidth();

        // 剩余空间高度
        windowLayoutParams.height = ScreenUtils.getScreenHeight(context) - affiliatedView.getBottom();

        // 设置悬浮框位置
        windowLayoutParams.gravity = Gravity.TOP;

        // 减去状态栏高度,沉浸式布局,
        // 如果不是沉浸式布局,扩展重写此方法
        windowLayoutParams.y = affiliatedView.getBottom() - BarUtils.getStatusBarHeight(context);

        // 显示,当前根视图隐藏了,所以不显示
        windowManager.addView(rootView, windowLayoutParams);
    }

    public void setRootChildView(ViewGroup rootChildView) {
        this.rootChildView = rootChildView;
    }

    private void createWindowManager() {
        if (windowManager != null) {
            return;
        }

        // 创建 windowManager对象
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        // 创建布局参数
        windowLayoutParams = new WindowManager.LayoutParams();

        // 设置窗口类型
        windowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;

        // 设置悬浮框不可触摸,默认接收事件,会导致底层view,接收不到事件
        windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        // 背景颜色,设置为透明
        windowLayoutParams.format = PixelFormat.TRANSPARENT;
    }

    // 插入布局
    public void insertViewLayout(View view) {
        if (useRootChildView) {
            if (rootChildView.getChildCount() == 0) {
                rootChildView.addView(view);
            } else {
                rootChildView.removeAllViews();
                rootChildView.addView(view);
            }
        } else {
            if (rootView.getChildCount() == 0) {
                rootView.addView(view);
            } else {
                rootView.removeAllViews();
                rootView.addView(view);
            }
        }
        alphaAnimation(true);
    }

    // 隐藏窗口
    public static void dismiss() {
        alphaAnimation(false);
    }

    // 单例,使用完成后一定要清空
    // 不然 无法创建实例 if (instance == null) {}
    public void clear() {
        instance = null;
    }
}

4、源码  

demo东西比较多,是从自己项目里摘录出来的,扩展了MagicIndicator

核心类:AffiliatedBottomWindow

https://github.com/LanSeLianMa/CustomizeBottomWindow

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1072715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

​力扣:LCR 122. 路径加密​ 题目:剑指Offer 05.替换空格(c++)

本文章代码以c为例&#xff01; 力扣&#xff1a;LCR 122. 路径加密 题目&#xff1a; 代码&#xff1a; class Solution { public:string pathEncryption(string path) {for(int i0;i<path.size();i){if(path[i].){path[i] ;}}return path;} }; 难度升级&#xff08;原…

C++对象模型(4)-- 数据语义学:数据成员的内存布局

1、数据成员的声明顺序和内存布局 变量的布局顺序与它的声明顺序是一致的。 我们先来看一段代码&#xff1a; class Base { public:// 变量地址void print() {cout << " this:" << this << endl;cout << " &i1:" << …

Nacos 监控手册

Nacos 0.8.0版本完善了监控系统&#xff0c;支持通过暴露metrics数据接入第三方监控系统监控Nacos运行状态&#xff0c;目前支持prometheus、elastic search和influxdb&#xff0c;下面结合prometheus和grafana如何监控Nacos。与elastic search和influxdb结合可自己查找相关资料…

小视频APP源码选择指南:挑选最适合你的开发框架

在如今蓬勃发展的小视频APP行业中&#xff0c;源码的选择是打造一款成功应用的关键步骤。然而&#xff0c;面对众多开发框架的选择&#xff0c;如何挑选最适合你的小视频APP源码呢&#xff1f;作为这一领域的专家&#xff0c;我将为你提供一份详尽的指南&#xff0c;助你在源码…

一个rar压缩包如何分成三个?

一个rar压缩包体积太大了&#xff0c;想要将压缩包分为三个&#xff0c;该如何做到&#xff1f;其实很简单&#xff0c;方法就在我们经常使用的WinRAR当中。 我们先将压缩包内的文件解压出来&#xff0c;然后查看一下&#xff0c;然后打开WinRAR软件&#xff0c;找到文件&…

SpringBoot的创建与配置文件【.properties与.yml】

SpringBoot的优点&#xff1a; 1.快速添加外部jar包 2.内置运行容器&#xff0c;无需Tomcat 3.可以快速部署&#xff0c;不依赖外部容器 4.抛弃繁琐的XML 5.拥有更多监控指标 SpringBoot 项目创建 SpringBoot的创建步骤 1. 通过 idea 创建 a.专业版直接创建&#xff0c;无需插…

SSL证书一次性购买多年期,有什么好处?

根据国际标准要求&#xff0c;2020 年 9 月 1 日起&#xff0c;全球信任的SSL证书最长有效期不能超过一年&#xff08;398天&#xff09;。一方面SSL证书有效期缩短可以大大提升SSL证书的安全性&#xff0c;但另一方面也意味着&#xff0c;用户每年都需要重复采购、付款、验证等…

【Linux基础】Linux的基本指令使用(超详细解析,小白必看系列)

&#x1f449;系列专栏&#xff1a;【Linux基础】 &#x1f648;个人主页&#xff1a;sunnyll 目录 &#x1f4a6; ls 指令 &#x1f4a6; pwd指令 &#x1f4a6;cd指令 &#x1f4a6;touch指令 &#x1f4a6;mkdir指令&#xff08;重要&#xff09; &#x1f4a6;rmdir指令…

信息增益,经验熵和经验条件熵——决策树

目录 1.经验熵 2.经验条件熵 3.信息增益 4.增益比率 5.例子1 6.例子2 在决策树模型中&#xff0c;我们会考虑应该选择哪一个特征作为根节点最好&#xff0c;这里就用到了信息增益 通俗上讲&#xff0c;信息增益就是在做出判断时&#xff0c;该信息对你影响程度的大小。比…

服务器上部署python脚本

1.查看服务器上的python是否自带&#xff0c;一般都自带 2.将本地脚本上传到服务器 3.直接运行一下脚本看报什么错误 代码错误&#xff0c; 将f删除后报别的错误 上面是未安装依赖的错误。我们安装一下依赖 下面是编码的解决 #!/usr/bin/python # -*- coding: utf-8 -*- 先把…

Java多线程篇(9)——AQS之读写锁(ReentrantReadWriteLock)

文章目录 1、读写锁的实现1.1、state的分割与HoldCounter1.2、写锁的获取/释放1.3、读锁的获取/释放 2、写锁降级成读锁的使用场景 1、读写锁的实现 1.1、state的分割与HoldCounter ReentrantReadWriteLock 内部维护了读锁和写锁两个锁&#xff0c;这两个锁内部都依赖于同一个…

全网最新最全的软件测试面试题

一、前言 与开发工程师相比&#xff0c;软件测试工程师前期可能不会太深&#xff0c;但涉及面还是很广的。 在一年左右的实习生或岗位的早期面试中&#xff0c;主要是问一些基本的问题。 涉及到的知识主要包括MySQL数据库的使用、Linux操作系统的使用、软件测试框架问题、测试…

线性代数小例子

这样做有什么问题呢&#xff1a; A 2 A > A ( A − E ) 0 > A E A 0 A^2 A > A(A - E) 0> A E \quad A 0 A2A>A(A−E)0>AEA0 上述做法是错误的&#xff0c;这是因为两个矩阵的乘积结果为0&#xff0c;并不能说明这两个矩阵就是0&#xff0c;即上述…

chromadb 0.4.0 后的改动

本文基于一篇上次写的博客&#xff1a;[开源项目推荐]privateGPT使用体验和修改 文章目录 一.上次改好的ingest.py用不了了&#xff0c;折腾了一会儿二.发现privateGPT官方更新了总结下变化效果 三.others 一.上次改好的ingest.py用不了了&#xff0c;折腾了一会儿 pydantic和c…

Web自动化测试的详细流程和步骤

一、什么是web自动化测试 自动化&#xff08;Automation&#xff09;是指机器设备、系统或过程&#xff08;生产、管理过程&#xff09;在没有人或较少人的直接参与下&#xff0c;按照人的要求&#xff0c;经过自动检测、信息处理、分析判断、操纵控制&#xff0c;实现预期的目…

国内机械臂产业的现状

机械臂作为一种重要的工业自动化设备&#xff0c;具有高效、精准、灵活等特点&#xff0c;被广泛应用于制造业、物流、医疗、农业等领域。随着中国制造业的快速发展和自动化水平的提高&#xff0c;国内机械臂产业也迎来了快速发展的机遇。本文将对国内机械臂产业的现状进行综述…

voc数据集格式与yolo数据集格式的区别及相互转化

Pascal VOC数据集是目标检测领域最常用的标准数据集之一&#xff0c;几乎所有检测方向的论文都会给出其在VOC数据集上训练并评测的效果。VOC数据集包含的信息非常全&#xff0c;它不仅被拿来做目标检测&#xff0c;也可以拿来做分割等任务&#xff0c;因此除了目标检测所需的文…

kafka与zookeeper的集群

基础配置 systemctl stop firewalld && systemctl disable firewalld setenforce 0 sed -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/configvi /etc/hosts ip1 node1 ip2 node2 ip3 node3zookeeper介绍 zookeeper是一个分布式的协调服务&#xff0c;主要用…

【广州华锐互动】车辆零部件检修AR远程指导系统有效提高维修效率和准确性

在快速发展的科技时代&#xff0c;我们的生活和工作方式正在被重新定义。这种变化在许多领域都有所体现&#xff0c;尤其是在汽车维修行业。近年来&#xff0c;AR&#xff08;增强现实&#xff09;技术的进步为这个行业带来了前所未有的可能性。通过将AR技术与远程协助系统相结…

无为WiFi的一批服务器

我们在多个地区拥有高速服务器&#xff0c;保证网速给力&#xff0c;刷片无压力 嘿嘿 <?phpinclude("./includes/common.php"); $actisset($_GET[act])?daddslashes($_GET[act]):null; $urldaddslashes($_GET[url]); $authcodedaddslashes($_GET[authcode]);he…