Android实用控件自定义逼真相机光圈View

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

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

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

制作效果图:

先说明一下本自定义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;

/**
 * 上下滑动可以调节光圈大小;
 * 调用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>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android自定义照相机详解

    几乎每个APP都会用的相机功能,下面小编把内容整理分享到我们平台,供大家参考,感兴趣的朋友一起学习吧! 启动相机的两种方式 1.直接启动系统相机 <code class="hljs avrasm"> Intent intent = new Intent(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent);</code> 或者指定返回图片的名称mCurrentPho

  • Android 用 camera2 API 自定义相机

    前言 笔者因为项目需要自定义相机,所以了解了一下 Android 关于 camera 这块的 API.Android SDK 21(LOLLIPOP) 开始已经弃用了之前的 Camera 类,提供了 camera2 相关 API,目前网上关于 camera2 API 介绍的资料比较少,笔者搜集网上资料,结合自己的实践,在这里做一个总结. 流程 因为 camera2 提供的接口比较多,虽然很灵活,但是也增加了使用的复杂度.首先来大致了解一下调用 camera2 的流程,方便我们理清思路. 要显示相

  • Android 自定义相机及分析源码

    Android 自定义相机及分析源码 使用Android 系统相机的方法: 要想让应用有相机的action,咱们就必须在清单文件中做一些声明,好让系统知道,如下 <intent-filter> <action android:name="android.intent.action.IMAGE_CAPTURE" /> <category android:name="android.intent.category.DEFAULT" />

  • Android自定义照相机倒计时拍照

    自定义拍照会用到SurfaceView控件显示照片的预览区域,以下是布局文件: 两个TextView是用来显示提示信息和倒计时的秒数的 相关教程:Android开发从相机或相册获取图片裁剪 Android启动相机拍照并返回图片 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools&qu

  • Android自定义相机实现自动对焦和手动对焦

    Android自定义相机实现自动对焦和手动对焦: 不调用系统相机,因为不同的机器打开相机呈现的界面不统一也不能满足需求. 所以为了让程序在不同的机器上呈现出统一的界面,并且可以根据需求进行布局,做了此demo. 程序实现代码如下: import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.la

  • Android自定义相机界面的实现代码

    我们先实现拍照按钮的圆形效果哈,Android开发中,当然可以找美工人员设计图片,然后直接拿进来,不过我们可以自己写代码实现这个效果哈,最常用的的是用layout-list实现图片的叠加,我们这个layout命名为btn_take_photo.xml,这是一个自定义的drawable文件,所以按照规范,我们要将它放在drawable文件夹里. 注意:drawable文件夹一般是来放自定义的drawable文件的,可以将它看成自己写的背景样式等等哦 解释代码: layer-list里面放3个ite

  • Android中关于自定义相机预览界面拉伸问题

    关于自定义相机预览界面拉伸问题 1.导致主要变形的原因是Camera预览界面旋转的角度和摄像头挂载的角度不同导致的 2.我们的Activity设置的方向是竖屏,这是手机的自然方向 所以宽比高短 3.角度:所谓屏幕和摄像头的角度,指的是相对于自然方向旋转过的角度,根据旋转角度即可获知当前的方向 4.假如说:手机是竖屏的情况下, 自然角度为0,但是Camera逆时针旋转90度,那咱们设置顺时针旋转90度,就正常 .手机是横屏的情况下Camera返回为0度 ,如果设置顺时针旋转90度,就回旋转 怎么设

  • Android自定义组件获取本地图片和相机拍照图片

    iOS中有封装好的选择图片后长按出现动画删除效果,效果如下 而Android找了很久都没有找到有这样效果的第三方组件,最后懒得找了还是自己实现这效果吧 选择图片后还可对图片进行剪裁 当然,代码中还有很多不完善的地方,我接下来会继续完善这个组件的 已经上传到开源社区,欢迎大家来Star啊~ Demo源码:传送门 设计中的碰到的一些问题和解决思路 1.如何让加号图片显示在GridView最后面 首先在调用GridAdapter构造方法时就加载加号图片 /** * 图片适配器 * @param con

  • Android自定义相机实现定时拍照功能

    这篇博客为大家介绍Android自定义相机,并且实现倒计时拍照功能. 首先自定义拍照会用到SurfaceView控件显示照片的预览区域,以下是布局文件: activity_main.xml <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="m

  • Android自定义照相机Camera出现黑屏的解决方法

    本文实例讲述了Android自定义照相机Camera出现黑屏的解决方法.分享给大家供大家参考,具体如下: 对于一些手机,像HTC,当自定义Camera时,调用Camera.Parameters的 parameters.setPreviewSize(width, height)方法时,如果width和height为奇数情况下,则会出现黑屏现象,解决办法可参考SDK提供的ApiDemos中关于Camera的 例子: List<Size> sizes = parameters.getSupporte

随机推荐