实用控件分享:自定义逼真相机光圈View

最近手机界开始流行双摄像头,大光圈功能也应用而生。所谓大光圈功能就是能够对照片进行后期重新对焦,其实现的原理主要是对拍照期间获取的深度图片与对焦无穷远的图像通过算法来实现重新对焦的效果。

在某双摄手机的大光圈操作界面有个光圈的操作图标,能够模拟光圈调节时的真实效果,感觉还不错,于是想着实现该效果。现在把我的实现方法贡献给大家,万一你们公司也要做双摄手机呢?( ̄┰ ̄*)

首先,百度一下光圈图片,观察观察,就可以发现其关键在于计算不同的光圈值时各个光圈叶片的位置。为了计算简便,我以六个直边叶片的光圈效果为例来实现(其他形式,比如七个叶片,也就是位置计算稍微没那么方便;而一些圆弧的叶片,只要满足叶片两边的圆弧半径是一样的就行。为什么要圆弧半径一样呢?仔细观察就可以发现,相邻两叶片之间要相互滑动,而且要保持一样的契合距离,根据我曾今小学几何科打满分的经验可以判断出,等径的圆弧是不错滴,其他高级曲线能不能实现该效果,请问数学家( ̄┰ ̄*)!其他部分原理都是一样的)。 

制作效果图

 

先说明一下本自定义view的主要内容:

  1. 本效果的实现就是在光圈内六边形六个角上分别绘制六个光圈叶片
  2. 根据不同的光圈值计算出内六边形的大小,从而计算每个六边形的顶点的位置
  3. 设计叶片。也可以让美工MM提供,本方案是自己用代码画的。注意预留叶片之间的间隔距离以及每个叶片的角度为60°
  4. 定义颜色、间隔等自定义属性
  5. 上下滑动可以调节光圈大小
  6. 提供光圈值变动的监听接口

代码

可以在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>

 

posted @ 2016-08-11 23:27  willhua  阅读(1484)  评论(1编辑  收藏  举报