Android基于PhotoView實(shí)現(xiàn)的頭像/圓形裁剪控件
前言
常見的圖片裁剪有兩種,一種是圖片固定,裁剪框移動放縮來確定裁剪區(qū)域,早期見的比較多,缺點(diǎn)在于不能直接預(yù)覽裁剪后的效果;還有一種現(xiàn)在比較普遍了,就是裁剪框固定,直接拖動縮放圖片,便于預(yù)覽裁剪結(jié)果。
我做的這個控件屬于后者。一般來說,做圖片裁剪的思路無外乎是先監(jiān)聽手勢,獲取坐標(biāo),再對圖片變形,最后確定裁剪區(qū)域的坐標(biāo)對位圖進(jìn)行裁剪,最后保存圖片到本地。我嘛還是個技術(shù)小白,一想到要監(jiān)控手勢這些就頭疼,碰巧項(xiàng)目之前為了做查看大圖而引入了大名鼎鼎的第三方圖片查看控件——PhotoView(使用步驟參考這篇文章:Android PhotoView使用步驟實(shí)例詳解)。于是轉(zhuǎn)念一想,能不能把到圖片變形為止的前幾步交給PhotoView來搞定,我只要負(fù)責(zé)確定確定裁剪區(qū)域后面這幾步呢。后來掉了好幾個坑導(dǎo)致偷懶也沒輕松多少其實(shí)ε=(´ο`*)))唉~


先簡要介紹一下設(shè)計(jì)思路,如上圖所示,主要分為兩部分,上層是遮罩(也可以理解為是裁剪框),用于預(yù)覽裁剪后的效果;下層是PhotoView,這里多包了一層改為正方形顯示。
下面是遮罩的代碼,比較簡單,這里就不贅述了。
/**
* Created by MandyLu on 2018/7/14.
* 圓形裁剪框
*/
public class CircleCropView extends View {
public final int CIRCLE_MARGIN = 50;
public CircleCropView(Context context) {
super(context);
}
public CircleCropView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CircleCropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CircleCropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
Path path = new Path();
Rect viewDrawingRect = new Rect();
getDrawingRect(viewDrawingRect);
float radius = viewDrawingRect.width() / 2 - CIRCLE_MARGIN;
path.addCircle(viewDrawingRect.left + radius + CIRCLE_MARGIN,
viewDrawingRect.top + radius + CIRCLE_MARGIN, radius, Path.Direction.CW);
Paint outsidePaint = new Paint();
outsidePaint.setAntiAlias(true);
outsidePaint.setARGB(151, 0, 0, 0);
canvas.clipPath(path, Region.Op.DIFFERENCE);
canvas.drawRect(viewDrawingRect, outsidePaint);
canvas.restore();
}
}
SquarePhotoView只是在PhotoView的基礎(chǔ)上改了長寬,重寫一下onMeasure方法即可:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
那么現(xiàn)在最關(guān)鍵的一步,就是從PhotoView獲取當(dāng)前圖片顯示區(qū)域的Drawable或Bitmap了。粗略看了一下PhotoView的函數(shù),并沒有找到能用的(囧)。解決第一個坑的笨辦法就是,自己動手豐衣足食——直接拿原圖的bitmap,然后問PhotoView要當(dāng)前圖片的變形矩陣,自個兒通過矩陣一步步變形拿到對應(yīng)的位圖。
思路其實(shí)是沒問題的,然而第二個坑又出現(xiàn)了(囧)。這里的變形矩陣,我最早百度的結(jié)果是getSuppMatrix,源碼我沒有細(xì)看,但掉坑的過程中據(jù)我觀察,猜測應(yīng)該是對應(yīng)最新一次的手勢變形結(jié)果(不確定= =,也可能是其他坑綜合導(dǎo)致的錯誤結(jié)果)??傊詈笪也榱艘粫创a,最終確定用的是getDisplayMatrix。
緊接著是第三個坑,坑多了就習(xí)慣了。矩陣中的XY位移量,我起初以為是顯示區(qū)域中心相對于原圖中心的位移,即如果僅有縮放操作的話,位移應(yīng)該為0。但實(shí)際通過特殊位置(例如取四個頂點(diǎn))的裁剪結(jié)果來看,這里的XY位移量實(shí)際最后顯示區(qū)域左上角的點(diǎn)相對原點(diǎn)(即原圖左上角)的位移,簡單點(diǎn)說,可以把位移量作為最終顯示區(qū)域左上角的坐標(biāo)。
然后我就迎來了第四個坑(🙂)。這個坑現(xiàn)在回頭看其實(shí)是很簡單不應(yīng)該栽進(jìn)去的,然而當(dāng)時還沒想通的時候確實(shí)很慌(唉)。這個坑的問題就出在,Matrix里的值是基于手勢的,也就是說,是基于屏幕像素(換句話說,是基于實(shí)際顯示的圖片)的。而對位圖進(jìn)行裁剪時,是基于原圖像素的。那么這里還存在一個為了正常顯示而導(dǎo)致的縮放比例的問題,例如原圖是3000x4000,由于屏幕分辨率是1080*1920,那么實(shí)際顯示時,圖片是縮小了的,這個比例是9/25。所以在裁剪的過程中,需要把位移量再放大25/9倍進(jìn)行還原。
下面是裁剪部分的關(guān)鍵代碼(最后偷了一下懶,沒有裁圓形,只是用CIrcleImageView顯示):
fun cropImage(){
var degree = ImageUtils.readPictureDegree(imagePath)
var bitmap = ImageUtils.getRotatedBitmap(BitmapFactory.decodeFile(imagePath),degree)
var width: Int = 0
var startX: Int = 0
var startY: Int = 0
if (bitmap.width < bitmap.height){
startY = (bitmap.height - bitmap.width) / 2
width = bitmap.width
}else{
startX = (bitmap.width - bitmap.height) / 2
width = bitmap.height
}
var matrix = Matrix()
photo_preview.getDisplayMatrix(matrix)//獲取變形矩陣,直接取scaleX或translationX沒用
var values = FloatArray(9, {0.0f})
matrix.getValues(values)
var expWidth = Math.round(bitmap.width * values[0])//縮放x
var expHeight = Math.round(bitmap.height * values[4])//縮放y
var bitmap1 = Bitmap.createScaledBitmap(bitmap, expWidth, expHeight, false)
val ratio = width * 1.0f / photo_preview.width
startX = Math.round(startX * values[0] - values[2] * ratio)
startY = Math.round(startY * values[4] - values[5] * ratio)
var bitmap2 = Bitmap.createBitmap(bitmap1, startX, startY, width, width, null, false)
saveImage(bitmap2)
}
這里還有幾個小坑需要解釋一下:
讀取bitmap時需要注意一下角度。這個是我在裁剪本地圖片和網(wǎng)絡(luò)圖片的時候發(fā)現(xiàn)的,有些是正的有些就是轉(zhuǎn)了90度。每個手機(jī)也不一定一樣,所以保險起見,需要從圖片的EXIF信息里面獲取需要旋轉(zhuǎn)的角度,然后再進(jìn)一步處理。
我這里因?yàn)樽罱K顯示的是正方形,而且選的scaleType是centerCrop。所以默認(rèn)就是顯示中間的那一塊。所以裁減時的原點(diǎn)也需要從正方形的左上角開始。這里是計(jì)算兩種情況下的原點(diǎn)坐標(biāo):
var startX: Int = 0
var startY: Int = 0
if (bitmap.width < bitmap.height){
startY = (bitmap.height - bitmap.width) / 2
width = bitmap.width
}else{
startX = (bitmap.width - bitmap.height) / 2
width = bitmap.height
}
縮放操作后,原點(diǎn)坐標(biāo)也隨之變換,乘以相應(yīng)的縮放比例,再根據(jù)相應(yīng)的位移量確定裁剪區(qū)域的位置。
原本想直接使用Matrix進(jìn)行變形,失敗(原因不明)。查看別的裁剪控件源碼,決定使用createScaledBitmap來進(jìn)行方法操作。
最后還是要檢討一下:耍了小聰明想抄點(diǎn)近路,結(jié)果因?yàn)椴皇煜ぴ创a,遇到坑的時候也只能當(dāng)成黑盒;只能通過不斷實(shí)驗(yàn)來猜測問題所在,反倒是花了更多時間,得不償失了。以后有時間的時候,還是應(yīng)該仔細(xì)研究源碼,踏踏實(shí)實(shí)從原理出發(fā)解決問題(* ̄︶ ̄)~
最后,感謝幾位博主的無私分享,特此鳴謝~
>>>Android Bitmap 常見的幾個操作:縮放,裁剪,旋轉(zhuǎn),偏移
>>>Android ImageCropper 矩形 圓形 裁剪框
>>>Android裁剪圖片為圓形圖片的實(shí)現(xiàn)原理與代碼
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
淺談Android應(yīng)用的內(nèi)存優(yōu)化及Handler的內(nèi)存泄漏問題
這篇文章主要介紹了Android應(yīng)用的內(nèi)存優(yōu)化及Handler的內(nèi)存泄漏問題,文中對Activity無法被回收而造成的內(nèi)存泄漏給出了通常的解決方案,需要的朋友可以參考下2016-02-02
詳解Activity之singletast啟動模式及如何使用intent傳值
在一個新棧中創(chuàng)建該Activity實(shí)例,并讓多個應(yīng)用共享改棧中的該Activity實(shí)例。一旦改模式的Activity的實(shí)例存在于某個棧中,任何應(yīng)用再激活改Activity時都會重用該棧中的實(shí)例,其效果相當(dāng)于多個應(yīng)用程序共享一個應(yīng)用,不管誰激活該Activity都會進(jìn)入同一個應(yīng)用中2015-11-11
ViewPager打造輪播圖Banner/引導(dǎo)頁Guide
這篇文章主要為大家詳細(xì)介紹了ViewPager打造輪播圖Banner和引導(dǎo)頁Guide,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
教你一步步實(shí)現(xiàn)Android微信自動搶紅包
自從微信添加搶紅包的功能,微信的電商之旅算是正式開始正式火爆起來。但是作為Android開發(fā)者來說,我們首先考慮的是如何實(shí)現(xiàn)Android微信自動搶紅包呢,下面我們來一起看看吧。2016-08-08
android基礎(chǔ)教程之夜間模式實(shí)現(xiàn)示例
這篇文章主要介紹了android的夜間模式實(shí)現(xiàn)示例,需要的朋友可以參考下2014-02-02
詳解Android Webview加載網(wǎng)頁時發(fā)送HTTP頭信息
這篇文章主要介紹了詳解Android Webview加載網(wǎng)頁時發(fā)送HTTP頭信息的相關(guān)資料,需要的朋友可以參考下2017-05-05

