聊聊如何实现Android 放大镜效果

一、前言

很久没有更新Android 原生技术内容了,前些年一直在做跨端方向开发,最近换工作用重新回到原生技术,又回到了熟悉但有些生疏的环境,真是感慨万分。
近期也是因为准备做地图交互相关的需求,功能非常复杂,尤其是交互部分,不过再复杂的交互,只要一点点将它拆解,分而治之,问题还是可以解决,就比如接下来要做的放大镜功能。

二、功能设计

该功能的场景是在操作地图时,对于边缘的精细化操作(像素级别的)需要在放大镜里显示正在操作的地图区域。比如
在这里插入图片描述

如上图,我在操作地图里的内容时,可以在左上角看到我手指操作的内容。
这里需要支持如下几点:

  • 支持设置放大镜的放大倍数
  • 支持实时更新放大镜里的内容,手指操作地图时,放大镜要里的内容要一直显示(刷新),手指松开时,放大镜里的内容要清空。
  • 放大镜是个圆形。

三、功能实现

分下下来,放大镜的功能实现,拆解下来可以分成两部分来实现。

  • 绘制圆形容器。
  • 绘制手指操作的区域内容。
    接下来我们逐个实现

3.1 绘制圆形容器

关于绘制圆形容器,这里会涉及到Path 路径知识,因为正常的图形都是方形,因此需要图形裁切才行,这里会涉及到canvas.clipPath()API。
具体代码如下:

	private Paint paint;
    // 用于裁剪成圆形
    private Path clipPath;
	private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        clipPath = new Path();
    }
    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        // 获取 MagnifierView 的宽高
        int viewWidth = getWidth();
        int viewHeight = getHeight();
        // 计算圆形半径
        float radius = Math.min(viewWidth, viewHeight) / 2f;
        clipPath.reset();
        // 绘制圆形路径
        clipPath.addCircle(viewHeight / 2f, (float) viewHeight /2,radius,Path.Direction.CW);
        // 裁切圆形画布
        canvas.clipPath(clipPath);
    }

3.2 绘制手指操作区域

放大镜本质就是放大图片,而图片在Android 里面就是Bitmap。这里有个问题。

3.2.1 如何获取当前手指操作View 的Bitmap呢?

答案是用getDrawingCache()。
具体实现如下:

setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
setDrawingCacheEnabled(false);

当然,这里里的Bitmap是一整个View的,我们仅想放大手指操作(比如点击、滑动)区域的Bitmap,这就需要手指所在的坐标区域。
我们可以自定义一个View,然后重写它的onTouchEvent方法,通过event.getX(), event.getY()获取手指实时操作的坐标,然后传给放大镜View(比如我们这次自定义的放大镜MagnifierView)里面,连同前面的Bitmap一起传过去。这里可以搞一个回调接口。

	public interface MagnifierListener {
        // 传递放大镜内容
        void onMagnify(Bitmap bitmap, float x, float y);

        // 隐藏放大镜
        void onHideMagnifier();
    }

3.2.2 绘制操作区域

得到了要绘制的内容Bitmap,接下来就是绘制出来。
绘制Bitmap很简单,就是调用下面的API:

canvas.drawBitmap(bitmap, srcRect, dstRect, paint);

复杂的是计算放大内容的源区域,也就是srcRect。
这里先解释下dstRect,它是定义目标矩形(也就是放大镜自身大小)实现如下:

RectF dstRect = new RectF(0, 0, viewWidth, viewHeight);

接下来我们开始设置srcRect。写到这里我们好像漏了一个关键参数“放大倍数”。

// 放大倍数
private static final float SCALE_FACTOR = 2.0f;

因为我们要显示的区域和放大倍数有直接关联。srcRect 是一个Rect 对象,它里面有四个参数(左上右下),相当于当前显示区域范围,计算公式如下:

// 计算放大内容的源区域
float srcLeft = focusX - (viewWidth / SCALE_FACTOR) / 2;
float srcTop = focusY - (viewHeight / SCALE_FACTOR) / 2;
float srcRight = focusX + (viewWidth / SCALE_FACTOR) / 2;
float srcBottom = focusY + (viewHeight / SCALE_FACTOR) / 2;

