当前位置: 首页 > news >正文

Android:截屏/视频截图

需求描述

实现截取Android应用当前界面的功能,需包含界面中视频(此博客的参考代码以存储在设备本地的视频为例,未检验在线视频的情况)当前的播放帧截图。

调研准备

首先应用需要获取设备存储的读写权限,需要在AndroidManifest.xml中加上请求权限的配置代码:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

此外,Android原生的视频播放组件VideoView不支持修改视频的分辨率(视频分辨率与容器宽高不一致时,需要让视频拉伸填充容器),因此需要自己封装一个继承了VideoView的组件;在项目中新建一个MyVideoView.java,内容如下:

package XXX;import android.content.Context;
import android.media.MediaPlayer;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.VideoView;
import com.lzy.okgo.utils.HttpUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;public class MyVideoView extends VideoView {private static final String TAG = "####MyVideoView ";// 记录当前播放视频的路径(用于截取播放帧)private String currentVideoUrl;public MyVideoView(Context paramContext) {super(paramContext);}public MyVideoView(Context paramContext, AttributeSet paramAttributeSet) {super(paramContext, paramAttributeSet);}public MyVideoView(Context paramContext, AttributeSet paramAttributeSet, int paramInt) {super(paramContext, paramAttributeSet, paramInt);}// 判断是否为视频文件public static boolean isVideo(String filePath) {filePath = filePath.toLowerCase();String[] vFiles = {".mov", ".mkv", ".mp4", ".avi"};for (byte vIdx = 0; vIdx < vFiles.length; vIdx++) {if (filePath.endsWith(vFiles[vIdx])) return true; } return false;}// 循环播放视频public void LoopPlayBack(final String videoPath) {File file = new File(videoPath);if (!file.exists() || !isVideo(videoPath)) return;// 开始播放视频this.currentVideoUrl = videoPath;setVideoPath(videoPath);start();// 视频播放完成,重新开始播放setOnCompletionListener(new MediaPlayer.OnCompletionListener() {  @Overridepublic void onCompletion(MediaPlayer mp) {MyVideoView.this.start();}});// 视频播放报错监听setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp) {Log.d(TAG, "播放错误..");return false;}});}public String getCurrentVideoUrl() { // 获取视频文件路径(用于截取播放帧)return this.currentVideoUrl;}protected void onMeasure(int paramInt1, int paramInt2) { // 调整视频分辨率,使视频拉伸填充容器setMeasuredDimension(getDefaultSize(getWidth(), paramInt1), getDefaultSize(getHeight(), paramInt2));}
}

在相应的布局.xml中使用MyVideoView视频组件:
P.S.如果要实现圆角视频效果,可以在MyVideoView外再套一层CardView,可参考:CardView-卡片布局

<LinearLayoutandroid:orientation="horizontal"android:layout_width="800.0px"android:layout_height="600.0px"
><XXX.MyVideoViewandroid:id="@id/video"android:visibility="visible"android:layout_width="fill_parent"android:layout_height="fill_parent"android:layout-align-parent-top="true"/>
</LinearLayout>

参考代码

