你的位置:首页 > 操作系统

[操作系统]实用控件分享:自定义逼真相机光圈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   @Override107   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   @Override128   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 bladeColor197   */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 space212   */213   public void setSpace(int space) {214     mSpace = space;215   }216 217   /**218    * 设置光圈最大值219    * @param maxApert220   */221   public void setMaxApert(float maxApert) {222     mMaxApert = maxApert;223   }224 225   /**226    * 设置光圈最小值227    * @param mMinApert228   */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 listener257   */258   public void setApertureChangedListener(ApertureChanged listener) {259     mApertureChanged = listener;260   }261 }

自定义属性的

<??><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>