最近手机界开始流行双摄像头,大光圈功能也应用而生。所谓大光圈功能就是能够对照片进行后期重新对焦,其实现的原理主要是对拍照期间获取的深度图片与对焦无穷远的图像通过算法来实现重新对焦的效果。
在某双摄手机的大光圈操作界面有个光圈的操作图标,能够模拟光圈调节时的真实效果,感觉还不错,于是想着实现该效果。现在把我的实现方法贡献给大家,万一你们公司也要做双摄手机呢?( ̄┰ ̄*)
首先,百度一下光圈图片,观察观察,就可以发现其关键在于计算不同的光圈值时各个光圈叶片的位置。为了计算简便,我以六个直边叶片的光圈效果为例来实现(其他形式,比如七个叶片,也就是位置计算稍微没那么方便;而一些圆弧的叶片,只要满足叶片两边的圆弧半径是一样的就行。为什么要圆弧半径一样呢?仔细观察就可以发现,相邻两叶片之间要相互滑动,而且要保持一样的契合距离,根据我曾今小学几何科打满分的经验可以判断出,等径的圆弧是不错滴,其他高级曲线能不能实现该效果,请问数学家( ̄┰ ̄*)!其他部分原理都是一样的)。
制作效果图:
先说明一下本自定义view的主要内容:
1.本效果的实现就是在光圈内六边形六个角上分别绘制六个光圈叶片
2.根据不同的光圈值计算出内六边形的大小,从而计算每个六边形的顶点的位置
3.设计叶片。也可以让美工MM提供,本方案是自己用代码画的。注意预留叶片之间的间隔距离以及每个叶片的角度为60°
4.定义颜色、间隔等自定义属性
5.上下滑动可以调节光圈大小
6.提供光圈值变动的监听接口
代码
可以在GitHub上下载:https://github.com/willhua/CameraAperture.git
package com.example.cameraaperture;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class ApertureView extends View {
public interface ApertureChanged {
public void onApertureChanged(float newapert);
}
private static final float ROTATE_ANGLE = 30;
private static final String TAG = "ApertureView";
private static final float COS_30 = 0.866025f;
private static final int WIDTH = 100; // 当设置为wrap_content时测量大小
private static final int HEIGHT = 100;
private int mCircleRadius;
private int mBladeColor;
private int mBackgroundColor;
private int mSpace;
private float mMaxApert = 1;
private float mMinApert = 0.2f;
private float mCurrentApert = 0.5f;
//利用PointF而不是Point可以减少计算误差,以免叶片之间间隔由于计算误差而不均衡
private PointF[] mPoints = new PointF[6];
private Bitmap mBlade;
private Paint mPaint;
private Path mPath;
private ApertureChanged mApertureChanged;
private float mPrevX;
private float mPrevY;
public ApertureView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
//读取自定义布局属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ApertureView);
mSpace = (int)array.getDimension(R.styleable.ApertureView_blade_space, 5);
mBladeColor = array.getColor(R.styleable.ApertureView_blade_color, 0xFF000000);
mBackgroundColor = array.getColor(R.styleable.ApertureView_background_color, 0xFFFFFFFF);
array.recycle();
mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
mPaint.setAntiAlias(true);
for (int i = 0; i < 6; i++) {
mPoints[i] = new PointF();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int paddX = getPaddingLeft() + getPaddingRight();
int paddY = getPaddingTop() + getPaddingBottom();
//光圈的大小要考虑减去view的padding值
mCircleRadius = widthSpecSize - paddX < heightSpecSize - paddY ? (widthSpecSize - paddX) / 2
: (heightSpecSize - paddY) / 2;
//对布局参数为wrap_content时的处理
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WIDTH, HEIGHT);
mCircleRadius = (WIDTH - paddX) / 2;
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WIDTH, heightSpecSize);
mCircleRadius = WIDTH - paddX < heightSpecSize - paddY ? (WIDTH - paddX) / 2
: (heightSpecSize - paddY) / 2;
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, HEIGHT);
mCircleRadius = widthSpecSize - paddX < HEIGHT - paddY ? (widthSpecSize - paddX) / 2
: (HEIGHT - paddY) / 2;
}
if (mCircleRadius < 1) {
mCircleRadius = 1;
}
//measure之后才能知道所需要绘制的光圈大小
mPath = new Path();
mPath.addCircle(0, 0, mCircleRadius, Path.Direction.CW);
createBlade();
}
@Override
public void onDraw(Canvas canvas) {
canvas.save();
calculatePoints();
//先把canbvas平移到view的中间
canvas.translate(getWidth() / 2, getHeight() / 2);
//让光圈的叶片整体旋转,更加贴合实际
canvas.rotate(ROTATE_ANGLE * (mCurrentApert - mMinApert) / (mMaxApert - mMinApert));
canvas.clipPath(mPath);
canvas.drawColor(mBackgroundColor);
for (int i = 0; i < 6; i++) {
canvas.save();
canvas.translate(mPoints[i].x, mPoints[i].y);
canvas.rotate(-i * 60);
canvas.drawBitmap(mBlade, 0, 0, mPaint);
canvas.restore();
}
canvas.restore();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getPointerCount() > 1) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = event.getX();
mPrevY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float diffx = Math.abs((event.getX() - mPrevX));
float diffy = Math.abs((event.getY() - mPrevY));
if (diffy > diffx) { // 竖直方向的滑动
float diff = (float) Math.sqrt(diffx * diffx + diffy * diffy)
/ mCircleRadius * mMaxApert;
if (event.getY() > mPrevY) { //判断方向
setCurrentApert(mCurrentApert - diff);
} else {
setCurrentApert(mCurrentApert + diff);
}
mPrevX = event.getX();
mPrevY = event.getY();
}
break;
default:
break;
}
return true;
}
private void calculatePoints() {
if (mCircleRadius - mSpace <= 0) {
Log.e(TAG, "the size of view is too small and Space is too large");
return;
}
//mCircleRadius - mSpace可以保证内嵌六边形在光圈内
float curRadius = mCurrentApert / mMaxApert * (mCircleRadius - mSpace);
//利用对称关系,减少计算
mPoints[0].x = curRadius / 2;
mPoints[0].y = -curRadius * COS_30;
mPoints[1].x = -mPoints[0].x;
mPoints[1].y = mPoints[0].y;
mPoints[2].x = -curRadius;
mPoints[2].y = 0;
mPoints[3].x = mPoints[1].x;
mPoints[3].y = -mPoints[1].y;
mPoints[4].x = -mPoints[3].x;
mPoints[4].y = mPoints[3].y;
mPoints[5].x = curRadius;
mPoints[5].y = 0;
}
//创建光圈叶片,让美工MM提供更好
private void createBlade() {
mBlade = Bitmap.createBitmap(mCircleRadius,
(int) (mCircleRadius * 2 * COS_30), Config.ARGB_8888);
Path path = new Path();
Canvas canvas = new Canvas(mBlade);
path.moveTo(mSpace / 2 / COS_30, mSpace);
path.lineTo(mBlade.getWidth(), mBlade.getHeight());
path.lineTo(mBlade.getWidth(), mSpace);
path.close();
canvas.clipPath(path);
canvas.drawColor(mBladeColor);
}
public void setBladeColor(int bladeColor) {
mBladeColor = bladeColor;
}
public void setBackgroundColor(int backgroundColor) {
mBackgroundColor = backgroundColor;
}
public void setSpace(int space) {
mSpace = space;
}
public void setMaxApert(float maxApert) {
mMaxApert = maxApert;
}
public void setMinApert(float mMinApert) {
this.mMinApert = mMinApert;
}
public float getCurrentApert() {
return mCurrentApert;
}
public void setCurrentApert(float currentApert) {
if (currentApert > mMaxApert) {
currentApert = mMaxApert;
}
if (currentApert < mMinApert) {
currentApert = mMinApert;
}
if (mCurrentApert == currentApert) {
return;
}
mCurrentApert = currentApert;
invalidate();
if (mApertureChanged != null) {
mApertureChanged.onApertureChanged(currentApert);
}
}
public void setApertureChangedListener(ApertureChanged listener) {
mApertureChanged = listener;
}
}
自定义属性的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>
您可能感兴趣的文章:Android 用 camera2 API 自定义相机Android中关于自定义相机预览界面拉伸问题Android自定义相机实现定时拍照功能Android自定义组件获取本地图片和相机拍照图片Android自定义相机实现自动对焦和手动对焦Android自定义相机界面的实现代码Android自定义照相机Camera出现黑屏的解决方法Android自定义照相机详解Android自定义照相机倒计时拍照Android 自定义相机及分析源码