实用控件分享:自定义逼真相机光圈View
最近手机界开始流行双摄像头,大光圈功能也应用而生。所谓大光圈功能就是能够对照片进行后期重新对焦,其实现的原理主要是对拍照期间获取的深度图片与对焦无穷远的图像通过算法来实现重新对焦的效果。
在某双摄手机的大光圈操作界面有个光圈的操作图标,能够模拟光圈调节时的真实效果,感觉还不错,于是想着实现该效果。现在把我的实现方法贡献给大家,万一你们公司也要做双摄手机呢?( ̄┰ ̄*)
首先,百度一下光圈图片,观察观察,就可以发现其关键在于计算不同的光圈值时各个光圈叶片的位置。为了计算简便,我以六个直边叶片的光圈效果为例来实现(其他形式,比如七个叶片,也就是位置计算稍微没那么方便;而一些圆弧的叶片,只要满足叶片两边的圆弧半径是一样的就行。为什么要圆弧半径一样呢?仔细观察就可以发现,相邻两叶片之间要相互滑动,而且要保持一样的契合距离,根据我曾今小学几何科打满分的经验可以判断出,等径的圆弧是不错滴,其他高级曲线能不能实现该效果,请问数学家( ̄┰ ̄*)!其他部分原理都是一样的)。
制作效果图:
先说明一下本自定义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>
/************************* Stay hungry, Stay foolish. @willhua ************************/