編輯:關於Android編程
最近不是很忙,平常在家無聊時看看直播,總會看到一些新奇的驗證登錄方式,剛好自己也要熟悉一下新的開發工具android studio,所以打算自己實現一個.
一、確認需求
先看一下要到達的效果,如圖 :
滑塊圖片做的有點low,見諒.
分析一下,我們要實現這個功能需要什麼
首先需要一個滾動拖拽條,這個簡單,android系統給我們提供了原生SeekBar,裡邊的功能也比較齊全.
然後看一下我們重點要實現的功能,實現圖片上的驗證功能.我們分步來看
1.要在圖片上隨機位置顯示出一個陰影部分
2.要生成一個帶有邊框的並且符合陰影部分的驗證滑塊
3.生成的滑塊和陰影部分,角度隨機
4.生成的滑塊要隨著拖拽條一同滾動
5.驗證是否成功
二、思考解決方案
第一點,其實很簡單,我們只需要一張有透明度的陰影圖片,並且隨機生成一個坐標,遮擋住圖片即可.
第二點,可以用setXfermode中的圖像交叉模式來生成一個帶有邊框的滑塊.
第三點,可以通過改變圖像的matrix來實現對圖片的旋轉.
第四點,對外提供一個方法,來不斷改變滑塊的位置.
第五點,提供一個回調接口,來驗證是否成功.
三、代碼實現
有了上邊的思路,我們打算自定義一個DouYuView繼承自ImageView來實現功能.
先是創建一個attr文件定義一些自定義的屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> <!--滑塊的高度--> <attr name="unitHeight" format="dimension" /> <!--滑塊的寬度--> <attr name="unitWidth" format="dimension" /> <!--滑塊占圖片高度的比例--> <attr name="unitHeightScale" format="integer" /> <!--滑塊占圖片寬度的比例--> <attr name="unitWidthScale" format="integer" /> <!--滑塊邊框的圖片資源--> <attr name="unitShadeSrc" format="reference" /> <!--陰影部分的圖片資源--> <attr name="unitShowSrc" format="reference" /> <!--是否需要旋轉--> <attr name="needRotate" format="boolean" /> <!--驗證時的誤差值--> <attr name="deviate" format="integer" /> <declare-styleable name="DouYuView"> <attr name="unitHeight" /> <attr name="unitWidth" /> <attr name="unitHeightScale" /> <attr name="unitWidthScale" /> <attr name="unitShadeSrc" /> <attr name="unitShowSrc" /> <attr name="needRotate" /> <attr name="deviate" /> </declare-styleable> </resources>
代碼中的屬性及初始化
/** * 定義畫筆 */ private Paint mPaint; /** * 驗證的圖像 */ private Bitmap mBitmap; /** * 驗證滑塊的高 */ private int mUintHeight; /** * 驗證滑塊的寬 */ private int mUintWidth; /** * 驗證滑塊寬占用整體圖片大小的比例,默認1/5 */ private int mUnitWidthScale; /** * 驗證滑塊高度占用整體圖片大小的比例,默認1/4 */ private int mUnitHeightScale; /** * 隨機生成滑塊的X坐標 */ private int mUnitRandomX; /** * 隨機生成滑塊的Y坐標 */ private int mUnitRandomY; /*** * 滑塊移動的距離 */ private float mUnitMoveDistance = 0; /*** * 滑塊圖像 */ private Bitmap mUnitBp; /** * 驗證位置圖像 */ private Bitmap mShowBp; /** * 背景陰影圖像 */ private Bitmap mShadeBp; /** * 是否需要旋轉 **/ private boolean needRotate; /** * 旋轉的角度 */ private int rotate; /** * 判斷是否完成的偏差量,默認為10 */ public int DEFAULT_DEVIATE; /** * 判斷是否重新繪制圖像 */ private boolean isReSet = true; public DouYuView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 獲取自定義屬性 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DouYuView); mUintWidth = ta.getDimensionPixelOffset(R.styleable.DouYuView_unitHeight, 0); mUintHeight = ta.getDimensionPixelOffset(R.styleable.DouYuView_unitHeight, 0); mUnitHeightScale = ta.getInteger(R.styleable.DouYuView_unitHeightScale, 4); mUnitWidthScale = ta.getInteger(R.styleable.DouYuView_unitWidthScale, 5); Drawable showBp = ta.getDrawable(R.styleable.DouYuView_unitShowSrc); mShowBp = drawableToBitamp(showBp); Drawable shadeBp = ta.getDrawable(R.styleable.DouYuView_unitShadeSrc); mShadeBp = drawableToBitamp(shadeBp); needRotate = ta.getBoolean(R.styleable.DouYuView_needRotate, true); DEFAULT_DEVIATE = ta.getInteger(R.styleable.DouYuView_deviate, 10); ta.recycle(); // 初始化 mPaint = new Paint(); mPaint.setAntiAlias(true); if (needRotate) { rotate = (int) (Math.random() * 3) * 90; } else { rotate = 0; } } 注釋寫的比較全,應該很好明白,然後我們需要獲取我們的圖片資源並且對圖片做一些處理,來使我們的圖片不會出現變形等情況. /** * drawable轉bitmap * * @param drawable * @return */ private Bitmap drawableToBitamp(Drawable drawable) { if (null == drawable) { return null; } if (drawable instanceof BitmapDrawable) { BitmapDrawable bd = (BitmapDrawable) drawable; return bd.getBitmap(); } int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, w, h); drawable.draw(canvas); return bitmap; } /** * 縮放圖片 * * @param bp * @param x * @param y * @return */ public static Bitmap handleBitmap(Bitmap bp, float x, float y) { int w = bp.getWidth(); int h = bp.getHeight(); float sx = (float) x / w; float sy = (float) y / h; Matrix matrix = new Matrix(); matrix.postScale(sx, sy); Bitmap resizeBmp = Bitmap.createBitmap(bp, 0, 0, w, h, matrix, true); return resizeBmp; }
我們需要獲取到要驗證的圖片資源,然後需要通過縮放來使圖片和控件寬高保持一致,以免導致圖片顯示異常等問題.
獲取到圖片資源之後就可以去生成陰影部分和滑塊了,我們使用兩張圖片,需要注意的是第二張圖片邊框內部分使用白色,這次用兩張星星的圖片如下:
在生成陰影部分之前,需要隨機生成一個坐標點,代碼如下:
/** * 隨機生成生成滑塊的XY坐標 */ private void initUnitXY() { mUnitRandomX = (int) (Math.random() * (mBitmap.getWidth() - mUintWidth)); mUnitRandomY = (int) (Math.random() * (mBitmap.getHeight() - mUintHeight)); // 防止生成的位置距離太近 if (mUnitRandomX <= mBitmap.getWidth() / 2) { mUnitRandomX = mUnitRandomX + mBitmap.getWidth() / 4; } // 防止生成的X坐標截圖時導致異常 if (mUnitRandomX + mUintWidth > getWidth()) { initUnitXY(); return; } } 獲取X坐標需要注意兩點,一點是如果X太小,可能驗證時我們滑動的距離太近,效果不太好,所以進行了判斷.另外,如果我們取到的X坐標過大,會導致截取滑塊時,截取異常.非別進行了處理. 算好了坐標值,就可以生成我們的陰影圖片和滑塊了 /** * 創建遮擋的圖片(陰影部分) * * @return */ private Bitmap drawTargetBitmap() { // 繪制圖片 Bitmap showB; if (null != mShowBp) { showB = handleBitmap(mShowBp, mUintWidth, mUintHeight); } else { showB = handleBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.puzzle_show), mUintWidth, mUintHeight); } // 如果需要旋轉圖片,進行旋轉,旋轉後為了保持和滑塊大小一致,需要重新縮放比例 if (needRotate) { showB = handleBitmap(rotateBitmap(rotate, showB), mUintWidth, mUintHeight); } return showB; } 需要注意的一點就是,如果需要隨機旋轉的時候,我們要重新去算一下陰影圖片的寬高,保持和滑塊一致. 繪制帶有邊框的滑塊 /** * 創建結合的圖片(滑塊) * * @param bp */ private Bitmap drawResultBitmap(Bitmap bp) { // 繪制圖片 Bitmap shadeB; if (null != mShadeBp) { shadeB = handleBitmap(mShadeBp, mUintWidth, mUintHeight); } else { shadeB = handleBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.puzzle_shade), mUintWidth, mUintHeight); } // 如果需要旋轉圖片,進行旋轉,旋轉後為了和畫布大小保持一致,避免出現圖像顯示不全,需要重新縮放比例 if (needRotate) { shadeB = handleBitmap(rotateBitmap(rotate, shadeB), mUintWidth, mUintHeight); } Bitmap resultBmp = Bitmap.createBitmap(mUintWidth, mUintHeight, Bitmap.Config.ARGB_8888); Paint paint = new Paint(); paint.setAntiAlias(true); Canvas canvas = new Canvas(resultBmp); canvas.drawBitmap(shadeB, new Rect(0, 0, mUintWidth, mUintHeight), new Rect(0, 0, mUintWidth, mUintHeight), paint); // 選擇混合模式 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); canvas.drawBitmap(bp, new Rect(0, 0, mUintWidth, mUintHeight), new Rect(0, 0, mUintWidth, mUintHeight), paint); return resultBmp; }
同樣在旋轉完成後,需要重新去按照比例對滑塊進行縮放,不然會出現顯示不全的問題.
最後是繪制這些內容
@Override
protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (isReSet) { mBitmap = getBaseBitmap(); if (0 == mUintWidth) { mUintWidth = mBitmap.getWidth() / mUnitWidthScale; } if (0 == mUintHeight) { mUintHeight = mBitmap.getHeight() / mUnitHeightScale; } initUnitXY(); mUnitBp = Bitmap.createBitmap(mBitmap, mUnitRandomX, mUnitRandomY, mUintWidth, mUintHeight); } isReSet = false; canvas.drawBitmap(drawTargetBitmap(), mUnitRandomX, mUnitRandomY, mPaint); canvas.drawBitmap(drawResultBitmap(mUnitBp), mUnitMoveDistance, mUnitRandomY, mPaint); } 准備工作基本完成了,接著需要提供一些方法來完成滑塊滑動、滑塊重置、驗證等功能. /** * 滑塊移動距離 * * @param distance */ public void setUnitMoveDistance(float distance) { mUnitMoveDistance = distance; // 防止滑塊滑出圖片 if (mUnitMoveDistance > mBitmap.getWidth() - mUintWidth) { mUnitMoveDistance = mBitmap.getWidth() - mUintWidth; } invalidate(); } /** * 重置 */ public void reSet() { isReSet = true; mUnitMoveDistance = 0; if (needRotate) { rotate = (int) (Math.random() * 3) * 90; } else { rotate = 0; } invalidate(); } /** * 拼圖成功的回調 **/ interface onPuzzleListener { public void onSuccess(); public void onFail(); } /** * 回調 */ private onPuzzleListener mlistener; /** * 驗證是否拼接成功 */ public void testPuzzle() { if (Math.abs(mUnitMoveDistance - mUnitRandomX) <= DEFAULT_DEVIATE) { if (null != mlistener) { mlistener.onSuccess(); } } else { if (null != mlistener) { mlistener.onFail(); } } } 四、完整代碼及使用 DouYuView: import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.widget.ImageView; /** * Created by junweiliu on 16/4/26. */ public class DouYuView extends ImageView { /** * 定義畫筆 */ private Paint mPaint; /** * 驗證的圖像 */ private Bitmap mBitmap; /** * 驗證滑塊的高 */ private int mUintHeight; /** * 驗證滑塊的寬 */ private int mUintWidth; /** * 驗證滑塊寬占用整體圖片大小的比例,默認1/5 */ private int mUnitWidthScale; /** * 驗證滑塊高度占用整體圖片大小的比例,默認1/4 */ private int mUnitHeightScale; /** * 隨機生成滑塊的X坐標 */ private int mUnitRandomX; /** * 隨機生成滑塊的Y坐標 */ private int mUnitRandomY; /*** * 滑塊移動的距離 */ private float mUnitMoveDistance = 0; /*** * 滑塊圖像 */ private Bitmap mUnitBp; /** * 驗證位置圖像 */ private Bitmap mShowBp; /** * 背景陰影圖像 */ private Bitmap mShadeBp; /** * 是否需要旋轉 **/ private boolean needRotate; /** * 旋轉的角度 */ private int rotate; /** * 判斷是否完成的偏差量,默認為10 */ public int DEFAULT_DEVIATE; /** * 判斷是否重新繪制圖像 */ private boolean isReSet = true; /** * 拼圖成功的回調 **/ interface onPuzzleListener { public void onSuccess(); public void onFail(); } /** * 回調 */ private onPuzzleListener mlistener; /** * 設置回調 * * @param listener */ public void setPuzzleListener(onPuzzleListener listener) { this.mlistener = listener; } public DouYuView(Context context) { this(context, null); } public DouYuView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DouYuView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DouYuView); mUintWidth = ta.getDimensionPixelOffset(R.styleable.DouYuView_unitHeight, 0); mUintHeight = ta.getDimensionPixelOffset(R.styleable.DouYuView_unitHeight, 0); mUnitHeightScale = ta.getInteger(R.styleable.DouYuView_unitHeightScale, 4); mUnitWidthScale = ta.getInteger(R.styleable.DouYuView_unitWidthScale, 5); Drawable showBp = ta.getDrawable(R.styleable.DouYuView_unitShowSrc); mShowBp = drawableToBitamp(showBp); Drawable shadeBp = ta.getDrawable(R.styleable.DouYuView_unitShadeSrc); mShadeBp = drawableToBitamp(shadeBp); needRotate = ta.getBoolean(R.styleable.DouYuView_needRotate, true); DEFAULT_DEVIATE = ta.getInteger(R.styleable.DouYuView_deviate, 10); ta.recycle(); // 初始化 mPaint = new Paint(); mPaint.setAntiAlias(true); if (needRotate) { rotate = (int) (Math.random() * 3) * 90; } else { rotate = 0; } } /** * 隨機生成生成滑塊的XY坐標 */ private void initUnitXY() { mUnitRandomX = (int) (Math.random() * (mBitmap.getWidth() - mUintWidth)); mUnitRandomY = (int) (Math.random() * (mBitmap.getHeight() - mUintHeight)); // 防止生成的位置距離太近 if (mUnitRandomX <= mBitmap.getWidth() / 2) { mUnitRandomX = mUnitRandomX + mBitmap.getWidth() / 4; } // 防止生成的X坐標截圖時導致異常 if (mUnitRandomX + mUintWidth > getWidth()) { initUnitXY(); return; } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (isReSet) { mBitmap = getBaseBitmap(); if (0 == mUintWidth) { mUintWidth = mBitmap.getWidth() / mUnitWidthScale; } if (0 == mUintHeight) { mUintHeight = mBitmap.getHeight() / mUnitHeightScale; } initUnitXY(); mUnitBp = Bitmap.createBitmap(mBitmap, mUnitRandomX, mUnitRandomY, mUintWidth, mUintHeight); } isReSet = false; canvas.drawBitmap(drawTargetBitmap(), mUnitRandomX, mUnitRandomY, mPaint); canvas.drawBitmap(drawResultBitmap(mUnitBp), mUnitMoveDistance, mUnitRandomY, mPaint); } /** * 重置 */ public void reSet() { isReSet = true; mUnitMoveDistance = 0; if (needRotate) { rotate = (int) (Math.random() * 3) * 90; } else { rotate = 0; } invalidate(); } /** * 獲取每次滑動的平均偏移值 * * @return */ public float getAverageDistance(int max) { return (float) (mBitmap.getWidth() - mUintWidth) / max; } /** * 滑塊移動距離 * * @param distance */ public void setUnitMoveDistance(float distance) { mUnitMoveDistance = distance; // 防止滑塊滑出圖片 if (mUnitMoveDistance > mBitmap.getWidth() - mUintWidth) { mUnitMoveDistance = mBitmap.getWidth() - mUintWidth; } invalidate(); } /** * 驗證是否拼接成功 */ public void testPuzzle() { if (Math.abs(mUnitMoveDistance - mUnitRandomX) <= DEFAULT_DEVIATE) { if (null != mlistener) { mlistener.onSuccess(); } } else { if (null != mlistener) { mlistener.onFail(); } } } /** * 創建遮擋的圖片(陰影部分) * * @return */ private Bitmap drawTargetBitmap() { // 繪制圖片 Bitmap showB; if (null != mShowBp) { showB = handleBitmap(mShowBp, mUintWidth, mUintHeight); } else { showB = handleBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.puzzle_show), mUintWidth, mUintHeight); } // 如果需要旋轉圖片,進行旋轉,旋轉後為了保持和滑塊大小一致,需要重新縮放比例 if (needRotate) { showB = handleBitmap(rotateBitmap(rotate, showB), mUintWidth, mUintHeight); } return showB; } /** * 創建結合的圖片(滑塊) * * @param bp */ private Bitmap drawResultBitmap(Bitmap bp) { // 繪制圖片 Bitmap shadeB; if (null != mShadeBp) { shadeB = handleBitmap(mShadeBp, mUintWidth, mUintHeight); } else { shadeB = handleBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.puzzle_shade), mUintWidth, mUintHeight); } // 如果需要旋轉圖片,進行旋轉,旋轉後為了和畫布大小保持一致,避免出現圖像顯示不全,需要重新縮放比例 if (needRotate) { shadeB = handleBitmap(rotateBitmap(rotate, shadeB), mUintWidth, mUintHeight); } Bitmap resultBmp = Bitmap.createBitmap(mUintWidth, mUintHeight, Bitmap.Config.ARGB_8888); Paint paint = new Paint(); paint.setAntiAlias(true); Canvas canvas = new Canvas(resultBmp); canvas.drawBitmap(shadeB, new Rect(0, 0, mUintWidth, mUintHeight), new Rect(0, 0, mUintWidth, mUintHeight), paint); // 選擇交集去上層圖片 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); canvas.drawBitmap(bp, new Rect(0, 0, mUintWidth, mUintHeight), new Rect(0, 0, mUintWidth, mUintHeight), paint); return resultBmp; } /** * 獲取實際顯示的圖片 * * @return */ public Bitmap getBaseBitmap() { Bitmap b = drawableToBitamp(getDrawable()); float scaleX = 1.0f; float scaleY = 1.0f; // 如果圖片的寬或者高與view的寬高不匹配,計算出需要縮放的比例;縮放後的圖片的寬高,一定要大於我們view的寬高;所以我們這裡取大值; scaleX = getWidth() * 1.0f / b.getWidth(); scaleY = getHeight() * 1.0f / b.getHeight(); Matrix matrix = new Matrix(); matrix.setScale(scaleX, scaleY); Bitmap bd = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, true); return bd; } /** * drawable轉bitmap * * @param drawable * @return */ private Bitmap drawableToBitamp(Drawable drawable) { if (null == drawable) { return null; } if (drawable instanceof BitmapDrawable) { BitmapDrawable bd = (BitmapDrawable) drawable; return bd.getBitmap(); } int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, w, h); drawable.draw(canvas); return bitmap; } /** * 縮放圖片 * * @param bp * @param x * @param y * @return */ public static Bitmap handleBitmap(Bitmap bp, float x, float y) { int w = bp.getWidth(); int h = bp.getHeight(); float sx = (float) x / w; float sy = (float) y / h; Matrix matrix = new Matrix(); matrix.postScale(sx, sy); Bitmap resizeBmp = Bitmap.createBitmap(bp, 0, 0, w, h, matrix, true); return resizeBmp; } /** * 旋轉圖片 * * @param degree * @param bitmap * @return */ public Bitmap rotateBitmap(int degree, Bitmap bitmap) { Matrix matrix = new Matrix(); matrix.postRotate(degree); Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); return bm; } } MainActivity: import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.SeekBar; import android.widget.Toast; public class MainActivity extends Activity { /** * 滑塊 */ private SeekBar mSeekBar; /** * 自定義的控件 */ private DouYuView mDY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { mDY = (DouYuView) findViewById(R.id.dy_v); mSeekBar = (SeekBar) findViewById(R.id.sb_dy); mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { // Log.e("main", "當前位置" + i); mDY.setUnitMoveDistance(mDY.getAverageDistance(seekBar.getMax()) * i); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { mDY.testPuzzle(); } }); mDY.setPuzzleListener(new DouYuView.onPuzzleListener() { @Override public void onSuccess() { // mSeekBar.setEnabled(false); Toast.makeText(MainActivity.this, "驗證成功", Toast.LENGTH_SHORT).show(); mSeekBar.setProgress(0); mDY.reSet(); } @Override public void onFail() { Toast.makeText(MainActivity.this, "驗證失敗", Toast.LENGTH_SHORT).show(); mSeekBar.setProgress(0); } }); } }
顯示的結果圖:
handler在安卓開發中是必須掌握的技術,但是很多人都是停留在使用階段。使用起來很簡單,就兩個步驟,在主線程重寫handler的handleMessage( )方法,在
Android N 除了提供諸多新特性和功能外,還對系統和 API 行為做出了各種變更。 本文重點介紹您應該了解並在開發應用時加以考慮的一些重要變更。如果您之前發布過 A
Android 目前支持下面幾個版本的OpenGL ES API : OpenGL ES 1.0 和 1.1 :Android 1.0和更高的版本支持這個API規范。 O
題外話這篇本來和之前的系列要一起出的,但是因為中間公司要發布一個版本,給耽擱了,今天工作做完了,又閒了下來。所以就又來繼續jenkins構建Android項目持續集成系列