實用控制項分享:自定義逼真相機光圈View

来源:http://www.cnblogs.com/willhua/archive/2016/08/11/5763189.html
-Advertisement-
Play Games

最近手機界開始流行雙攝像頭,大光圈功能也應用而生。所謂大光圈功能就是能夠對照片進行後期重新對焦,其實現的原理主要是對拍照期間獲取的深度圖片與對焦無窮遠的圖像通過演算法來實現重新對焦的效果。 在某雙攝手機的大光圈操作界面有個光圈的操作圖標,能夠模擬光圈調節時的真實效果,感覺還不錯,於是想著實現該效果。現 ...


最近手機界開始流行雙攝像頭,大光圈功能也應用而生。所謂大光圈功能就是能夠對照片進行後期重新對焦,其實現的原理主要是對拍照期間獲取的深度圖片與對焦無窮遠的圖像通過演算法來實現重新對焦的效果。

在某雙攝手機的大光圈操作界面有個光圈的操作圖標,能夠模擬光圈調節時的真實效果,感覺還不錯,於是想著實現該效果。現在把我的實現方法貢獻給大家,萬一你們公司也要做雙攝手機呢?( ̄┰ ̄*)

首先,百度一下光圈圖片,觀察觀察,就可以發現其關鍵在於計算不同的光圈值時各個光圈葉片的位置。為了計算簡便,我以六個直邊葉片的光圈效果為例來實現(其他形式,比如七個葉片,也就是位置計算稍微沒那麼方便;而一些圓弧的葉片,只要滿足葉片兩邊的圓弧半徑是一樣的就行。為什麼要圓弧半徑一樣呢?仔細觀察就可以發現,相鄰兩葉片之間要相互滑動,而且要保持一樣的契合距離,根據我曾今小學幾何科打滿分的經驗可以判斷出,等徑的圓弧是不錯滴,其他高級曲線能不能實現該效果,請問數學家( ̄┰ ̄*)!其他部分原理都是一樣的)。 

製作效果圖

 

先說明一下本自定義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>

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一.點擊查看大圖 1.點擊圖片或按鈕(點擊查看大圖按鈕),modal出來一個控制器,顯示大圖片 2.怎麼處理能讓點擊圖片就能查看大圖? 兩種方法:1.給圖片添加點按手勢 2.給圖片所在的view上添加 - (void)touchesEnded:(NSSet<UITouch *> *)touches ...
  • 目前市面上的應用,貌似除了微信和手Q都會比較擔心被用戶或者系統(廠商)殺死問題。本文對 Android 進程拉活進行一個總結。 ...
  • 一.整體佈局 一.整體佈局 1.項目需求 點擊左邊cell,右邊的cell數據更新 2.界面搭建 2.1交給兩個控制器管理比較麻煩,點擊一個控制器需要通知另外一個控制器 2. 2因此交給一個控制器管理比較好 2.3用xib搭建,左右各放一個tableView就可以了 3.開發順序 先做左邊的tabl ...
  • 前言:在上一篇文章中我們講到了AsyncTask的基本使用、AsyncTask的封裝、AsyncTask 的串列/並行線程隊列、自定義線程池、線程池的快速創建方式。 對線程池不瞭解的同學可以先看 Android AsyncTask 深度理解、簡單封裝、任務隊列分析、自定義線程池 1、Executor ...
  • 問題描述:在使用AndroidStudio開發項目時,使用環信重寫了聊天界面後,運行時app就崩掉了,查看日誌報告,提示報錯如下: 根據提示,明顯是配置出現問題,找環信的客服咨詢,他們只說讓我去掉引用的v7包,可是仍然不好使。 解決方案:後來機智如我,查看環信Demo的Activity,是繼承Eas ...
  • 一.上拉刷新 1.為什麼要做上拉刷新? 想要看一些舊的(更多)數據,就需要上拉刷新,載入更多數據 2.上拉刷新永遠都顯示在tableView最底部,用什麼搭建? tableFootView永遠在tableView最底部,可以用它來搭建 3.上拉刷新業務邏輯 3.1當上拉刷新控制項(footView)全 ...
  • 寫android通知的時候發現Notification的setLatestEventInfo被棄用,於是搜素並整理了一下新的android通知的基本用法。 一、獲取NotificationManager實例 二、創建Notification實例 在這裡需要根據project的min sdk來選擇實現 ...
  • 10
    10 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...