介紹
本篇Codelab基于手勢處理和截屏能力,介紹了手勢截屏的實現(xiàn)過程。樣例主要包括以下功能:
- 根據(jù)下滑手勢調(diào)用全屏截圖功能。
- 全屏截圖,同時右下角有彈窗提示截圖成功。
- 根據(jù)雙擊手勢調(diào)用區(qū)域截圖功能。
- 區(qū)域截圖,通過調(diào)整選擇框大小完成。
相關(guān)概念
- Canvas:畫布組件,用于自定義繪制圖形。
- CanvasRenderingContext2D對象:使用RenderingContext在Canvas組件上進(jìn)行繪制,繪制對象可以是矩形、文本、圖片等。
- 雙擊手勢:手指雙擊屏幕回調(diào)事件。
- 手指滑動手勢:手指在屏幕滑動回調(diào)事件。
相關(guān)權(quán)限
- 本篇Codelab用到屏幕截圖的能力,需要在配置文件module.json5里添加屏幕截圖的權(quán)限:ohos.permission.CAPTURE_SCREEN。
- 本篇Codelab需要使用的screenshot為系統(tǒng)接口。需要使用Full SDK手動從鏡像站點獲取,并在DevEco Studio中替換。
- 本篇Codelab使用的部分API僅系統(tǒng)應(yīng)用可用,需要提升應(yīng)用等級為system_core。
環(huán)境搭建
軟件要求
- [DevEco Studio]版本:DevEco Studio 3.1 Release。
- OpenHarmony SDK版本:API version 9。
硬件要求
- 開發(fā)板類型:[潤和RK3568開發(fā)板]。
- OpenHarmony系統(tǒng):3.2 Release。
環(huán)境搭建
完成本篇Codelab我們首先要完成開發(fā)環(huán)境的搭建,本示例以RK3568開發(fā)板為例,參照以下步驟進(jìn)行:
- [獲取OpenHarmony系統(tǒng)版本]:標(biāo)準(zhǔn)系統(tǒng)解決方案(二進(jìn)制)。以3.2 Release版本為例:
- 搭建燒錄環(huán)境。
- [完成DevEco Device Tool的安裝]
- [完成RK3568開發(fā)板的燒錄]
- 搭建開發(fā)環(huán)境。
代碼結(jié)構(gòu)解讀
本篇Codelab只對核心代碼進(jìn)行講解,對于完整代碼,我們會在gitee中提供。
├──entry/src/main/ets // 代碼區(qū)
│ ├──common
│ │ └──utils
│ │ ├──CommonConstants.ets // 公共常量類
│ │ ├──DrawUtil.ets // 畫布相關(guān)工具類
│ │ └──Logger.ets // 日志打印類
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口類
│ ├──model
│ │ └──OffsetModel.ets // 區(qū)域截圖坐標(biāo)相關(guān)工具類
│ ├──pages
│ │ └──GestureScreenshot.ets // 主界面
│ └──view
│ ├──AreaScreenshot.ets // 自定義區(qū)域截屏組件類
│ └──ScreenshotDialog.ets // 自定義截屏顯示彈窗組件類
└──entry/src/main/resources // 資源文件目錄
構(gòu)建截屏主頁面
使用下滑手勢,進(jìn)行全屏截圖并展示圖片。效果如圖所示:
主界面主要實現(xiàn)以下功能:
- 下滑手勢綁定在主界面上,雙擊手勢綁定在區(qū)域手勢的最底層Stack組件上。
- 如果使用下滑手勢,就進(jìn)行全屏截圖并展示圖片。
- 如果使用雙擊手勢,就喚起區(qū)域截圖相關(guān)組件。
// 區(qū)域截圖最底層,當(dāng)主頁面縮放后會露出,設(shè)置為黑色
Stack() {
// 主頁面布局
Column() {
...
})
// 添加滑動手勢事件
.gesture(
// fingers:觸發(fā)手指數(shù) direction:觸發(fā)方向 distance:觸發(fā)滑動距離
PanGesture({
fingers: 1,
direction: PanDirection.Down,
distance: CommonConstants.MINIMUM_FINGER_DISTANCE
})// 觸發(fā)開始回調(diào)
.onActionStart(() = > {
let screenshotOptions: screenshot.ScreenshotOptions = {
rotation: 0
};
screenshot.save(screenshotOptions, (err: Error, data: image.PixelMap) = > {
if (err) {
Logger.error(`Failed to save the screenshot. Error:${ JSON.stringify(err) }`);
}
if (this.pixelMap !== undefined) {
this.pixelMap.release();
}
this.pixelMap = data;
this.dialogController.open();
});
})
)
.scale(this.scaleNum)
// 區(qū)域截圖相關(guān)組件
AreaScreenshot({ showScreen: this.showScreen, pixelMap: this.pixelMap, scaleNum: this.scaleNum })
}
.backgroundColor($r('app.color.black_area'))
// 添加雙擊手勢事件
.gesture(
TapGesture({ count: 2 })
.onAction(() = > {
this.showScreen = true;
this.scaleNum = {
x: CommonConstants.X_SCALE_DOWN,
y: CommonConstants.Y_SCALE_DOWN
}
})
)
構(gòu)建區(qū)域截圖組件
本章節(jié)將完成區(qū)域選擇框的繪制并完成區(qū)域截圖,效果如圖所示:
在繪制區(qū)域選擇框之前,首先需要在AreaScreenshot.ets的aboutToAppear方法中獲取屏幕的寬和高,并初始化offsetModel和drawUtil對象(初始化參數(shù)為屏幕的寬高)。offsetModel對輸入的坐標(biāo)進(jìn)行計算和更改,drawUtil使用offsetModel的坐標(biāo)在屏幕上繪制區(qū)域選擇框。
// AreaScreenshot.ets
aboutToAppear() {
window.getLastWindow(getContext(this))
.then((window) = > {
let property = window.getWindowProperties();
this.systemBarHeight = property.windowRect.top;
drawUtil.initDrawUtil(
this.canvasRenderingContext,
px2vp(property.windowRect.width),
px2vp(property.windowRect.height)
);
offsetModel.initOffsetModel(
px2vp(property.windowRect.width),
px2vp(property.windowRect.height)
);
// 在展示截圖的時候,用于計算圖片大小
this.screenAspectRatio = px2vp(property.windowRect.height) / px2vp(property.windowRect.width);
})
.catch((err: Error) = > {
Logger.error(`window loading has error: ${ JSON.stringify(err) }`);
})
}
在AreaScreenshot.ets布局頁面中添加Canvas組件,通過showScreen變量控制局部截屏頁面的顯示,并控制主頁面的縮放。步驟如下:
- 根據(jù)手指按下的位置確定需要移動的邊框。
- 手指移動后,更新offsetModel記錄的坐標(biāo)信息。
- 根據(jù)offsetModel提供的坐標(biāo),使用drawUtil繪制區(qū)域選擇框。
- 點擊保存按鈕后,設(shè)置截屏區(qū)域坐標(biāo)。
- 根據(jù)截屏區(qū)域坐標(biāo)進(jìn)行區(qū)域截屏。
// AreaScreenshot.ets
// 關(guān)閉區(qū)域截屏相關(guān)組件,并還原主頁面
private resetParameter() {
this.showScreen = false;
this.scaleNum = {
x: CommonConstants.NO_SCALE_DOWN,
y: CommonConstants.NO_SCALE_DOWN
};
offsetModel.resetDefaultOffSet();
}
// 使用if渲染,控制區(qū)域截圖相關(guān)組件的顯隱
if (this.showScreen) {
Stack() {
Canvas(this.canvasRenderingContext)
...
.onReady(() = > {
// 通過draw方法繪制選擇框和非高亮區(qū)域
drawUtil.draw();
})
// 截圖的工具欄
Row() {
...
// 區(qū)域截圖并展示圖像
Image($r('app.media.ic_save'))
.onClick(() = > {
let screenshotOptions: screenshot.ScreenshotOptions = {
// 截屏區(qū)域Rect參數(shù)
screenRect: {
left: vp2px(offsetModel.getXLeft()),
top: vp2px(offsetModel.getYTop()) + this.systemBarHeight,
width: vp2px(offsetModel.getWidth()),
height: vp2px(offsetModel.getHeight())
} as screenshot.Rect,
// 截圖的大小
imageSize: {
width: vp2px(offsetModel.getWidth()),
height: vp2px(offsetModel.getHeight())
} as screenshot.Size,
rotation: 0,
displayId: 0
};
screenshot.save(screenshotOptions, (err: Error, data: image.PixelMap) = > {
if (err) {
Logger.error(`Failed to save the screenshot. Error:${JSON.stringify(err)}`);
}
if (this.pixelMap !== undefined) {
this.pixelMap.release();
}
this.pixelMap = data;
// 使用彈窗組件展示截完的圖片
this.dialogController.open();
});
this.resetParameter();
})
}
...
// 根據(jù)手指位置調(diào)整選擇框大小和位置
.onTouch((event: TouchEvent) = > {
switch(event.type) {
case TouchType.Down:
// 根據(jù)手指位置,判斷移動哪個坐標(biāo)
offsetModel.setXLocationType(event.touches[0].screenX);
offsetModel.setYLocationType(event.touches[0].screenY);
break;
case TouchType.Move:
// 更新坐標(biāo)信息,并保證坐標(biāo)值合法
offsetModel.resetOffsetXY(event.touches[0].screenX, event.touches[0].screenY);
drawUtil.draw();
break;
default:
break;
}
})
}
區(qū)域選擇框工具類的實現(xiàn)
在構(gòu)建區(qū)域截圖組件中介紹了OffsetModel和DrawUtil兩個工具類,本章節(jié)介紹一下具體的實現(xiàn)步驟。
使用OffsetModel校驗坐標(biāo)的范圍,并保存坐標(biāo)相關(guān)信息。
- 在初始化對象的時候,根據(jù)屏幕的縮放比例計算出黑色區(qū)域的寬高。
- 使用setXLocationType方法和setYLocationType方法,判斷需要移動的x、y坐標(biāo)位置。
- 根據(jù)傳入的x、y坐標(biāo)值,更改offset對應(yīng)的坐標(biāo)值,并保證選擇框的寬高大于等于預(yù)設(shè)的選擇框的最小值。
- 再次校驗offset坐標(biāo)值,是否超出可截屏區(qū)域。
// OffsetModel.ets
public initOffsetModel(width: number, height: number) {
...
this.blackAreaWidth = this.screenWidth * (1 - CommonConstant.X_SCALE_DOWN);
this.blackAreaWidth = this.blackAreaWidth / CommonConstant.BLACK_AREA_NUM;
this.blackAreaHeight = this.screenHeight * (1 - CommonConstant.Y_SCALE_DOWN);
this.blackAreaHeight = this.blackAreaHeight / CommonConstant.BLACK_AREA_NUM;
}
// 判斷x坐標(biāo)位置
public setXLocationType(offsetX: number) {
if (offsetX > this.offsetXRight - CommonConstant.OFFSET_RANGE &&
offsetX < this.offsetXRight + CommonConstant.OFFSET_RANGE) {
this.xLocationType = XLocationEnum.XRight;
} else if (offsetX > this.offsetXLeft - CommonConstant.OFFSET_RANGE &&
offsetX < this.offsetXLeft + CommonConstant.OFFSET_RANGE) {
this.xLocationType = XLocationEnum.XLeft;
} else {
this.xLocationType = XLocationEnum.noChange;
}
}
// 判斷y坐標(biāo)位置
public setYLocationType(offsetY: number) {
...
}
// 根據(jù)參數(shù)改變坐標(biāo)值
public resetOffsetXY(offsetX: number, offsetY: number) {
if (this.xLocationType === XLocationEnum.XLeft) {
this.offsetXLeft = this.offsetXRight - offsetX < CommonConstant.OFFSET_RANGE * 2 ?
this.offsetXLeft : offsetX;
}
...
this.checkOffsetXY();
}
// 再次校驗坐標(biāo)值,是否超出可截屏區(qū)域
private checkOffsetXY() {
this.offsetXLeft = this.offsetXLeft < this.blackAreaWidth ? this.blackAreaWidth : this.offsetXLeft;
this.offsetXRight = this.offsetXRight > this.screenWidth - this.blackAreaWidth ?
this.screenWidth - this.blackAreaWidth : this.offsetXRight;
this.offsetYTop = this.offsetYTop < this.blackAreaHeight ? this.blackAreaHeight : this.offsetYTop;
this.offsetYBottom = this.offsetYBottom > this.screenHeight - this.blackAreaHeight ?
this.screenHeight - this.blackAreaHeight : this.offsetYBottom;
}
DrawUtil主要提供繪制方法,用于繪制區(qū)域選擇框。
// DrawUtil.ets
// 繪制整個區(qū)域選擇框
public draw() {
this.offsetXLeft = offsetModel.getXLeft();
this.offsetXRight = offsetModel.getXRight();
this.offsetYTop = offsetModel.getYTop();
this.offsetYBottom = offsetModel.getYBottom();
// 填充非高亮區(qū)域
this.drawScreenSelection();
// 繪制框選線
this.drawLines();
}
// 填充非高亮區(qū)域,設(shè)置回形區(qū)域并填充顏色
private drawScreenSelection() {
this.canvasContext.clearRect(0, 0, this.screenWidth, this.screenHeight)
this.canvasContext.beginPath();
this.canvasContext.moveTo(0, 0);
this.canvasContext.lineTo(this.screenWidth, 0);
this.canvasContext.lineTo(this.screenWidth, this.screenHeight);
this.canvasContext.lineTo(0, this.screenHeight);
this.canvasContext.closePath();
this.canvasContext.moveTo(this.offsetXRight, this.offsetYTop);
this.canvasContext.lineTo(this.offsetXLeft, this.offsetYTop);
this.canvasContext.lineTo(this.offsetXLeft, this.offsetYBottom);
this.canvasContext.lineTo(this.offsetXRight, this.offsetYBottom);
this.canvasContext.globalAlpha = Constants.UNSELECT_AREA_ALPHA;
this.canvasContext.fillStyle = Constants.UNSELECT_AREA_COLOR;
this.canvasContext.closePath();
this.canvasContext.fill();
}
// 繪制框選線
private drawLines() {
this.canvasContext.beginPath();
...
this.canvasContext.moveTo(
(this.offsetXLeft + Constants.LINES_MAX_LENGTH),
(this.offsetYTop - Constants.GAP_WIDTH)
);
this.canvasContext.lineTo(
(this.offsetXLeft - Constants.GAP_WIDTH),
(this.offsetYTop - Constants.GAP_WIDTH)
);
this.canvasContext.lineTo(
(this.offsetXLeft - Constants.GAP_WIDTH),
(this.offsetYTop + Constants.LINES_MAX_LENGTH)
);
...
this.canvasContext.stroke();
}
展示截圖
采用彈窗組件展示截屏,需要在aboutToAppear方法中計算對應(yīng)的寬度:
- 截圖長寬比小于或者等于屏幕長寬比:此截圖展示時和全屏截圖展示時等寬。
- 截圖長寬比大于屏幕長寬比:此截圖展示時和全屏截圖展示時等長,通過計算對應(yīng)的寬來實現(xiàn)。
// ScreenshotDialog.ets
aboutToAppear() {
this.getDialogWidth();
}
...
private async getDialogWidth() {
if (this.pixelMap !== undefined) {
let info = await this.pixelMap.getImageInfo();
let pixelMapAspectRatio = info.size.height / info.size.width;
if ((this.screenAspectRatio !== -1) && (pixelMapAspectRatio > this.screenAspectRatio)) {
let width = CommonConstants.HEIGHT_FIRST / pixelMapAspectRatio * this.screenAspectRatio;
this.dialogWidth = width + '%';
} else {
this.dialogWidth = CommonConstants.WIDTH_FIRST;
}
}
}
審核編輯 黃宇
-
鴻蒙
+關(guān)注
關(guān)注
57文章
2351瀏覽量
42849 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3722瀏覽量
16313
發(fā)布評論請先 登錄
相關(guān)推薦
評論