一、前文
之前有個朋友委託我實現一個圖片拼接的元件,感覺挺有意思,於是週末花了些時間去研究了下,其實拼接這一步並不難,但是我在研究中發現了Matrix這個東西,非常好的東西。為此,我竟然拾起了多年沒有動過的線性代數。
二、原理
要徹底搞懂matrix還是需要一定的線性代數上面的理解,不過對於基本使用,瞭解到矩陣乘法就足夠了。
在android座標系中,分為x、y和z三個軸,分別代表了長、寬、高三個維度。如下圖所示
在android中,使用三維座標(x,y,z)組成一個行列式與一個三階行列式進行矩陣乘法。
圖中顯示的使用初始座標組成的矩陣與單位矩陣進行矩陣乘法。矩陣乘法使用可以參考矩陣乘法
Martix會把輸入進來的矩陣帶入到其內部的矩陣中進行計算,最終輸出新的矩陣,來達到對圖形形態的處理。
三、基本方法的使用
Matrix提供的基本方法有三種模式,
- setXXX()方法,例如 setRotate(),setScale()
- preXXX()方法,例如 preRotate(),preScale()
- postXXX()方法,例如 postRotate(),postScale()
其中,setXXX()會先將矩陣重置為單位矩陣,然後再進行矩陣變幻
preXXX()和postXXX()方法會牽扯到矩陣的前乘和後乘,如果瞭解了矩陣乘法規則,就會明白矩陣前乘和後乘得出來的結果是不一樣的,不過一般情況下都會選擇使用post方法,後乘。
其中還有擴充套件方法比如: - mapRect(rect) / mapRect(desRect,orgRect)
第一個方法即使用原始矩陣代入運算,會將返回的矩陣直接覆蓋在傳入的矩陣中
第二個方法則是對於需要儲存原始矩陣的情況下,會把原始矩陣的計算結果賦值到指定的矩陣中 - setRectToRect(src,des,stf)
這個方法相當於將原始矩陣填充到目標矩陣中,所以也就要求兩個矩陣都是有值的。其中填充模式由第三個引數決定。/** * 獨立縮放X和Y,直到和src的rect和目標rect確切的匹配。這可能會改變原始rect的寬高比 */ FILL(0), /** * 在保持原有寬高比的情況下計算出一個合適的縮放比例,但也會確保原始rect合適的填入目標rect, * 最終會把開始的一個邊與目標的開始邊左邊對齊 */ START(1), /** * 與START類似,不過最終結果會盡可能居中 */ CENTER(2), /** * 與START類似,不過最終結果會盡可能靠右邊 */ END(3); 複製程式碼
- invert(inverse)
反轉矩陣,可以應用到類似倒影一類的實現中 - setPolyToPoly(src,srcIndex,dst,dstIndex,pointCount)
這是一個比較神奇的方法。隨著pointCount點數量,可以對原始矩陣進行平移、旋轉、錯切、翻頁效果。功能非常強大。
此外,關於Matrix還有顏色變幻等效果,更多擴充套件用法後面會講到。
四、實踐到自定義view中
寫一個自定義view,最重要的是要了解view的繪製過程。簡單的繪製流程如下
其中不帶on的方法都為排程方法,不可被重寫,這些方法裡面會把前期一些必要的資料準備出來,帶on字首的方法都是實際進行處理的方法。
measure方法是測量控制元件大小的,layout是用來佈局,根據measure測量的結果,把其中每個元素在其內部進行位置的計算。最後會執行的draw方法,draw也分為draw和onDraw,可以根據自己需求來改寫對應的方法。
其中,onMeasure的方法如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// measure child img
final int maxImgWidth = getMeasuredWidth();
final int maxImgHeight = getMeasuredHeight();
final int measureWidthSize = MeasureSpec.getSize(widthMeasureSpec);
final int measureHeightSize = MeasureSpec.getSize(heightMeasureSpec);
int totalImageHeight = 0;
// 縮放和旋轉影響size的交給measure
for (int i = 0; i < imgList.size(); i++) {
ImageData imageData = imgList.get(i);
final int imgOrgWidth = imageData.getImgWidth();
final int imgOrgHeight = imageData.getImgHeight();
int imgRotateWidth;
int imgRotateHeight;
if (imageData.scale > 0) {
imageData.matrix.setScale(imageData.scale, imageData.scale);
} else {
final float sizeProportion = (float) imgOrgWidth / imgOrgHeight;
if (imgOrgHeight > imgOrgWidth) {
if (measureHeightSize == MeasureSpec.EXACTLY &&
imgOrgHeight > maxImgHeight) {
imgRotateWidth = (int) (maxImgHeight * sizeProportion);
imgRotateHeight = maxImgHeight;
} else {
imgRotateWidth = imgOrgWidth;
imgRotateHeight = imgOrgHeight;
}
} else {
if (imgOrgWidth > maxImgWidth) {
imgRotateHeight = (int) (maxImgWidth / sizeProportion);
imgRotateWidth = maxImgWidth;
} else {
imgRotateWidth = imgOrgWidth;
imgRotateHeight = imgOrgHeight;
}
}
// resize
imageData.reSize(imgRotateWidth, imgRotateHeight);
}
// rotate
imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
imageData.matrix.postRotate(imageData.rotateAngle, imageData.drawRect.centerX(),
imageData.drawRect.top + (imageData.drawRect.height() * 0.5f));
imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
totalImageHeight += imageData.drawRect.height();
}
switch (measureHeightSize) {
// wrap_content
case MeasureSpec.AT_MOST:
setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
measureWidthSize), MeasureSpec.makeMeasureSpec(totalImageHeight,
measureHeightSize));
break;
// match_parent or accurate num
case MeasureSpec.EXACTLY:
setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
measureHeightSize));
break;
case MeasureSpec.UNSPECIFIED:
setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
measureWidthSize), MeasureSpec.makeMeasureSpec(totalImageHeight,
measureHeightSize));
break;
}
}
複製程式碼
所有影響尺寸計算相關的方法都會放到這個measure裡面進行計算,比如scale和rotate,都會影響size大小。所以在這裡計算完成後,好在layout中進行正確的佈局。
layout中的程式碼如下:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// measure child layout
int cursorTop = top;
int mid = (right - left) >> 1;
for (int i = 0; i < imgList.size(); i++) {
final ImageData imageData = imgList.get(i);
// fix layout translate
imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
int translateTop = (int) (cursorTop + (imageData.orgRect.top -
imageData.drawRect.top));
int translateLeft = (int) (mid - imageData.drawRect.centerX());
imageData.matrix.postTranslate(translateLeft, translateTop);
imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
cursorTop = (int) imageData.drawRect.bottom;
}
}
複製程式碼
兩個方法中,要做到Matrix多效果疊加,切記要保留一個bitmap最原始的矩陣,然後再接下來的計算中需要用到當前尺寸的時候,使用Martix計算出臨時的尺寸對其進行計算。
兩個方法中,Bitmap被封裝到一個ImageData類裡面,進行物件化,這樣可以更好的管理Bitmap的處理和資料記錄。
ImageData如下:
public class ImageData {
public ImageData(Bitmap bitmap) {
this.bitmap = bitmap;
this.matrix = new Matrix();
orgRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
}
// 預設置0
float scale = 0f;
// 0點在3點鐘方向,達到垂直居中的效果,需要置為-90度
float rotateAngle = -90f;
RectF drawRect = new RectF();
RectF orgRect = new RectF();
Bitmap bitmap;
Matrix matrix;
private float distanceStub = 0f;
private float angleStub = 0f;
public Bitmap getBitmap() {
return bitmap;
}
public RectF getDrawRect() {
return drawRect;
}
public int getImgWidth() {
return bitmap.getWidth();
}
public int getImgHeight() {
return bitmap.getHeight();
}
public void layout(int l, int t, int r, int b) {
drawRect.set(l, t, r, b);
}
void reSize(int w, int h) {
int orgWidth = bitmap.getWidth();
int orgHeight = bitmap.getHeight();
// 計算縮放比例
float scaleWidth = ((float) w) / orgWidth;
float scaleHeight = ((float) h) / orgHeight;
scale = (scaleWidth + scaleHeight) * 0.5f;
matrix.postScale(scale, scale);
}
void clearMatrixCache() {
matrix.reset();
}
void setScale(float scale) {
this.scale = scale;
}
float getScale() {
return this.scale;
}
void setRotateAngle(float angle) {
this.rotateAngle = angle;
}
float getRotateAngle() {
return this.rotateAngle;
}
/**
* imageData的觸控處理事件
*
* @param e 觸控事件
*/
protected void onTouchEvent(MotionEvent e) {
// ...
}
private float getPointDistance(MotionEvent e) {
// ...
}
private float getPointAngle(MotionEvent e) {
// ...
}
}
複製程式碼
這裡面跟本文無關的方法都隱藏了,隨後會講到.
那麼我們來看看效果
使用方法,跟目錄gradle裡面新增:
repositories {
...
maven { url 'https://jitpack.io' }
}
複製程式碼
app.gradle中新增:
compile 'com.github.Kongdy:ImageStitching:v1.0.0'
複製程式碼
本文程式碼:github.com/Kongdy/Imag…
個人github地址:github.com/Kongdy
個人掘金主頁:juejin.im/user/595a64…
csdn主頁:blog.csdn.net/u014303003