如果界面中有视频播放,使用getDrawingCache截取整个应用界面时,视频区域会显示为黑屏;因此要另外获取视频当前的播放帧,再通过Canvas绘制Bitmap将视频截图“粘贴”到界面截图相应区域,从而实现截取整个界面(包括视频)的效果:

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.MediaMetadataRetriever;// 调用此函数进行完整截屏(参数:本地视频路径)
private void taskScreenshot(String videoPath) {try {Bitmap screenPic = takeScreenBitmap(); // 屏幕截图Bitmap videoPic = getCurrentVideoBitmap(this.myVideoView); // 视频截图if (screenPic != null) {Bitmap wholePic = screenPic; // 完整截图(默认取屏幕截图)if (videoPic != null) // 如果获取到了视频截图,完整截图由屏幕截图“粘贴”视频截图得到wholePic = mergeBitmap(screenPic, scaleBitmap(videoPic, 2), 0, 2); // 获取截图保存路径String picPath = Environment.getExternalStorageDirectory().getPath() + "/screenshot/testPic.png";File picFile = new File(picPath);if (picFile.exists())picFile.delete(); FileOutputStream fileOutputStream = new FileOutputStream();// 保存截图wholePic.compress(Bitmap.CompressFormat.PNG, 80, fileOutputStream);fileOutputStream.flush();fileOutputStream.close();} } catch (Exception e) {} 
}// 截屏(不包含视频)
private Bitmap takeScreenBitmap() {int width = getWindow().getDecorView().getRootView().getWidth();int height = getWindow().getDecorView().getRootView().getHeight();View view = getWindow().getDecorView().getRootView();view.setDrawingCacheEnabled(false);view.buildDrawingCache();Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache(), 0, 0, width, height);view.destroyDrawingCache();return bitmap;
}// 截取视频关键帧
public static Bitmap getCurrentVideoBitmap(MyVideoView paramMyVideoView) {Bitmap bitmap = null;MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();String videoPath = paramMyVideoView.getCurrentVideoUrl();try {if (Build.VERSION.SDK_INT >= 24) {Uri uri = Uri.parse(videoPath);mediaMetadataRetriever.setDataSource(this, uri);} else {FileInputStream fileInputStream = new FileInputStream();File file = new File();this(videoPath);this(file.getAbsolutePath());mediaMetadataRetriever.setDataSource(fileInputStream.getFD());} bitmap = mediaMetadataRetriever.getFrameAtTime((paramMyVideoView.getCurrentPosition() * 1000), MediaMetadataRetriever.OPTION_CLOSEST);}catch (Exception e) { }finally {try {mediaMetadataRetriever.release();} catch (RuntimeException runtimeException1) {stringBuilder = new StringBuilder();stringBuilder.append("getCurrentVideoBitmap-RuntimeException2:");stringBuilder.append(runtimeException1.getMessage());StoreData.appendLogFile("####FullscreenActivity ", stringBuilder.toString());}return (bitmap == null) ? null : Bitmap.createBitmap(bitmap);}
}// bitmap变换:截取的视频截图尺寸和页面容器可能不一致,需拉伸为容器尺寸
private Bitmap scaleBitmap(Bitmap paramBitmap, float cWidth, float cHeight) {if (paramBitmap == null)return null; int vWidth = paramBitmap.getWidth();int vHeight = paramBitmap.getHeight();Matrix matrix = new Matrix();matrix.postScale(cWidth / vWidth, cHeight / vHeight);Bitmap bitmap = Bitmap.createBitmap(paramBitmap, 0, 0, vWidth, vHeight, matrix, false);if (!paramBitmap.isRecycled()) paramBitmap.recycle(); return bitmap;
}// bitmap变换:将视频截图“粘贴”到屏幕截图的对应区域
private Bitmap mergeBitmap(Bitmap paramBitmap1, Bitmap paramBitmap2, int[] size1, int[] size2) {int width1 = size1[0], height1 = size1[1];int width2 = size2[0], height2 = size2[1];// 创建与屏幕截图大小一样的画布,然后分别将屏幕截图、视频截图绘制到画布对应位置Bitmap bitmap = Bitmap.createBitmap(width1, height1, Bitmap.Config.RGB_565);Canvas canvas = new Canvas(bitmap);canvas.drawBitmap(paramBitmap1, new Rect(0, 0, width1, height1), new Rect(0, 0, width1, height1), null);canvas.drawBitmap(paramBitmap2, new Rect(0, 0, width2, height2), new Rect(0, 0, width2, height2), null);return bitmap;
}

参考文档
[1] Android播放网络视频截图
[2] setDataSource RuntimeException 0xFFFFFFEA
[3] Android Bitmap相关操作
[4] Android使用Canvas绘制Bitmap相关
[5] CardView-卡片布局

