最近手機界開始流行雙攝像頭,大光圈功能也應用而生。所謂大光圈功能就是能夠對照片進行後期重新對焦,其實現的原理主要是對拍照期間獲取的深度圖片與對焦無窮遠的圖像通過演算法來實現重新對焦的效果。 在某雙攝手機的大光圈操作界面有個光圈的操作圖標,能夠模擬光圈調節時的真實效果,感覺還不錯,於是想著實現該效果。現 ...
最近手機界開始流行雙攝像頭,大光圈功能也應用而生。所謂大光圈功能就是能夠對照片進行後期重新對焦,其實現的原理主要是對拍照期間獲取的深度圖片與對焦無窮遠的圖像通過演算法來實現重新對焦的效果。
在某雙攝手機的大光圈操作界面有個光圈的操作圖標,能夠模擬光圈調節時的真實效果,感覺還不錯,於是想著實現該效果。現在把我的實現方法貢獻給大家,萬一你們公司也要做雙攝手機呢?( ̄┰ ̄*)
首先,百度一下光圈圖片,觀察觀察,就可以發現其關鍵在於計算不同的光圈值時各個光圈葉片的位置。為了計算簡便,我以六個直邊葉片的光圈效果為例來實現(其他形式,比如七個葉片,也就是位置計算稍微沒那麼方便;而一些圓弧的葉片,只要滿足葉片兩邊的圓弧半徑是一樣的就行。為什麼要圓弧半徑一樣呢?仔細觀察就可以發現,相鄰兩葉片之間要相互滑動,而且要保持一樣的契合距離,根據我曾今小學幾何科打滿分的經驗可以判斷出,等徑的圓弧是不錯滴,其他高級曲線能不能實現該效果,請問數學家( ̄┰ ̄*)!其他部分原理都是一樣的)。
製作效果圖:
先說明一下本自定義view的主要內容:
- 本效果的實現就是在光圈內六邊形六個角上分別繪製六個光圈葉片
- 根據不同的光圈值計算出內六邊形的大小,從而計算每個六邊形的頂點的位置
- 設計葉片。也可以讓美工MM提供,本方案是自己用代碼畫的。註意預留葉片之間的間隔距離以及每個葉片的角度為60°
- 定義顏色、間隔等自定義屬性
- 上下滑動可以調節光圈大小
- 提供光圈值變動的監聽介面
代碼
可以在GitHub上下載:https://github.com/willhua/CameraAperture.git
1 package com.example.cameraaperture; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.graphics.Bitmap; 6 import android.graphics.Bitmap.Config; 7 import android.graphics.Canvas; 8 import android.graphics.Paint; 9 import android.graphics.Path; 10 import android.graphics.PointF; 11 import android.util.AttributeSet; 12 import android.util.Log; 13 import android.view.MotionEvent; 14 import android.view.View; 15 16 /** 17 * 上下滑動可以調節光圈大小; 18 * 調用setApertureChangedListener設置光圈值變動監聽介面; 19 * 繪製的光圈最大直徑將填滿整個view 20 * @author willhua http://www.cnblogs.com/willhua/ 21 * 22 */ 23 public class ApertureView extends View { 24 25 public interface ApertureChanged { 26 public void onApertureChanged(float newapert); 27 } 28 29 private static final float ROTATE_ANGLE = 30; 30 private static final String TAG = "ApertureView"; 31 private static final float COS_30 = 0.866025f; 32 private static final int WIDTH = 100; // 當設置為wrap_content時測量大小 33 private static final int HEIGHT = 100; 34 private int mCircleRadius; 35 private int mBladeColor; 36 private int mBackgroundColor; 37 private int mSpace; 38 private float mMaxApert = 1; 39 private float mMinApert = 0.2f; 40 private float mCurrentApert = 0.5f; 41 42 //利用PointF而不是Point可以減少計算誤差,以免葉片之間間隔由於計算誤差而不均衡 43 private PointF[] mPoints = new PointF[6]; 44 private Bitmap mBlade; 45 private Paint mPaint; 46 private Path mPath; 47 private ApertureChanged mApertureChanged; 48 49 private float mPrevX; 50 private float mPrevY; 51 52 public ApertureView(Context context, AttributeSet attrs) { 53 super(context, attrs); 54 init(context, attrs); 55 } 56 57 private void init(Context context, AttributeSet attrs) { 58 //讀取自定義佈局屬性 59 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ApertureView); 60 mSpace = (int)array.getDimension(R.styleable.ApertureView_blade_space, 5); 61 mBladeColor = array.getColor(R.styleable.ApertureView_blade_color, 0xFF000000); 62 mBackgroundColor = array.getColor(R.styleable.ApertureView_background_color, 0xFFFFFFFF); 63 array.recycle(); 64 mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); 65 mPaint.setAntiAlias(true); 66 for (int i = 0; i < 6; i++) { 67 mPoints[i] = new PointF(); 68 } 69 } 70 71 @Override 72 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 73 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 74 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 75 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 76 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 77 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 78 int paddX = getPaddingLeft() + getPaddingRight(); 79 int paddY = getPaddingTop() + getPaddingBottom(); 80 //光圈的大小要考慮減去view的padding值 81 mCircleRadius = widthSpecSize - paddX < heightSpecSize - paddY ? (widthSpecSize - paddX) / 2 82 : (heightSpecSize - paddY) / 2; 83 //對佈局參數為wrap_content時的處理 84 if (widthSpecMode == MeasureSpec.AT_MOST 85 && heightSpecMode == MeasureSpec.AT_MOST) { 86 setMeasuredDimension(WIDTH, HEIGHT); 87 mCircleRadius = (WIDTH - paddX) / 2; 88 } else if (widthSpecMode == MeasureSpec.AT_MOST) { 89 setMeasuredDimension(WIDTH, heightSpecSize); 90 mCircleRadius = WIDTH - paddX < heightSpecSize - paddY ? (WIDTH - paddX) / 2 91 : (heightSpecSize - paddY) / 2; 92 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 93 setMeasuredDimension(widthSpecSize, HEIGHT); 94 mCircleRadius = widthSpecSize - paddX < HEIGHT - paddY ? (widthSpecSize - paddX) / 2 95 : (HEIGHT - paddY) / 2; 96 } 97 if (mCircleRadius < 1) { 98 mCircleRadius = 1; 99 } 100 //measure之後才能知道所需要繪製的光圈大小 101 mPath = new Path(); 102 mPath.addCircle(0, 0, mCircleRadius, Path.Direction.CW); 103 createBlade(); 104 } 105 106 @Override 107 public void onDraw(Canvas canvas) { 108 canvas.save(); 109 calculatePoints(); 110 //先把canbvas平移到view的中間 111 canvas.translate(getWidth() / 2, getHeight() / 2); 112 //讓光圈的葉片整體旋轉,更加貼合實際 113 canvas.rotate(ROTATE_ANGLE * (mCurrentApert - mMinApert) / (mMaxApert - mMinApert)); 114 canvas.clipPath(mPath); 115 canvas.drawColor(mBackgroundColor); 116 117 for (int i = 0; i < 6; i++) { 118 canvas.save(); 119 canvas.translate(mPoints[i].x, mPoints[i].y); 120 canvas.rotate(-i * 60); 121 canvas.drawBitmap(mBlade, 0, 0, mPaint); 122 canvas.restore(); 123 } 124 canvas.restore(); 125 } 126 127 @Override 128 public boolean onTouchEvent(MotionEvent event) { 129 if (event.getPointerCount() > 1) { 130 return false; 131 } 132 switch (event.getAction()) { 133 case MotionEvent.ACTION_DOWN: 134 mPrevX = event.getX(); 135 mPrevY = event.getY(); 136 break; 137 case MotionEvent.ACTION_MOVE: 138 float diffx = Math.abs((event.getX() - mPrevX)); 139 float diffy = Math.abs((event.getY() - mPrevY)); 140 if (diffy > diffx) { // 豎直方向的滑動 141 float diff = (float) Math.sqrt(diffx * diffx + diffy * diffy) 142 / mCircleRadius * mMaxApert; 143 if (event.getY() > mPrevY) { //判斷方向 144 setCurrentApert(mCurrentApert - diff); 145 } else { 146 setCurrentApert(mCurrentApert + diff); 147 } 148 mPrevX = event.getX(); 149 mPrevY = event.getY(); 150 } 151 break; 152 default: 153 break; 154 } 155 return true; 156 } 157 158 private void calculatePoints() { 159 if (mCircleRadius - mSpace <= 0) { 160 Log.e(TAG, "the size of view is too small and Space is too large"); 161 return; 162 } 163 //mCircleRadius - mSpace可以保證內嵌六邊形在光圈內 164 float curRadius = mCurrentApert / mMaxApert * (mCircleRadius - mSpace); 165 //利用對稱關係,減少計算 166 mPoints[0].x = curRadius / 2; 167 mPoints[0].y = -curRadius * COS_30; 168 mPoints[1].x = -mPoints[0].x; 169 mPoints[1].y = mPoints[0].y; 170 mPoints[2].x = -curRadius; 171 mPoints[2].y = 0; 172 mPoints[3].x = mPoints[1].x; 173 mPoints[3].y = -mPoints[1].y; 174 mPoints[4].x = -mPoints[3].x; 175 mPoints[4].y = mPoints[3].y; 176 mPoints[5].x = curRadius; 177 mPoints[5].y = 0; 178 } 179 180 //創建光圈葉片,讓美工MM提供更好 181 private void createBlade() { 182 mBlade = Bitmap.createBitmap(mCircleRadius, 183 (int) (mCircleRadius * 2 * COS_30), Config.ARGB_8888); 184 Path path = new Path(); 185 Canvas canvas = new Canvas(mBlade); 186 path.moveTo(mSpace / 2 / COS_30, mSpace); 187 path.lineTo(mBlade.getWidth(), mBlade.getHeight()); 188 path.lineTo(mBlade.getWidth(), mSpace); 189 path.close(); 190 canvas.clipPath(path); 191 canvas.drawColor(mBladeColor); 192 } 193 194 /** 195 * 設置光圈片的顏色 196 * @param bladeColor 197 */ 198 public void setBladeColor(int bladeColor) { 199 mBladeColor = bladeColor; 200 } 201 202 /** 203 * 設置光圈背景色 204 */ 205 public void setBackgroundColor(int backgroundColor) { 206 mBackgroundColor = backgroundColor; 207 } 208 209 /** 210 * 設置光圈片之間的間隔 211 * @param space 212 */ 213 public void setSpace(int space) { 214 mSpace = space; 215 } 216 217 /** 218 * 設置光圈最大值 219 * @param maxApert 220 */ 221 public void setMaxApert(float maxApert) { 222 mMaxApert = maxApert; 223 } 224 225 /** 226 * 設置光圈最小值 227 * @param mMinApert 228 */ 229 public void setMinApert(float mMinApert) { 230 this.mMinApert = mMinApert; 231 } 232 233 public float getCurrentApert() { 234 return mCurrentApert; 235 } 236 237 public void setCurrentApert(float currentApert) { 238 if (currentApert > mMaxApert) { 239 currentApert = mMaxApert; 240 } 241 if (currentApert < mMinApert) { 242 currentApert = mMinApert; 243 } 244 if (mCurrentApert == currentApert) { 245 return; 246 } 247 mCurrentApert = currentApert; 248 invalidate(); 249 if (mApertureChanged != null) { 250 mApertureChanged.onApertureChanged(currentApert); 251 } 252 } 253 254 /** 255 * 設置光圈值變動的監聽 256 * @param listener 257 */ 258 public void setApertureChangedListener(ApertureChanged listener) { 259 mApertureChanged = listener; 260 } 261 }
自定義屬性的xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ApertureView"> <attr name="blade_color" format="color" /> <attr name="background_color" format="color" /> <attr name="blade_space" format="dimension" /> </declare-styleable> </resources>