HarmonyOS用Matrix實現(xiàn)各種圖片ScaleType縮放
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
本文將從零開始實現(xiàn)一個圖片組件,并展示如何使用 Matrix 實現(xiàn)圖片的各種 ScaleType 縮放效果。
背景知識:
Matrix 內(nèi)部通過維護一個 float[9] 的數(shù)組來構(gòu)成 3x3 矩陣的形式,從底層原理來看,所有的變換方法就是更改數(shù)組中某個或某幾個位置的數(shù)值;
Matrix提供了Translate(平移)、Scale(縮放)、Rotate(旋轉(zhuǎn))、Skew(扭曲)四中變換操作,這四種操作實質(zhì)上是調(diào)用了setValues()方法來設(shè)置矩陣數(shù)組來達到變換效果。除Translate(平移)外,Scale(縮放)、Rotate(旋轉(zhuǎn))、Skew(扭曲)都可以圍繞一個中心點來進行,如果不指定,在默認情況下是圍繞(0, 0)來進行相應(yīng)的變換的。
Matrix提供的四種操作,每一種都有pre、set、post三種形式。原因是矩陣乘法不滿足乘法交換律,因此左乘還是右乘最終的效果都不一樣。我們可以把Matrix變換想象成一個隊列,隊列里面包含了若干個變換操作,隊列中每個操作按照先后順序操作變換目標完成變換,pre相當于向隊首增加一個操作,post相當于向隊尾增加一個操作,set相當于清空當前隊列重新設(shè)置。
鴻蒙 Image 組件沒有對外公開 setMatrix 接口,如果項目中需要通過 setMatrix 來控制圖片顯示,可參考本文,自己實現(xiàn)自繪式 Image 組件。
創(chuàng)建圖片組件
先創(chuàng)建一個組件類 MyImageComponent,因為是從零開始,所以我們從 Component 繼承,包含以下三個屬性:
- public class MyImageComponent extends Component implements Component.DrawTask {
- // 圖片資源,從 src 屬性讀取
- private Element element;
- // 讀取 scaleType 屬性
- private ScaleType scaleType = ScaleType.fitCenter;
- // 用于實現(xiàn) scaleType 的 Matrix
- private final Matrix matrix = new Matrix();
- // ...其他構(gòu)造函數(shù)略
- public MyImageComponent(Context context, AttrSet attrSet, int resId) {
- super(context, attrSet, resId);
- init(attrSet);
- }
- }
然后執(zhí)行初始化流程:
- // 初始化,包括讀取屬性,根據(jù) scaleType 設(shè)置 matrix,添加繪制方法
- private void init(AttrSet attrSet) {
- readAttrSet(attrSet);
- dealScaleType();
- addDrawTask(this);
- }
- // 讀取 xml 屬性,包括 src 和 scaleType
- private void readAttrSet(AttrSet attrSet) {
- element = attrSet.getAttr("src").get().getElement();
- if (attrSet.getAttr("scaleType").isPresent()) {
- String scaleTypeString = attrSet.getAttr("scaleType").get().getStringValue();
- scaleType = Utils.getEnumFromString(ScaleType.class, scaleTypeString, ScaleType.center);
- }
- }
- // 根據(jù) scaleType 屬性實現(xiàn)對應(yīng)的縮放效果
- private void dealScaleType() {
- switch (scaleType) {
- default:
- case fitCenter:
- fitCenter();
- break;
- case center:
- center();
- break;
- case fitXY:
- fitXY();
- break;
- case fitStart:
- fitStart();
- break;
- case fitEnd:
- fitEnd();
- break;
- case centerCrop:
- centerCrop();
- break;
- case centerInside:
- centerInside();
- break;
- }
- }
- private int getDisplayWidth() {
- return getWidth() - getPaddingLeft() - getPaddingRight();
- }
- private int getDisplayHeight() {
- return getHeight() - getPaddingLeft() - getPaddingRight();
- }
- private int getElementWidth() {
- return element.getWidth();
- }
- private int getElementHeight() {
- return element.getHeight();
- }
ScaleType 效果展示和對應(yīng)源碼
以下逐個展示各種 ScaleType 效果及其實現(xiàn)代碼,為方便對比不同大小的圖片的 ScaleType 差異,準備了一大一小兩張圖片。


用于預(yù)覽的 xml 布局代碼如下:
- <?xml version="1.0" encoding="utf-8"?>
- <DirectionalLayout
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- xmlns:app="http://schemas.ohos.com/apk/res-auto"
- ohos:height="match_parent"
- ohos:width="match_parent"
- ohos:alignment="center"
- ohos:orientation="vertical">
- <com.bm.mycomponent.MyImageComponent
- ohos:width="200vp"
- ohos:height="200vp"
- ohos:background_element="#4682B4"
- ohos:margin="6vp"
- app:src="$media:big"
- app:scaleType="center"
- />
- <com.bm.mycomponent.MyImageComponent
- ohos:width="200vp"
- ohos:height="200vp"
- ohos:margin="6vp"
- ohos:background_element="#4682B4"
- app:src="$media:small"
- app:scaleType="center"
- />
- </DirectionalLayout>
以下是各種 scaleType 的效果和源碼:
center