相关文章:

Android:截屏/视频截图

需求描述 实现截取Android应用当前界面的功能&#xff0c;需包含界面中视频&#xff08;此博客的参考代码以存储在设备本地的视频为例&#xff0c;未检验在线视频的情况&#xff09;当前的播放帧截图。 调研准备 首先应用需要获取设备存储的读写权限&#xff0c;需要在Andro…...

leecode-C语言实现-28. 找出字符串中第一个匹配项的下标

一、题目给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 haystack 的一部分&#xff0c;则返回 -1 。示例 1&#xff1a;输入&#xff1a;haystack …...

使用 Postman 实现 API 自动化测试

目录&#xff1a;导读 背景介绍 名词解析 使用说明 执行 API 测试 集成 CI 实现 API 自动化测试 写在最后 背景介绍 相信大部分开发人员和测试人员对 postman 都十分熟悉&#xff0c;对于开发人员和测试人员而言&#xff0c;使用 postman 来编写和保存测试用例会是一种比…...

k8s环境jenkins发布vue项目指定nodejs版本

k8s环境jenkins发布vue项目指定nodejs版本1、背景2、分析3、解决方法3.1、 找到配置镜像位置3.2、 制作新镜像3.3、 推送镜像到私有仓库3.4、 修改配置文件1、背景 发布一个前端项目&#xff0c;它需要nodejs 16.9.0版本支持&#xff0c;而kubesphere 3.2.0集成的jenkins 的镜…...

我应该把毕业设计做到什么程度才能过关?

本篇博客包含了狗哥多年职业生涯对于软件项目的一丢丢理解&#xff0c;也讲述了对于大学生毕业设计的一些理解。如果你还是懵懵懂懂就要离开学校了&#xff0c;被老师告知不得不做出一套毕业设计的时候&#xff0c;希望你可以看到这篇博客&#xff0c;让你有点头绪&#xff0c;…...

力扣-合作过至少三次的演员和导演

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;1050. 合作过至少三次的演员和导演二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运…...