先来解释下上面的公式意义,focusX对应的就是手指操作的x坐标,即event.getX(),focusY 同理则是event.getY()。
SCALE_FACTOR 是放大倍数。
因为我们想要显示的是“手指为中心,显示区域大小是当前放大镜的面积”,因此需要(viewWidth / SCALE_FACTOR) / 2,这里用focusX是确定左边界的位置。后面参数计算和前面差不多,这里我不重复解释。不过写到这里还不算完成。为了防止越界,这里还需要做一次矫正防护:

// 防止越界
srcLeft = Math.max(0, srcLeft);
srcTop = Math.max(0, srcTop);
srcRight = Math.min(bitmap.getWidth(), srcRight);
srcBottom = Math.min(bitmap.getHeight(), srcBottom);

最后的放大区域代码如下:

		// 计算放大内容的源区域
        float srcLeft = focusX - (viewWidth / SCALE_FACTOR) / 2;
        float srcTop = focusY - (viewHeight / SCALE_FACTOR) / 2;
        float srcRight = focusX + (viewWidth / SCALE_FACTOR) / 2;
        float srcBottom = focusY + (viewHeight / SCALE_FACTOR) / 2;

        // 防止越界
        srcLeft = Math.max(0, srcLeft);
        srcTop = Math.max(0, srcTop);
        srcRight = Math.min(bitmap.getWidth(), srcRight);
        srcBottom = Math.min(bitmap.getHeight(), srcBottom);

        // 定义源矩形(放大的内容区域)
        Rect srcRect = new Rect(
                (int) srcLeft,
                (int) srcTop,
                (int) srcRight,
                (int) srcBottom
        );

3.3 小结

这里附上这个功能的完整代码:

/**
 * author      : coffer
 * date        : 2025/1/11
 * description : 放大镜视图
 */
public class MagnifierView extends View {
    private float focusX = 0f; // 放大内容的中心点X
    private float focusY = 0f; // 放大内容的中心点Y
    // 要放大的内容
    private Bitmap bitmap;
    private Paint paint;
    // 用于裁剪成圆形
    private Path clipPath;
    // 放大倍数
    private static final float SCALE_FACTOR = 2.0f;
    // 是否可以显示
    private boolean isVisible = false;
    public MagnifierView(Context context) {
        super(context);
        init();
    }