- /**
- * 圖片居中顯示在組件中,不對圖片進行縮放
- */
- private void center() {
- float wTranslate = (getDisplayWidth() - getElementWidth()) * 0.5f;
- float hTranslate = (getDisplayHeight() - getElementHeight()) * 0.5f;
- matrix.postTranslate(wTranslate, hTranslate);
- }
fitCenter
保持圖片的寬高比,對圖片進行X和Y方向縮放,直到一個方向鋪滿組件,縮放后的圖片居中顯示在組件中。

- private void fitCenter() {
- float wPercent = (float)getDisplayWidth() / (float)getElementWidth();
- float hPercent = (float)getDisplayHeight() / (float)getElementHeight();
- float minPercent = Math.min(wPercent, hPercent);
- float targetWidth = minPercent * getElementWidth();
- float targetHeight = minPercent * getElementHeight();
- float wTranslate = (getDisplayWidth() - targetWidth) * 0.5f;
- float hTranslate = (getDisplayHeight() - targetHeight) * 0.5f;
- matrix.setScale(minPercent, minPercent);
- matrix.postTranslate(wTranslate, hTranslate);
- }
fitXY
對X和Y方向獨立縮放,直到圖片鋪滿組件。這種方式可能會改變圖片原本的寬高比,導致圖片拉伸變形。

- private void fitXY(){
- float wPercent = (float)getDisplayWidth() / (float)getElementWidth();
- float hPercent = (float)getDisplayHeight() / (float)getElementHeight();
- matrix.setScale(wPercent, hPercent);
- }
fitStart
保持圖片的寬高比,對圖片進行X和Y方向縮放,直到一個方向鋪滿組件,縮放后的圖片與組件左上角對齊進行顯示。

- private void fitStart() {
- float wPercent = (float)getDisplayWidth() / (float)getElementWidth();
- float hPercent = (float)getDisplayHeight() / (float)getElementHeight();
- float minPercent = Math.min(wPercent, hPercent);
- matrix.setScale(minPercent, minPercent);
- }
fitEnd
保持圖片的寬高比,對圖片進行X和Y方向縮放,直到一個方向鋪滿組件,縮放后的圖片與組件右下角對齊進行顯示。

- private void fitEnd() {
- float wPercent = (float)getDisplayWidth() / (float)getElementWidth();
- float hPercent = (float)getDisplayHeight() / (float)getElementHeight();
- float minPercent = Math.min(wPercent, hPercent);
- float targetWidth = minPercent * getElementWidth();
- float targetHeight = minPercent * getElementHeight();
- float wTranslate = getDisplayWidth() - targetWidth;
- float hTranslate = getDisplayHeight() - targetHeight;
- matrix.setScale(minPercent, minPercent);
- matrix.postTranslate(wTranslate, hTranslate);
- }
centerCrop
保持圖片的寬高比,等比例對圖片進行X和Y方向縮放,直到每個方向都大于等于組件對應(yīng)的尺寸,縮放后的圖片居中顯示在組件中,超出部分做裁剪處理。

- private void centerCrop() {
- float scale;
- float dx;
- float dy;
- if (getElementWidth() * getDisplayHeight() > getDisplayWidth() * getElementHeight()) {
- scale = (float)getDisplayHeight() / (float)getElementHeight();
- dx = (getDisplayWidth() - getElementWidth() * scale) * 0.5f;
- dy = 0f;
- } else {
- scale = (float)getDisplayWidth() / (float)getElementWidth();
- dx = 0f;
- dy = (getDisplayHeight() - getElementHeight() * scale) * 0.5f;
- }
- matrix.setScale(scale, scale);
- matrix.postTranslate(dx + 0.5f, dy + 0.5f);
- }
centerInside
如果圖片寬度<= 組件寬度&&圖片高度<= 組件高度,不執(zhí)行縮放,居中顯示在組件中。其余情況按 fitCenter 處理。

- private void centerInside() {
- if (getElementWidth() <= getDisplayWidth() && getElementHeight() <= getElementHeight()){
- float wTranslate = (getDisplayWidth() - getElementWidth()) * 0.5f;
- float hTranslate = (getDisplayHeight() - getElementHeight()) * 0.5f;
- matrix.setTranslate(wTranslate, hTranslate);
- } else {
- fitCenter();
- }
- }
繪制圖片組件
關(guān)鍵是這句 canvas.concat(matrix)
- @Override
- public void onDraw(Component component, Canvas canvas) {
- // 以下是關(guān)鍵代碼,將 matrix 應(yīng)用到 canvas
- canvas.concat(matrix);
- // 將圖片繪制到 canvas
- element.drawToCanvas(canvas);
- }
項目地址:my-image-component
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)