想了解更多内容,请访问:
51CTO和华为官方合作共建的鸿蒙技术社区
https://harmonyos.51cto.com
本文将从零开始实现一个图片组件,并展示如何使用 Matrix 实现图片的各种 ScaleType 缩放效果。
背景知识:
Matrix 内部通过维护一个 float[9] 的数组来构成 3x3 矩阵的形式,从底层原理来看,所有的变换方法就是更改数组中某个或某几个位置的数值;
Matrix提供了Translate(平移)、Scale(缩放)、Rotate(旋转)、Skew(扭曲)四中变换操作,这四种操作实质上是调用了setValues()方法来设置矩阵数组来达到变换效果。除Translate(平移)外,Scale(缩放)、Rotate(旋转)、Skew(扭曲)都可以围绕一个中心点来进行,如果不指定,在默认情况下是围绕(0, 0)来进行相应的变换的。
Matrix提供的四种操作,每一种都有pre、set、post三种形式。原因是矩阵乘法不满足乘法交换律,因此左乘还是右乘最终的效果都不一样。我们可以把Matrix变换想象成一个队列,队列里面包含了若干个变换操作,队列中每个操作按照先后顺序操作变换目标完成变换,pre相当于向队首增加一个操作,post相当于向队尾增加一个操作,set相当于清空当前队列重新设置。
鸿蒙 Image 组件没有对外公开 setMatrix 接口,如果项目中需要通过 setMatrix 来控制图片显示,可参考本文,自己实现自绘式 Image 组件。
创建图片组件
先创建一个组件类 MyImageComponent,因为是从零开始,所以我们从 Component 继承,包含以下三个属性:
public class MyImageComponent extends Component implements Component.DrawTask {
// 图片资源,从 src 属性读取
private Element element;
// 读取 scaleType 属性
private ScaleType scaleType = ScaleType.fitCenter;
// 用于实现 scaleType 的 Matrix
private final Matrix matrix = new Matrix();
// ...其他构造函数略
public MyImageComponent(Context context, AttrSet attrSet, int resId) {
super(context, attrSet, resId);
init(attrSet);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
然后执行初始化流程:
// 初始化,包括读取属性,根据 scaleType 设置 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);
}
}
// 根据 scaleType 属性实现对应的缩放效果
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();
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
ScaleType 效果展示和对应源码
以下逐个展示各种 ScaleType 效果及其实现代码,为方便对比不同大小的图片的 ScaleType 差异,准备了一大一小两张图片。
用于预览的 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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
以下是各种 scaleType 的效果和源码:
center
/**
* 图片居中显示在组件中,不对图片进行缩放
*/
private void center() {
float wTranslate = (getDisplayWidth() - getElementWidth()) * 0.5f;
float hTranslate = (getDisplayHeight() - getElementHeight()) * 0.5f;
matrix.postTranslate(wTranslate, hTranslate);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
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);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
fitXY
对X和Y方向独立缩放,直到图片铺满组件。这种方式可能会改变图片原本的宽高比,导致图片拉伸变形。
private void fitXY(){
float wPercent = (float)getDisplayWidth() / (float)getElementWidth();
float hPercent = (float)getDisplayHeight() / (float)getElementHeight();
matrix.setScale(wPercent, hPercent);
}
- 1.
- 2.
- 3.
- 4.
- 5.
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);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
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);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
centerCrop
保持图片的宽高比,等比例对图片进行X和Y方向缩放,直到每个方向都大于等于组件对应的尺寸,缩放后的图片居中显示在组件中,超出部分做裁剪处理。
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);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
centerInside
如果图片宽度<= 组件宽度&&图片高度<= 组件高度,不执行缩放,居中显示在组件中。其余情况按 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();
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
绘制图片组件
关键是这句 canvas.concat(matrix)
@Override
public void onDraw(Component component, Canvas canvas) {
// 以下是关键代码,将 matrix 应用到 canvas
canvas.concat(matrix);
// 将图片绘制到 canvas
element.drawToCanvas(canvas);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
项目地址:my-image-component
想了解更多内容,请访问:
51CTO和华为官方合作共建的鸿蒙技术社区
https://harmonyos.51cto.com