熟悉我们案例的老朋友一定记得之前做的AI旅行助手,用户问一个出行相关的问题,AI能生成一份详细的攻略,包含景点、美食、交通建议,以及对应的富媒体卡片。然后问题来了:用户在PC端使用这个应用时,发现应用默认全屏显示,挡住了其他工作窗口,想切换到悬浮窗模式却找不到入口。更麻烦的是,当用户想把这份攻略分享给朋友时,PC大屏上的内容一屏装不下,传统的截图方式只能截取当前可视区域,长内容需要多次截图再拼接,体验极其割裂。
今天,我将带你解决这两个看似独立实则紧密相关的问题:如何在PC端让应用优雅地以悬浮窗模式运行,以及如何在大屏设备上实现智能长截图功能。这两个问题的结合,正是HarmonyOS 6在多设备协同和内容分享场景下的重要实践。
一、问题现场:PC端应用的两大痛点
1.1 全屏霸屏的困扰
我们的AI旅行助手在手机端体验良好,但当用户尝试在PC(2in1设备)上使用时,遇到了这样的反馈:
"应用一打开就占满整个屏幕,我想边查攻略边做行程规划都不行。能不能像其他应用那样,有个小窗口悬浮在旁边?"
更具体的问题表现:
-
默认全屏:应用启动后自动全屏,无法调整窗口大小
-
多任务受阻:无法与其他应用并行工作
-
操作不便:需要频繁切换应用窗口
1.2 大屏截图的尴尬
PC端的大屏幕本应是优势,但在内容分享时却成了负担:
-
内容显示不全:攻略内容超过一屏,单次截图只能捕获部分
-
手动拼接困难:多次截图后需要手动对齐拼接
-
分享体验差:接收方需要来回滑动查看多张图片
-
分辨率问题:PC端高分辨率截图在手机端查看时文字过小
二、PC端悬浮窗模式:从全屏到自由的蜕变
2.1 问题根源:配置与代码的双重限制
通过查看官方文档和问题定位,我们发现PC端应用默认全屏的原因有两个:
-
配置文件限制 :
module.json5中的supportWindowMode属性默认为fullscreen -
代码未适配:没有针对2in1设备设置悬浮窗支持
2.2 技术原理:HarmonyOS的窗口管理机制
在HarmonyOS中,窗口模式的管理分为三个层次:
-
声明层 :在
module.json5中声明应用支持的窗口模式 -
设置层:在代码中动态设置窗口支持模式
-
恢复层:将窗口从全屏模式恢复到悬浮窗模式
关键API:
-
deviceInfo.deviceType:获取设备类型('2in1'表示PC/平板二合一设备) -
windowStage.setSupportedWindowModes():设置窗口支持模式 -
window.recover():将窗口恢复为悬浮窗模式
2.3 解决方案:智能设备检测与窗口模式适配
我们需要在应用启动时判断设备类型,如果是2in1设备,则自动启用悬浮窗模式。核心思路如下:
// 设备类型检测与窗口模式适配
import { window } from '@kit.ArkUI';
import { deviceInfo } from '@kit.BasicServicesKit';
import { bundleManager } from '@kit.AbilityKit';
class WindowModeManager {
/**
* 初始化窗口模式(在onWindowStageCreate中调用)
*/
static async initializeWindowMode(windowStage: window.WindowStage): Promise<void> {
// 1. 检测设备类型
const deviceType = deviceInfo.deviceType;
// 2. 如果是2in1设备,启用悬浮窗模式
if (deviceType === '2in1' && this.canUseWindowSessionManager()) {
await this.setFloatingWindowMode(windowStage);
}
}
/**
* 检查是否支持窗口会话管理能力
*/
private static canUseWindowSessionManager(): boolean {
return canIUse('SystemCapability.Window.SessionManager');
}
/**
* 设置悬浮窗模式
*/
private static async setFloatingWindowMode(windowStage: window.WindowStage): Promise<void> {
try {
// 设置支持悬浮窗和全屏两种模式
await windowStage.setSupportedWindowModes([
bundleManager.SupportWindowMode.FLOATING,
bundleManager.SupportWindowMode.FULL_SCREEN
]);
console.info('窗口模式设置成功:支持悬浮窗和全屏模式');
// 获取主窗口并恢复为悬浮窗模式
const mainWindow = await windowStage.getMainWindow();
await mainWindow.recover();
console.info('窗口已恢复为悬浮窗模式');
} catch (error) {
console.error('设置窗口模式失败:', error);
}
}
/**
* 获取当前窗口模式
*/
static async getCurrentWindowMode(): Promise<string> {
try {
const windowStage = window.getLastWindowStage();
if (!windowStage) return '未知';
const mainWindow = await windowStage.getMainWindow();
const mode = await mainWindow.getWindowMode();
return this.windowModeToString(mode);
} catch (error) {
console.error('获取窗口模式失败:', error);
return '未知';
}
}
/**
* 窗口模式枚举转字符串
*/
private static windowModeToString(mode: number): string {
switch (mode) {
case 1: return '全屏模式';
case 2: return '分屏模式';
case 3: return '悬浮窗模式';
default: return '未知模式';
}
}
}
2.4 在UIAbility中集成
// EntryAbility.ts - 在onWindowStageCreate中集成窗口模式管理
import { UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { WindowModeManager } from '../utils/WindowModeManager';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 1. 设置窗口模式(针对2in1设备)
WindowModeManager.initializeWindowMode(windowStage);
// 2. 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('加载页面失败:', err);
return;
}
console.info('页面加载成功');
});
}
// 其他生命周期方法...
}
2.5 用户控制界面
为了让用户能够手动切换窗口模式,我们还需要提供控制界面:
// 窗口模式控制组件
@Component
struct WindowControlPanel {
@State currentMode: string = '检测中...';
@State isFloatingSupported: boolean = false;
aboutToAppear(): void {
this.checkWindowMode();
}
async checkWindowMode(): Promise<void> {
this.currentMode = await WindowModeManager.getCurrentWindowMode();
this.isFloatingSupported = deviceInfo.deviceType === '2in1';
}
build() {
Column() {
// 当前模式显示
Text(`当前窗口模式:${this.currentMode}`)
.fontSize(16)
.fontColor('#333333')
.margin({ bottom: 20 })
// 模式切换按钮(仅2in1设备显示)
if (this.isFloatingSupported) {
Row() {
Button('切换到悬浮窗模式')
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.onClick(async () => {
await this.switchToFloatingMode();
await this.checkWindowMode();
})
.margin({ right: 10 })
Button('切换到全屏模式')
.backgroundColor('#F0F0F0')
.fontColor('#333333')
.onClick(async () => {
await this.switchToFullScreenMode();
await this.checkWindowMode();
})
}
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
} else {
Text('当前设备不支持悬浮窗模式')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 10 })
}
}
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
private async switchToFloatingMode(): Promise<void> {
try {
const windowStage = window.getLastWindowStage();
if (!windowStage) return;
const mainWindow = await windowStage.getMainWindow();
await mainWindow.recover();
prompt.showToast({ message: '已切换到悬浮窗模式' });
} catch (error) {
console.error('切换悬浮窗模式失败:', error);
prompt.showToast({ message: '切换失败' });
}
}
private async switchToFullScreenMode(): Promise<void> {
try {
const windowStage = window.getLastWindowStage();
if (!windowStage) return;
const mainWindow = await windowStage.getMainWindow();
await mainWindow.setFullScreen(true);
prompt.showToast({ message: '已切换到全屏模式' });
} catch (error) {
console.error('切换全屏模式失败:', error);
prompt.showToast({ message: '切换失败' });
}
}
}
三、PC端智能长截图:大屏内容的一键捕获
3.1 PC端长截图的特殊挑战
在PC端实现长截图,相比手机端有几个特殊挑战:
-
分辨率更高:PC屏幕分辨率通常为2K或4K,截图尺寸大
-
内容更复杂:PC端应用界面通常包含更多UI元素
-
性能要求更高:大尺寸图片处理需要更多内存和计算资源
-
多窗口干扰:悬浮窗模式下,其他应用窗口可能遮挡内容
3.2 技术原理:滚动截图与智能拼接
长截图的核心原理是:滚动-截图-裁剪-拼接四步循环。但在PC端,我们需要对这个流程进行优化:
// PC端优化的长截图管理器
class PCLongScreenshotManager {
private scrollStep: number = 0; // 每次滚动的像素数
private overlapRatio: number = 0.2; // 重叠比例(20%)
private maxHeight: number = 10000; // 最大截图高度限制
/**
* PC端优化的长截图捕获
*/
async captureForPC(
targetComponent: any, // 目标组件(List或Web)
componentType: 'list' | 'web'
): Promise<image.PixelMap> {
// 1. 根据设备类型调整参数
this.adjustParametersForPC();
// 2. 获取内容总高度
const totalHeight = await this.getContentHeight(targetComponent, componentType);
// 3. 计算滚动次数(考虑PC端大屏幕)
const viewportHeight = await this.getViewportHeight();
const scrollCount = this.calculateScrollCount(totalHeight, viewportHeight);
console.info(`PC端长截图:总高度=${totalHeight}px,视口高度=${viewportHeight}px,需要滚动${scrollCount}次`);
// 4. 执行滚动截图
const screenshotParts: image.PixelMap[] = [];
for (let i = 0; i <= scrollCount; i++) {
// 滚动到指定位置
await this.scrollToPosition(targetComponent, i * this.scrollStep, componentType);
// 等待稳定(PC端需要更长时间)
await this.waitForStable(componentType);
// 截图
const screenshot = await this.captureViewport(targetComponent, componentType);
// 裁剪(第一张不裁剪,后续只保留新增部分)
const croppedScreenshot = i === 0
? screenshot
: await this.cropNewContent(screenshot, this.overlapRatio);
screenshotParts.push(croppedScreenshot);
// 进度回调
this.onProgress?.(i + 1, scrollCount + 1);
}
// 5. 智能合并(PC端优化)
return await this.mergeScreenshotsForPC(screenshotParts);
}
/**
* 根据PC设备调整参数
*/
private adjustParametersForPC(): void {
const deviceType = deviceInfo.deviceType;
if (deviceType === '2in1' || deviceType === 'pc') {
// PC端参数调整
this.scrollStep = 600; // PC端滚动步长更大
this.overlapRatio = 0.15; // PC端重叠比例可以更小
this.maxHeight = 20000; // PC端允许更大的截图高度
} else {
// 移动端参数
this.scrollStep = 400;
this.overlapRatio = 0.2;
this.maxHeight = 10000;
}
}
/**
* PC端优化的截图合并
*/
private async mergeScreenshotsForPC(
parts: image.PixelMap[]
): Promise<image.PixelMap> {
if (parts.length === 0) {
throw new Error('没有截图可合并');
}
// 计算总高度
let totalHeight = 0;
for (const part of parts) {
totalHeight += part.height;
}
// PC端限制最大高度
if (totalHeight > this.maxHeight) {
console.warn(`截图总高度${totalHeight}px超过限制,将压缩到${this.maxHeight}px`);
totalHeight = this.maxHeight;
}
const firstPart = parts[0];
const width = firstPart.width;
// 创建最终图像(PC端使用更高压缩比)
const finalImage = await image.createPixelMap({
width,
height: totalHeight,
pixelFormat: image.PixelFormat.RGBA_8888,
alphaType: image.AlphaType.PREMUL,
editable: true
});
// 合并所有部分
let currentY = 0;
for (const part of parts) {
// PC端优化:如果超出最大高度,停止绘制
if (currentY + part.height > this.maxHeight) {
const remainingHeight = this.maxHeight - currentY;
if (remainingHeight > 0) {
// 绘制剩余部分
await image.drawPixelMap(finalImage, part, {
x: 0,
y: currentY,
width: part.width,
height: remainingHeight
});
}
break;
}
await image.drawPixelMap(finalImage, part, {
x: 0,
y: currentY,
width: part.width,
height: part.height
});
currentY += part.height;
}
return finalImage;
}
// 其他辅助方法...
private async getContentHeight(component: any, type: 'list' | 'web'): Promise<number> {
// 根据组件类型获取内容高度
if (type === 'list') {
return await this.getListHeight(component);
} else {
return await this.getWebHeight(component);
}
}
private async getListHeight(list: any): Promise<number> {
// 获取List组件总高度
// 实际实现需要根据List的itemCount和itemHeight计算
return 3000; // 示例值
}
private async getWebHeight(webView: any): Promise<number> {
// 获取Web组件内容高度
// 需要调用WebView的JavaScript接口
try {
const height = await webView.runJavaScript('document.documentElement.scrollHeight');
return parseInt(height) || 0;
} catch {
return 2000; // 默认值
}
}
private async getViewportHeight(): Promise<number> {
// 获取当前视口高度
return 800; // 示例值,实际需要动态获取
}
private calculateScrollCount(totalHeight: number, viewportHeight: number): number {
const effectiveScroll = viewportHeight * (1 - this.overlapRatio);
return Math.ceil((totalHeight - viewportHeight) / effectiveScroll);
}
private async scrollToPosition(
component: any,
position: number,
type: 'list' | 'web'
): Promise<void> {
if (type === 'list') {
component.scrollTo({ offset: position });
} else {
await component.runJavaScript(`window.scrollTo({ top: ${position}, behavior: 'smooth' })`);
}
await new Promise(resolve => setTimeout(resolve, 300));
}
private async waitForStable(type: 'list' | 'web'): Promise<void> {
// Web组件需要更长的等待时间
const waitTime = type === 'web' ? 500 : 200;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
private async captureViewport(
component: any,
type: 'list' | 'web'
): Promise<image.PixelMap> {
// 使用componentSnapshot API截图
return await componentSnapshot.get(component);
}
private async cropNewContent(
screenshot: image.PixelMap,
newContentRatio: number
): Promise<image.PixelMap> {
const width = screenshot.width;
const height = screenshot.height;
const newContentHeight = Math.floor(height * newContentRatio);
return await image.createPixelMap({
width,
height: newContentHeight,
src: screenshot,
region: {
x: 0,
y: height - newContentHeight,
width,
height: newContentHeight
}
});
}
// 进度回调
onProgress?: (current: number, total: number) => void;
}
3.3 Web组件的特殊处理
在PC端,Web组件的长截图需要特别注意:
// Web组件长截图优化
class PCWebScreenshotManager extends PCLongScreenshotManager {
private webViewController: WebviewController | null = null;
/**
* Web组件特殊配置
*/
async prepareWebForScreenshot(): Promise<void> {
if (!this.webViewController) return;
// 1. 启用全网页绘制(关键!)
await this.webViewController.enableWholeWebPageDrawing(true);
// 2. 等待页面完全加载
await this.waitForWebPageLoad();
// 3. 禁用动态效果以提高截图质量
await this.disableWebAnimations();
}
/**
* 等待Web页面加载完成
*/
private async waitForWebPageLoad(): Promise<void> {
return new Promise((resolve) => {
if (!this.webViewController) {
resolve();
return;
}
let loaded = false;
// 监听页面加载完成事件
this.webViewController.onPageEnd(() => {
loaded = true;
resolve();
});
// 超时处理
setTimeout(() => {
if (!loaded) {
console.warn('Web页面加载超时,继续执行截图');
resolve();
}
}, 10000); // 10秒超时
});
}
/**
* 禁用Web动画以提高截图质量
*/
private async disableWebAnimations(): Promise<void> {
if (!this.webViewController) return;
try {
await this.webViewController.runJavaScript(`
// 禁用CSS动画和过渡
const style = document.createElement('style');
style.textContent = \`
* {
animation: none !important;
transition: none !important;
}
\`;
document.head.appendChild(style);
// 暂停视频和音频
document.querySelectorAll('video, audio').forEach(media => {
media.pause();
});
`);
} catch (error) {
console.warn('禁用Web动画失败:', error);
}
}
/**
* 截图完成后清理
*/
async cleanupWebAfterScreenshot(): Promise<void> {
if (!this.webViewController) return;
// 恢复全网页绘制设置
await this.webViewController.enableWholeWebPageDrawing(false);
// 恢复滚动位置
await this.webViewController.runJavaScript('window.scrollTo(0, 0)');
}
}
3.4 悬浮窗模式下的截图优化
当应用处于悬浮窗模式时,截图需要特殊处理:
// 悬浮窗模式截图优化
class FloatingWindowScreenshotManager {
/**
* 获取悬浮窗的实际内容区域
*/
static async getFloatingWindowContentArea(): Promise<{ x: number, y: number, width: number, height: number }> {
try {
const windowStage = window.getLastWindowStage();
if (!windowStage) throw new Error('未找到窗口');
const mainWindow = await windowStage.getMainWindow();
const windowProperties = await mainWindow.getProperties();
// 排除窗口边框和标题栏
const contentArea = {
x: 0,
y: 40, // 标题栏高度
width: windowProperties.windowRect.width,
height: windowProperties.windowRect.height - 40 // 减去标题栏
};
return contentArea;
} catch (error) {
console.error('获取悬浮窗内容区域失败:', error);
// 返回默认值
return { x: 0, y: 0, width: 800, height: 600 };
}
}
/**
* 截图时排除其他窗口遮挡
*/
static async ensureWindowOnTop(): Promise<void> {
try {
const windowStage = window.getLastWindowStage();
if (!windowStage) return;
const mainWindow = await windowStage.getMainWindow();
// 将窗口置于最前
await mainWindow.moveTo(0, 0);
await mainWindow.focus();
// 短暂等待确保窗口在前
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.warn('置顶窗口失败:', error);
}
}
}
四、完整实现:悬浮窗与长截图的完美结合
4.1 集成解决方案
将悬浮窗模式和长截图功能结合,提供完整的PC端优化体验:
// 主页面集成示例
@Entry
@Component
struct MainPage {
@State isFloatingMode: boolean = false;
@State isCapturing: boolean = false;
@State captureProgress: number = 0;
private windowModeManager = new WindowModeManager();
private screenshotManager = new PCLongScreenshotManager();
private webScreenshotManager = new PCWebScreenshotManager();
build() {
Column() {
// 窗口模式控制
WindowControlPanel()
.margin({ bottom: 20 })
// 内容区域
List() {
// 你的应用内容...
ForEach(this.travelData, (item: TravelItem) => {
ListItem() {
TravelCard({ data: item })
}
})
}
.id('contentList') // 为截图提供ID
.height('100%')
// 截图控制
Row() {
Button('生成长截图')
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.onClick(() => {
this.captureLongScreenshot();
})
.enabled(!this.isCapturing)
if (this.isCapturing) {
Progress({ value: this.captureProgress, total: 100 })
.width(100)
.margin({ left: 10 })
}
}
.justifyContent(FlexAlign.Center)
.margin({ top: 20 })
}
.padding(20)
.onAppear(() => {
this.checkWindowMode();
})
}
async checkWindowMode(): Promise<void> {
const mode = await this.windowModeManager.getCurrentWindowMode();
this.isFloatingMode = mode.includes('悬浮窗');
}
async captureLongScreenshot(): Promise<void> {
this.isCapturing = true;
this.captureProgress = 0;
try {
// 如果是悬浮窗模式,确保窗口在最前
if (this.isFloatingMode) {
await FloatingWindowScreenshotManager.ensureWindowOnTop();
}
// 设置进度回调
this.screenshotManager.onProgress = (current, total) => {
this.captureProgress = Math.floor((current / total) * 100);
};
// 获取内容组件
const listComponent = this.$refs.contentList;
// 执行截图
const screenshot = await this.screenshotManager.captureForPC(
listComponent,
'list'
);
// 保存到相册
await this.saveToAlbum(screenshot);
prompt.showToast({ message: '长截图生成成功' });
} catch (error) {
console.error('截图失败:', error);
prompt.showToast({ message: '截图失败,请重试' });
} finally {
this.isCapturing = false;
this.captureProgress = 0;
}
}
async saveToAlbum(pixelMap: image.PixelMap): Promise<void> {
// 使用SaveButton安全控件保存到相册
// 注意:实际代码中需要使用SaveButton组件
console.info('保存截图到相册');
}
}
4.2 配置文件设置
在module.json5中正确配置窗口模式支持:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"actions": [
"action.system.home"
],
"entities": [
"entity.system.home"
]
}
],
"supportWindowMode": [
"fullscreen",
"split",
"floating"
]
}
]
}
}
五、测试与优化
5.1 测试要点
窗口模式测试:
-
在2in1设备上启动应用,应自动进入悬浮窗模式
-
窗口应可拖动、调整大小
-
切换全屏/悬浮窗模式功能正常
-
在多任务场景下与其他应用协同工作
长截图测试:
-
不同分辨率下的截图质量
-
超长内容的截图性能
-
悬浮窗模式下的截图准确性
-
Web组件动态内容的截图完整性
5.2 性能优化
-
内存管理:及时释放不再使用的PixelMap对象
-
进度反馈:提供实时进度提示,改善用户体验
-
错误恢复:截图失败时提供重试机制
-
智能裁剪:根据内容类型调整重叠比例
5.3 兼容性考虑
-
设备适配:区分手机、平板、PC设备的不同参数
-
系统版本:检查API可用性,提供降级方案
-
权限处理:妥善处理相册保存权限
-
资源释放:确保异常情况下的资源清理
六、技术思考:从功能实现到体验优化
6.1 设计哲学:以用户场景为中心
这两个功能的结合,体现了以用户场景为中心的设计思想:
-
窗口模式:不是简单的技术配置,而是为了满足用户多任务处理的需求
-
长截图:不是简单的图片拼接,而是为了满足用户内容分享的需求
6.2 技术实现:系统API的深度理解
通过这两个功能的实现,我们深入理解了HarmonyOS的:
-
窗口管理系统 :
setSupportedWindowModes、recover等API的使用场景 -
内容捕获系统 :
componentSnapshot、enableWholeWebPageDrawing等API的工作原理 -
设备适配系统 :
deviceInfo、canIUse等API的实践应用
6.3 未来展望
随着HarmonyOS的不断发展,这些功能还有进一步优化的空间:
-
智能窗口管理:根据用户使用习惯自动调整窗口模式和大小
-
AI增强截图:自动识别并裁剪无关内容,生成更精炼的长图
-
跨设备协同:在手机端开始截图,在PC端继续编辑
-
云端处理:将耗时的图片处理放到云端,降低设备负载
七、总结
从PC端悬浮窗模式到智能长截图,这两个看似独立的功能,在HarmonyOS 6的多设备协同生态中找到了完美的结合点。通过setSupportedWindowModes和recoverAPI,我们让应用在PC端更加灵活;通过滚动截图和智能拼接算法,我们让内容分享更加便捷。
更重要的是,这个解决方案体现了HarmonyOS开发的核心思想:深入理解系统能力,紧密结合用户场景,用技术解决实际问题。无论是窗口管理还是内容捕获,都不是孤立的技术点,而是构成完整用户体验的重要环节。
在HarmonyOS 6的时代,PC端应用不再只是手机应用的简单移植,而是需要充分考虑大屏设备特性、多任务场景和高效内容分享的新体验。通过本文的实践,相信你已经掌握了在PC端优化应用体验的关键技术,期待你在自己的应用中创造出更多优秀的跨设备体验。