【 PMU】信号生成、采样、分割、估计器应用和误差计算(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…...

电子技术——AB类输出阶的偏置

电子技术——AB类输出阶的偏置 下面我们介绍两种AB类输出阶的偏置的方法。 使用二极管偏置 下图展示了电流源 III 加两个二极管的偏置方法&#xff1a; 因为输出阶需要大功率输出&#xff0c;因此输出推挽三极管可能是几何体积比较大的晶体管。对于二极管来说&#xff0c;并不…...

元宇宙营业厅,数字技术融合,赋能实体经济

在我国数字经济与虚拟服务市场规模扩大下&#xff0c;元宇宙营业厅强势来袭&#xff0c;从多场景、多内容&#xff0c;深耕高效协同的特色功能&#xff0c;基于多元化、灵活的交互体验&#xff0c;更大程度上解决线上业务办理抽象繁琐&#xff0c;线下业务办理的时空受限、业务…...

MySql面试精选—分库分表

目录 1、分库分表使用场景 2、常见的分库分表方案 3、常用的分库分表中间件...

Spring上下文生命周期

基于入口来分析 import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration;Configuration ComponentScan public cl…...

GitHub 标星 15w,如何用 Python 实现所有算法?

学会了 Python 基础知识&#xff0c;想进阶一下&#xff0c;那就来点算法吧&#xff01;毕竟编程语言只是工具&#xff0c;结构算法才是灵魂。 新手如何入门 Python 算法&#xff1f; 几位印度小哥在 GitHub 上建了一个各种 Python 算法的新手入门大全。从原理到代码&#xf…...

LeetCode 700. 二叉搜索树中的搜索

LeetCode 700. 二叉搜索树中的搜索 难度&#xff1a;easy\color{Green}{easy}easy 难度&#xff1a;middle\color{orange}{middle}middle 难度&#xff1a;hard\color{red}{hard}hard 题目描述 给定二叉搜索树&#xff08;BST&#xff09;的根节点 rootrootroot 和一个整数值…...

【数据结构】树与二叉树

目录 1、树的概念及结构 1.1、概念 1、树的特点 2、树与非树 1.2、概念 &#xff08;重要&#xff09; 1.3、树的表示形式 2、二叉树&#xff08;重点&#xff09; 2.1、概念 2.2、二叉树的特点 2.3、两种特殊的二叉树 1、满二叉树 2、完全二叉树 2.4、二叉树的性…...

Stress压力工具的部署及使用

Stress压力工具的部署及使用 下载地址&#xff1a;wget https://fossies.org/linux/privat/old/stress-1.0.5.tar.gz 1.部署 进入目录执行./autogen.sh [rootiZ2ze1pj93eyq389c2ppi5Z stress-1.0.5]# ./autogen.sh ps&#xff1a;如果执行过程中缺包&#xff0c;安装对应的…...

[蓝桥杯 2020 省 AB3] 乘法表

题目描述九九乘法表是学习乘法时必须要掌握的。在不同进制数下&#xff0c;需要不同的乘法表。例如, 四进制下的乘法表如下所示&#xff1a;1*11 2*12 2*210 3*13 3*212 3*321请注意&#xff0c;乘法表中两个数相乘的顺序必须为样例中所示的顺序&#xff0c;不能随意交换两个乘…...

Python基础知识

基础知识 基础知识包括输入输出、变量、数据类型、表达式、运算符这5个方面。 1.输入输出 Python有很多函数&#xff0c;后面我们会细讲&#xff0c;但这里先将两个最基本的函数&#xff1a;输入和输出。 输出函数print()&#xff0c;在前面我们已经用过了&#xff0c;语法…...

FME案例实战教程:聚焦实战应用,摆脱思路束缚,您值得拥有

一、教程链接&#xff08;一&#xff09;FME案例实战教程链接1.FME案例实战教程&#xff08;完整版&#xff09; ☚强烈推荐☚2.FME案例实战教程&#xff08;A组&#xff09;3.FME案例实战教程&#xff08;B组&#xff09;4.FME案例实战教程&#xff08;C组&#xff09;&#…...

【JavaScript】根据元素内容遍历元素的方案

▒ 目录 ▒&#x1f6eb; 导读需求1️⃣ jQuery2️⃣ XPATH&#xff08;document.evaluate&#xff09;3️⃣ 原生js&#xff08;querySelectorAll & Array&#xff09;&#x1f6ec; 文章小结&#x1f4d6; 参考资料&#x1f6eb; 导读 需求 因业务需要&#xff0c;根据元…...

kafka全解

目录Kafka概述定义消息队列目录结构分析传统消息队列的应用场景消息队列的两种模式点对点模式发布/订阅模式Kafka基础架构Kafka快速入门安装部署集群规划集群部署集群启停脚本Kafka命令行操作Kafka基础架构主题命令行操作生产者命令行操作消费者命令行操作kafka可视化工具Kafka…...

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用&#xff0c;操作系统&#xff1a;Ubuntu24.04&#xff0c;Neofj版本&#xff1a;2025.04.0。 Apt安装 Neofj可以进行官网安装&#xff1a;Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

ES6从入门到精通:前言

ES6简介 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript语言的重大更新&#xff0c;引入了许多新特性&#xff0c;包括语法糖、新数据类型、模块化支持等&#xff0c;显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var&#xf…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具

作者&#xff1a;来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗&#xff1f;了解下一期 Elasticsearch Engineer 培训的时间吧&#xff01; Elasticsearch 拥有众多新功能&#xff0c;助你为自己…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者

抖音增长新引擎&#xff1a;品融电商&#xff0c;一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中&#xff0c;品牌如何破浪前行&#xff1f;自建团队成本高、效果难控&#xff1b;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...