    public MagnifierView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MagnifierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        clipPath = new Path();
    }

    /**
     * 设置放大镜的内容和位置
     */
    public void setFocus(Bitmap bitmap, float x, float y) {
        this.bitmap = bitmap;
        this.focusX = x;
        this.focusY = y;
        this.isVisible = true;
        invalidate(); // 触发重绘
    }

    /**
     * 隐藏放大镜
     */
    public void hide() {
        this.isVisible = false;
        invalidate();
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        if (!isVisible || bitmap == null) {
            return;
        }
        // 获取 MagnifierView 的宽高
        int viewWidth = getWidth();
        int viewHeight = getHeight();
        // 计算圆形半径
        float radius = Math.min(viewWidth, viewHeight) / 2f;

        clipPath.reset();
        clipPath.addCircle(viewHeight / 2f, (float) viewHeight /2,radius,Path.Direction.CCW);
        canvas.clipPath(clipPath);
        // 计算放大内容的源区域
        float srcLeft = focusX - (viewWidth / SCALE_FACTOR) / 2;
        float srcTop = focusY - (viewHeight / SCALE_FACTOR) / 2;
        float srcRight = focusX + (viewWidth / SCALE_FACTOR) / 2;
        float srcBottom = focusY + (viewHeight / SCALE_FACTOR) / 2;

        // 防止越界
        srcLeft = Math.max(0, srcLeft);
        srcTop = Math.max(0, srcTop);
        srcRight = Math.min(bitmap.getWidth(), srcRight);
        srcBottom = Math.min(bitmap.getHeight(), srcBottom);

        // 定义源矩形(放大的内容区域)
        Rect srcRect = new Rect(
                (int) srcLeft,
                (int) srcTop,
                (int) srcRight,
                (int) srcBottom
        );

        // 定义目标矩形(放大镜自身大小)
        RectF dstRect = new RectF(0, 0, viewWidth, viewHeight);

        // 绘制放大内容
        canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <stroke android:color="#abf4db"
        android:width="1dp"/>

</shape>

上面的这个drawable 可有可无,这里这是方便测试用。

四、总结

一开始接到这个需求的时候我是真的有些懵,因为以前从来没有写过。不过后来在仔细拆分问题后,觉的还是可以实现的。这里我要着重感谢ChatGPT,它给了我很大的帮助,就比如这个功能的实现,它就给了我思路,就像老师一样,能从它身上学到很多技能。
但是,请注意!不能完全依赖它,即使AI 帮你做了,你也必须要深入搞懂背后的原理,要把知识吸收到自己大脑思维中,否则未来你将会被AI取代!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++,设计模式,【目录篇】

文章目录 1. 简介2. 设计模式的分类2.1 创建型模式&#xff08;Creational Patterns&#xff09;&#xff1a;2.2 结构型模式&#xff08;Structural Patterns&#xff09;&#xff1a;2.3 行为型模式&#xff08;Behavioral Patterns&#xff09;&#xff1a; 3. 使用设计模式…

RabbitMQ集群安装rabbitmq_delayed_message_exchange

1、单节点安装rabbitmq安装延迟队列 安装延迟队列rabbitmq_delayed_message_exchange可以参考这个文章&#xff1a; rabbitmq安装延迟队列-CSDN博客 2、集群安装rabbitmq_delayed_message_exchange 在第二个节点 join_cluster 之后&#xff0c;start_app 就会报错了 (CaseC…

【C++】如何从源代码编译红色警戒2地图编辑器

【C】如何从源代码编译红色警戒2地图编辑器 操作视频视频中的代码不需要下载三方库&#xff0c;已经包含三方库。 一、运行效果&#xff1a;二、源代码来源及编程语言&#xff1a;三、环境搭建&#xff1a;安装红警2安装VS2022下载代码&#xff0c;源代码其实不太多&#xff0c…

下定决心不去读研了。。。

大家好&#xff0c;我是苍何。 之前发表过一篇文章&#xff0c;表达了自己读研的困惑和纠结&#xff0c;得到了大家很多的建议&#xff0c;也引起了很多人的共鸣&#xff0c;在留言区分享了自己的故事&#xff0c;看着这些故事&#xff0c;我觉得都够苍何写一部小说了。 可惜苍…

重温STM32之环境安装

缩写 CMSIS&#xff1a;common microcontroller software interface standard 1&#xff0c;keil mdk安装 链接 Keil Product Downloads 安装好后&#xff0c;开始安装平台软件支持包&#xff08;keil 5后不在默认支持所有的平台软件开发包&#xff0c;需要自行下载&#…

[苍穹外卖] 1-项目介绍及环境搭建

项目介绍 定位&#xff1a;专门为餐饮企业&#xff08;餐厅、饭店&#xff09;定制的一款软件产品 功能架构&#xff1a; 管理端 - 外卖商家使用 用户端 - 点餐用户使用 技术栈&#xff1a; 开发环境的搭建 整体结构&#xff1a; 前端环境 前端工程基于 nginx 运行 - Ngi…

能源物联网数据采集设备 串口服务器功能参数介绍

摘要 ​随着物联网技术的快速发展&#xff0c;各种传统设备的联网需求愈发迫切。串口服务器作为一种桥接传统串口设备与现代网络的关键设备&#xff0c;在工业控制、智能电网、交通运输等域发挥了重要作用。本文以APort100串口服务器为例&#xff0c;探讨串口服务器在现代物联…

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识 2025/1/18 15:12 缘起&#xff1a;做飞凌OK3588-C开发板/核心板【Linux R4】的时候&#xff0c;测试/生产要求没有开机LOGO【飞凌/Rockchip】 要求&#xff1a;黑屏或者中性界面。…

【STM32-学习笔记-4-】PWM、输入捕获(PWMI)

文章目录 1、PWMPWM配置 2、输入捕获配置3、编码器 1、PWM PWM配置 配置时基单元配置输出比较单元配置输出PWM波的端口 #include "stm32f10x.h" // Device headervoid PWM_Init(void) { //**配置输出PWM波的端口**********************************…

RocketMQ源码之消息刷盘分析

前言 刷盘是将内存中的消息写入磁盘,分为同步刷盘和异步刷盘。同步刷盘指一条消息写入磁盘才返回成功,异步刷盘指写入内存就返回成功,稍后异步线程刷盘。 在创建CommitLog对象的时候,会初始化刷盘服务: //代码位置:org.apache.rocketmq.store.CommitLog public CommitL…

navicat 折线图或面积图

折线图或面积图将信息显示为以直线段连接的一系列数据点。 折线图 面积图 堆积面积图 图表属性 选择图表类型后&#xff0c;可以更改其属性来自定义图表&#xff1a; 选项 描述 常规 背景颜色 设置图表区域的背景颜色。 不透明度 设置背景颜色的不透明度。 显示边框…

【数模学习笔记】插值算法和拟合算法

声明&#xff1a;以下笔记中的图片以及内容 均整理自“数学建模学习交流”清风老师的课程资料&#xff0c;仅用作学习交流使用 文章目录 插值算法定义三个类型插值举例插值多项式分段插值三角插值 一般插值多项式原理拉格朗日插值法龙格现象分段线性插值 牛顿插值法 Hermite埃尔…

机器学习皮马印第安人糖尿病数据集预测报告

目录 1.项目选题与设计方案 1.1项目选题 1.2设计方案 2.功能实现 2.1 主要功能介绍 2.2 开发环境及平台介绍 2.3 实现过程 2.3.1数据分析 2.3.2算法设计 2.3.3 python代码 3.个人心得体会 1.项目选题与设计方案 1.1项目选题 我国的糖尿病患者初诊时约&#xff18;&a…

STM32 FreeRTOS任务通知

目录 任务通知的简介 任务通知相关API函数介绍 任务通知的简介 任务通知是 FreeRTOS 中一种用于任务间通信的机制&#xff0c;它允许一个任务向其他任务发送简单的通知或信号&#xff0c;以实现任务间的同步和协作。任务通知通常用于替代二值信号量或事件标志组&#xff0c;提…

提示词的艺术----AI Prompt撰写指南(个人用)

提示词的艺术 写在前面 制定提示词就像是和朋友聊天一样&#xff0c;要求我们能够清楚地表达问题。通过这个过程&#xff0c;一方面要不断练习提高自己地表达能力&#xff0c;另一方面还要锻炼自己使用更准确精炼的语言提出问题的能力。 什么样的提示词有用&#xff1f; 有…

微服务网关,如何选择?

什么是API网关 API网关&#xff08;API Gateway&#xff09;是微服务架构中的一个关键组件&#xff0c;它充当了客户端与后端服务之间的中间层。其主要功能包括请求路由、协议转换、负载均衡、安全认证、限流熔断等。通过API网关&#xff0c;客户端无需直接与多个微服务交互&a…

PortSwigger靶场练习---第二关-查找和利用未使用的 API 端点

第二关&#xff1a;Finding and exploiting an unused API endpoint 实验&#xff1a;查找和利用未使用的 API 端点 PortSwigger靶场地址&#xff1a; Dashboard | Web Security Academy - PortSwigger 题目&#xff1a; 官方提示&#xff1a; 在 Burp 的浏览器中&#xff0c…

深度学习中的张量 - 使用PyTorch进行广播和元素级操作

深度学习中的张量 - 使用PyTorch进行广播和元素级操作 元素级是什么意思&#xff1f; 元素级操作在神经网络编程中与张量的使用非常常见。让我们从一个元素级操作的定义开始这次讨论。 一个_元素级_操作是在两个张量之间进行的操作&#xff0c;它作用于各自张量中的相应元素…

使用C语言实现栈的插入、删除和排序操作

栈是一种后进先出(LIFO, Last In First Out)的数据结构,这意味着最后插入的元素最先被删除。在C语言中,我们可以通过数组或链表来实现栈。本文将使用数组来实现一个简单的栈,并提供插入(push)、删除(pop)以及排序(这里采用一种简单的排序方法,例如冒泡排序)的操作示…

【逆境中绽放:万字回顾2024我在挑战中突破自我】

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 一、引言二、个人成长与盘点情感与心理成长学习与技能提升其它荣誉 三、年度创作历程回顾创作内容概…