美圖欣賞
0. 基本特性
- src: 設置圖片資源
- scaleType: 圖形設定顯示效果,是裁剪,縮放,拉伸等(下面的configureBounds方法重點關注);
- cropToPadding: 保證padding空間區域不會有圖形內容,會被裁剪掉;
- tint: 圖片上色處理.
- adjustViewBounds: 這個屬性對imageView的實際寬高有很大的影響,他的目的是保證控件寬高比例和圖形drawable比例相同.在測量過程時會通過該屬性以及drawble寬高比來構建控件的實際大小;
- 等等
1. 測量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
resolveUri();
int w;
int h;
//真是圖片drawable的寬高比記錄
float desiredAspect = 0.0f;
//控件的寬高是否支持動態的調整以適配圖片的寬高比;
boolean resizeWidth = false;
boolean resizeHeight = false;
//imageview的寬,高測量規格
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (mDrawable == null) {
// If no drawable, its intrinsic size is 0.
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {//如果有drawable;
w = mDrawableWidth;
h = mDrawableHeight;
if (w <= 0) w = 1;
if (h <= 0) h = 1;
//如果設置了adjustViewBounds="true"
if (mAdjustViewBounds) {
//寬或者高設定的不是match_parent可以
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
//記錄圖片draowable的寬高比
desiredAspect = (float) w / (float) h;
}
}
//控件的padding數值
int pleft = mPaddingLeft;
int pright = mPaddingRight;
int ptop = mPaddingTop;
int pbottom = mPaddingBottom;
int widthSize;
int heightSize;
//寬,高支持調整, 看看怎么調整?
if (resizeWidth || resizeHeight) {
/* If we get here, it means we want to resize to match the
drawables aspect ratio, and we have the freedom to change at
least one dimension.
*/
//從這里看出,如果iv控件如果沒有設定adjustViewBounds="true",maxWith,maxHeight是不會生效的.
//預先計算出iv的寬度,這里考慮了最大寬度;
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
//預先計算出iv的高度,這里考慮了最大高度;
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
//然而不止這樣,還要考慮drawable的寬高比,
if (desiredAspect != 0.0f) {
// 計算iv控件本身的寬,高比;
float actualAspect = (float)(widthSize - pleft - pright) /
(heightSize - ptop - pbottom);
//如果寬,高比是不同的時候,進入A1
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
//寬度是wrap的時候
if (resizeWidth) {
//根據drawable的寬高比,以及當前的iv的預測量高度來計算現在的寬度;
int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
pleft + pright;
//當高度固定的時候.會在newWidth和mMaxWidth中再得出一個最小的;
if (!resizeHeight && !mAdjustViewBoundsCompat) {
widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
}
//當高度固定的時候,該條件會滿足;若不固定即寬高是actualAspect和desiredAspect
//一般是相同的,也不會進入進入A1,固不會來到這里.
if (newWidth <= widthSize) {
widthSize = newWidth;
//等比計算iv寬度之后就不會再計算高了,不需要重復的計算了;
done = true;
}
}
// 如果寬沒有計算,即寬度是固定的,高度是wrap,那么就計算高的數值;原理和寬計算是一樣的.
if (!done && resizeHeight) {
int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
ptop + pbottom;
// Allow the height to outgrow its original estimate if width is fixed.
if (!resizeWidth && !mAdjustViewBoundsCompat) {
heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
heightMeasureSpec);
}
if (newHeight <= heightSize) {
heightSize = newHeight;
}
}
}
}
} else {
//當不存在寬,高調整的時候,如果設置了adjustViewBounds="false"
//計算方式就是根據drawable的寬,高,以及通常的測量方式來測量iv的大小了.
w += pleft + pright;
h += ptop + pbottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);
}
- 總結:測量有一些復雜,分為兩類.其一是普通的測量,根據padding, drawable的寬高,以及測量規格進行標準的圖形測量.其二,當圖形設置了adjustViewBounds=true這一個屬性,那么意圖是要讓圖形顯示的時候能夠保持原來的寬高比效果,必須要至少有一方的規格不是固定的,這樣就可以根據drawable的寬高比以及另外一方的fix尺寸進行比例縮放,進而讓圖形可以不變形地展示出來.測量就是實現了這樣的意圖.(注意: 圖形的最終是否變形,是否等比還是要看scaleType的屬性設定)
- 尺寸的修訂:
private int resolveAdjustedSize(int desiredSize, int maxSize,
int measureSpec) {
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
/* Parent says we can be as big as we want. Just don't be larger
than max size imposed on ourselves.
*/
result = Math.min(desiredSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be larger than
// the max size imposed on ourselves.
result = Math.min(Math.min(desiredSize, specSize), maxSize);
break;
case MeasureSpec.EXACTLY:
// No choice. Do what we are told.
result = specSize;
break;
}
return result;
}
- 根據圖形內容drawable寬/高, 以及限制的最大值來計算出圖形控件最終可以使用的寬與高.drawable最終填入到該圖形區域中.
2. 圖形效果設定, 主要是根據設定scaleType來縮放或者裁剪圖片.
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
int dwidth = mDrawableWidth;
int dheight = mDrawableHeight;
int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
int vheight = getHeight() - mPaddingTop - mPaddingBottom;
boolean fits = (dwidth < 0 || vwidth == dwidth) &&
(dheight < 0 || vheight == dheight);
//如果是color類型的drawable或者fitxy,那么drawable圖形會填滿控件,不管是否拉伸;
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
/* If the drawable has no intrinsic size, or we're told to
scaletofit, then we just fill our entire view.
*/
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
// 將drawable的寬高設地為原始的圖形寬高,后面的縮放都是在這個基礎上做的變換
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ScaleType.MATRIX == mScaleType) {
//設定了matrix模式,就用他們設定的;
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
} else if (fits) {
// 沒有縮放,也沒有偏移
mDrawMatrix = null;
} else if (ScaleType.CENTER == mScaleType) {
// 將Bitmap放在圖形控件的中心,不會去縮放drawable;不管大小如何
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f),
(int) ((vheight - dheight) * 0.5f + 0.5f));
} else if (ScaleType.CENTER_CROP == mScaleType) {
//CENTER_CROP,他會將圖形進行縮放,并且鋪滿整個iv空間居中顯示,至少一個方向保留所有內容
//圖形的寬高比大和iv控件寬高比進行比對: 圖形的寬高比大,就以iv的高進行縮放;圖形的寬高比小,
//就以iv的寬縮放.最后都圖形能鋪滿空間.
mDrawMatrix = mMatrix;
float scale;
float dx = 0, dy = 0;
//圖形的寬高比大,以iv的高為基準縮放,圖形的高內容保留,寬會丟失一部分圖形內容
if (dwidth * vheight > vwidth * dheight) {
scale = (float) vheight / (float) dheight;
dx = (vwidth - dwidth * scale) * 0.5f;
} else {//圖形的寬高比小,以iv的寬為基準縮放,高內容會有部分的圖形丟失
scale = (float) vwidth / (float) dwidth;
dy = (vheight - dheight * scale) * 0.5f;
}
//縮放
mDrawMatrix.setScale(scale, scale);
//平移drawable中心到iv中心;
mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
} else if (ScaleType.CENTER_INSIDE == mScaleType) {
//該模式會以完整展示為目的,圖形大于iv就縮放,小于就不做什么.
//fitcenter大于iv和center_inside一樣,小于iv大小會放大的,這個不會放大!
mDrawMatrix = mMatrix;
float scale;
float dx;
float dy;
//小了就不放大了
if (dwidth <= vwidth && dheight <= vheight) {
scale = 1.0f;
} else {
//大了就要去縮放一下了;
scale = Math.min((float) vwidth / (float) dwidth,
(float) vheight / (float) dheight);
}
dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f);
dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f);
//整合到matrix里面,后面組合到drawable中去;
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);
} else {
// ScaleType.FIT_START || ScaleType.FIT_CENTER || ScaleType.FIT_END,
//這三個模式的使用實現; 會等比縮放至填滿一邊,另外一邊一般會有空隙;
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
-
總結一下:
-
FIT_START || FIT_CENTER<默認的> || FIT_END
- 等比縮放,以至能夠填滿整個iv控件空間.
-
CENTER_INSIDE
- 以完整展示圖片為目的,drawable圖形大于iv就縮放,小于就不做什么.fit_center小于會放大填滿
-
CENTER
- 不會縮放,多大就是多大,圖形中心在iv控件中心
-
CENTER_CROP
- 縮放, 在一個方向頂邊,一個方向放大后裁剪,中心顯示
-
FIT_XY
- 填滿控件空間, 會出現拉扯變形現象.
-
MATRIX
- 可以自定義矩陣變化,平移,縮放,旋轉等等效果.
-
3. 圖片設定效果的生效與繪制
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//沒有drwable不繪制
if (mDrawable == null) {
return; // couldn't resolve the URI
}
//color,shape一開始沒有寬高,經過設定之后是有寬高的;
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
//如果沒有矩陣變化,沒有圖形設定就執行drawable的繪制了.
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
int saveCount = canvas.getSaveCount();
canvas.save();
//裁剪和偏移的內容;當mCropToPadding為true的時候,會將padding區域的圖形內容都切去.
// 不是padding區域本來就沒有圖形內容嗎?不是的,在有些場景下比如centerCrop類型.
//圖形在放大情況下會填充到padding區間的,這個時候如果進行mCropToPadding,才可以讓
//padding區域沒有圖形內容.
if (mCropToPadding) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
scrollX + mRight - mLeft - mPaddingRight,
scrollY + mBottom - mTop - mPaddingBottom);
}
//畫布偏移。
canvas.translate(mPaddingLeft, mPaddingTop);
//這里是重要的組合前面的圖形變化設定,組合到canvas中去
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
//drawbale來繪制圖形;
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
4. 實例說明:
-
假設是一張寬圖,寬/高 > 1. 當scaleType為centerCrop, padding=30dp, 我們實際的內容顯示是如何顯示的呢? 當paddingLeft=30dp的時候又是怎樣的效果呢?
//圖形會居中顯示,無邊距。 0. scaleType=centerCrop <ImageView android:id="@+id/iv" android:layout_width="100dp" android:layout_height="100dp" android:src="@drawable/reba" android:scaleType="centerCrop" /> //圖形上下有30內邊距,左右無內邊距,會居中顯示。 1. scaleType=centerCrop, padding=30dp; <ImageView android:id="@+id/iv" android:layout_width="100dp" android:layout_height="100dp" android:src="@drawable/reba" android:scaleType="centerCrop" android:padding="30dp" /> //圖形的中心在控件的中心右15dp位置,不會居中顯示,也沒有內邊距。 2. scaleType=centerCrop, paddingLeft=30dp; <ImageView android:id="@+id/iv" android:layout_width="100dp" android:layout_height="100dp" android:src="@drawable/reba" android:scaleType="centerCrop" android:paddingLeft="30dp" />
0模式下:根據
configureBounds
中centerCrop的計算規則圖形的寬高比大,就以iv的高進行縮放
, 所以高縮放到填滿高的空間,高不會裁剪,但是寬會有一些裁剪,然后根據dx = (vwidth - dwidth * scale) * 0.5f;
的矩陣中心偏移可以得出偏移量剛好是圖形中心和控件中心的差值,然后經過mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
偏移,就將圖形矩陣的中心和控件中心重合在一起了。所以圖形就居中顯示.
-
1模式下:同樣是和0一樣進行高縮放至填滿,因為有上下30的padding,所以只能縮放之后上下有30dp的空白,但是在這里經過矩陣偏移之后圖形的中心并不在控件的中心。假設經過縮放之后圖形的寬為200dp, 高為100dp:
1590912302158.png
-
該模式下, 當經過
dx = (vwidth - dwidth * scale) * 0.5f;
計算之后,dx = -80dp, 經過postTranslate偏移,也就是說圖形向中心會左移80, x中心為20dp了,并沒有與控件的中心(x = 50dp)對齊, 那么控件顯示中心是圖形的中心的右邊位置嗎?答案不是, 在的onDraw中還有一次畫布偏移,canvas.translate(mPaddingLeft, mPaddingTop);
這里會將圖形的位置往右移30dp,所以就會和控件中心水平重合。圖形會居中顯示的呢,同時paddingTop的向下偏移30dp在垂直方向上也能對齊控件中心。最終的圖形效果是這樣子, 紅色區域為圖形的畫布內容。1590913188325.png
- 在2模式下,只有paddingLeft=30dp, 圖形向右偏移30dp/2 = 15dp。來看下, 紅色為圖形drawable, 首先經過偏移之后, 圖形的中心在0點的右邊35dip之處。
然后經過canvas的右移paddingLeft, 30dp, 那么drawable的中心就在0點右側65dp處,所以圖形就在控件中心右側的15dp處而不是30dp處(控件中心在0點的右側50dp處)。
大概就這樣子吧,
5. 圖片的著色,濾色處理
-
ColorStateList: 這個是對圖片進行著色的顏色集,底層是通過ColorFilter來實現的.區別是他可以提供一組色彩處理.他會讀取tint設置的顏色屬性值. 他會和PorterDuff.Mode組合使用.
//讀取tint顏色到ColorStateList中 ColorStateList mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint); ....... //將顏色集設置到drawable中去;最后在onDraw方法中通過omDrawable.draw(canvas),生效圖形的著色處理. private void applyImageTint() { if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) { mDrawable = mDrawable.mutate(); if (mHasDrawableTint) { mDrawable.setTintList(mDrawableTintList); } if (mHasDrawableTintMode) { mDrawable.setTintMode(mDrawableTintMode); } } }
-
colorFilter: 也是著色,會覆蓋ColorStateList的效果.圖片的黑白處理就可以用他來實現.
//設置濾色效果 public void setColorFilter(ColorFilter cf) { if (mColorFilter != cf) { mColorFilter = cf; mHasColorFilter = true; mColorMod = true; //顏色 applyColorMod(); invalidate(); } } private void applyColorMod() { // Only mutate and apply when modifications have occurred. This should // not reset the mColorMod flag, since these filters need to be // re-applied if the Drawable is changed. if (mDrawable != null && mColorMod) { mDrawable = mDrawable.mutate(); if (mHasColorFilter) { //將顏色filter設定到drawable中去 mDrawable.setColorFilter(mColorFilter); } mDrawable.setXfermode(mXfermode); mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8); } } //實現圖片的灰白/黑白效果處理,一般是利用該策略來做的; ---- start ---- Drawable mDrawable = iv.getDrawable(); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(0);//灰白設定 ColorMatrixColorFilter cf = new ColorMatrixColorFilter(cm); mDrawable.setColorFilter(cf); ---- end ----