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

先说明一下本自定义view的主要内容:
- 本效果的实现就是在光圈内六边形六个角上分别绘制六个光圈叶片
- 根据不同的光圈值计算出内六边形的大小,从而计算每个六边形的顶点的位置
- 设计叶片。也可以让美工MM提供,本方案是自己用代码画的。注意预留叶片之间的间隔距离以及每个叶片的角度为60°
- 定义颜色、间隔等自定义属性
- 上下滑动可以调节光圈大小
- 提供光圈值变动的监听接口
代码
可以在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; /**
* 上下滑动可以调节光圈大小;
* 调用setApertureChangedListener设置光圈值变动监听接口;
* 绘制的光圈最大直径将填满整个view
* @author willhua http://www.cnblogs.com/willhua/
*
*/
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);
} /**
* 设置光圈片的颜色
* @param bladeColor
*/
public void setBladeColor(int bladeColor) {
mBladeColor = bladeColor;
} /**
* 设置光圈背景色
*/
public void setBackgroundColor(int backgroundColor) {
mBackgroundColor = backgroundColor;
} /**
* 设置光圈片之间的间隔
* @param space
*/
public void setSpace(int space) {
mSpace = space;
} /**
* 设置光圈最大值
* @param maxApert
*/
public void setMaxApert(float maxApert) {
mMaxApert = maxApert;
} /**
* 设置光圈最小值
* @param mMinApert
*/
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);
}
} /**
* 设置光圈值变动的监听
* @param listener
*/
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>
实用控件分享:自定义逼真相机光圈View的更多相关文章
- WPF 4 DataGrid 控件(自定义样式篇)
原文:WPF 4 DataGrid 控件(自定义样式篇) 在<WPF 4 DataGrid 控件(基本功能篇)>中我们已经学习了DataGrid 的基本功能及使用方法.本篇将继续 ...
- WPF Calendar 日历控件 样式自定义
原文:WPF Calendar 日历控件 样式自定义 粗略的在代码上做了些注释 blend 生成出来的模版 有的时候 会生成 跟 vs ui界面不兼容的代码 会导致可视化设计界面 报错崩溃掉 但是确不 ...
- Vue input 控件: 通过自定义指令(directive)使用正则表达式限制input控件的输入
前言: 网站中的input输入框使用非常广泛,因业务场景不同需要对输入框做合法性校验或限制输入,比如电话号码.邮件.区号.身份证号等.input框的不合法内容主要有两种方式处理:1.用户输入内容后,通 ...
- 从零开始学ios开发(四):IOS控件(1),Image View、Text Field、Keyboard
长话短说,谢谢大家的关注,这篇写了好长时间,下面继续学习ios.我将用2到3篇的篇幅来学习iphone上的一些常用控件,包括Image View.Text Field.Keyboard.Slider等 ...
- 【转】C# 控件的自定义拖动、改变大小方法
在用VS的窗体设计器时,我们可以发现控件都是可以拖动的,并且还可以调整大小.怎么在自己的程序中可以使用上述功能呢? 下面的方法值得借鉴! using System; using System.Wind ...
- WPF自定义控件与样式(10)-进度控件ProcessBar自定义样
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Pro ...
- Android学习笔记(九)——布局和控件的自定义
//此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! View是 Android中一种最基本的 UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件 ...
- Flex 文本控件实现自定义复制粘贴
由于添加了自定义右键菜单,导致Textinput控件默认的右键复制粘贴功能被屏蔽了.最后通过JS脚本实现这个功能,参考代码如下 <?xml version="1.0" enc ...
- UIButton图片文字控件位置自定义(图片居右文字居左、图片居中文字居中、图片居左文字消失等)
在开发中经常会碰到需要对按钮中的图片文字位置做调整的需求.第一种方式是通过设置按钮中图片文字的偏移量.通过方法setTitleEdgeInsets和setImageEdgeInsets实现 代码如下: ...
随机推荐
- DOM-Document类型
Document类型 JavaScript通过Document类型表示文档.在浏览器中,document对象是HTMLDocument(继承自Document类型)的一个实例,document对象是w ...
- 关于SubSonic3.0插件使用SqlQuery或Select查询时产生的System.NullReferenceException异常修复
早上在编写执行用例时,突然爆异常System.NullReferenceException: 未将对象引用设置到对象的实例 执行代码:
- WebSocket 学习(三)--用nodejs搭建服务器
前面已经学习了WebSocket API,包括事件.方法和属性.详情:WebSocket(二)--API WebSocket是基于事件驱动,支持全双工通信.下面通过三个简单例子体验一下. 简单开始 ...
- springmvc4环境简单搭建和定时任务
之前复制粘贴创建了几个ssm的项目,然而回头让自己写的时候还是一头雾水,究其原因是spring的陌生.仅仅是写过几个helloworld而已.而且是照着写.我都不知道springmvc到底需要多少ja ...
- linux源码分析(一)
前置:这里使用的linux版本是4.8,x86体系. 其实linux的内核启动的入口文件还是非常好找的,init/main.c. static 和 extern 首先理解的是static和extern ...
- MySQL数据迁移到SQL Server
数据迁移的工具有很多,基本SSMA团队已经考虑到其他数据库到SQL Server迁移的需求了,所以已经开发了相关的迁移工具来支持. 此博客主要介绍MySQL到SQL Server数据迁移的工具:SQL ...
- 《C#并发编程经典实例》笔记
1.前言 2.开宗明义 3.开发原则和要点 (1)并发编程概述 (2)异步编程基础 (3)并行开发的基础 (4)测试技巧 (5)集合 (6)函数式OOP (7)同步 1.前言 最近趁着项目的一段平稳期 ...
- ubuntu 解决 “E: Problem with MergeList /var/lib/apt/lists/”错误
这种错误的意思:无法解析或打开软件包的列表或是状态文件. 出现的原因:无法解析或打开软件包列表多数情况是安装的软件与本身系统有一些冲突之类的问题,或者曾在更新软件源或下载软件的时候意外中断造成的. 解 ...
- [WCF编程]10.操作:单向操作
一.单向操作概述 WCF提供了单向操作,一旦客户端调用,WCF会生成一个请求,但没有相关的应答信息返回给客户端.所以,单向操作是不能有返回值,服务抛出的任何异常都不会传递给客户端. 理想情况下,一旦客 ...
- iOS阶段学习第33天笔记(自定义标签栏(UITabBar)介绍)
iOS学习(UI)知识点整理 一.自定义标签栏 1.方法一 单个创建标签栏 #import "AppDelegate.h" #import "SecondViewCont ...