手势解锁

31 阅读6分钟

手势view 默认九宫格 使用时应注册回调,会返回路径。该自定义view最关键的算法是能检测是否能补全跨越点位,能有提高解锁成功率

解锁保护的关键是使用Application注册一个activity的监听器,在onActivityResume去处理是否要拉起解锁保护页

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.xxx.CommonUtil;

/**
 * @PackageName : com.xxx.lock
 * @Time : 5/11/23 11:49 AM
 * @Description :
 */

public class LockView extends View {
    public static int MINSELECTED = 4;
    private final static String TAG = "AlipayLockView";

    public interface OnLockInputListener {

        void onLockDone(String path);

        void onLockInput(int indexUnlocked);
    }

    public interface OnFirstInputListener {
        void onFirstInput();
    }

    private Paint mPaint;

    private int iCount = 9;
    private int iCountPerLine = 3;
    private int iPointBit = 0;
    private int[] iPointArray;
    private int iCurCount;
    private boolean iDrawLineNeeded = false;
    private boolean iDone = false;
    private Point iCurPointerPoint = new Point();
    private OnLockInputListener mOnLockInputListener = null;
    private OnFirstInputListener mOnFirstInputListener = null;
    private Drawable mGridFocused = null;
    private Drawable mGridNormal = null;
    private Drawable mGridError = null;
    float mGridMargin = 0;
    float mGridWidth = 0;
    float mGridHeight = 0;
    float mGridBetweenX = 0;
    float mGridBetweenY = 0;
    private float mGridRadius = 0;
    private boolean mDensityLow = false;
    private boolean isFirstInput = true;
    //手势轨迹是否隐藏
    private boolean isHideOrbit = false;
    //手势验证是否错误
    private boolean isCheckError = false;
    //是否为设置手势
    private boolean isSetGesture = false;


    public AlipayLockView(Context context) {
        this(context, null);

    }

    public AlipayLockView(Context context, AttributeSet attrs) {
        super(context, attrs, 0);

//        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockView);
//        mGridFocused = a.getDrawable(R.styleable.LockView_gridFocused);
//        mGridNormal = a.getDrawable(R.styleable.LockView_gridNormal);
//        mGridError = a.getDrawable(R.styleable.LockView_gridError);
//        a.recycle();
        init();
    }

    public void setOnLockInputListener(OnLockInputListener l) {
        mOnLockInputListener = l;
    }

    public void setOnFirstInputListener(OnFirstInputListener l) {
        mOnFirstInputListener = l;
    }

    public void clear() {
        iPointBit = 0;
        iDone = false;
        isFirstInput = true;
        isCheckError = false;
        if (0 != iCurCount) {
            iCurCount = 0;
            this.invalidate();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                iDone = true;
                if (null != mOnLockInputListener) {
                    String path = "";
                    for (int i = 0; i < iCurCount; i++) {
                        path += iPointArray[i];
                    }
                    if (path.length() > 0) {
                        mOnLockInputListener.onLockDone(path);
                    }
                }
                iDrawLineNeeded = false;
                this.invalidate();
                result = true;
                break;
            case MotionEvent.ACTION_DOWN:
                isCheckError = false;
                if (iDone) {
                    iPointBit = 0;
                    isFirstInput = true;
                    iCurCount = 0;
                    iDone = false;
                }
                result = checkDrawNeeded(event.getX(), event.getY());
                if (result) {
                    this.invalidate();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                result = true;
                break;
            case MotionEvent.ACTION_MOVE:
                iDrawLineNeeded = false;
                final int historySize = event.getHistorySize();
                for (int i = 0; i < historySize + 1; i++) {
                    final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
                    final float y = i < historySize ? event.getHistoricalY(i) : event.getY();

                    iDrawLineNeeded |= checkDrawNeeded(x, y);
                }

                if (iDrawLineNeeded) {
                    iCurPointerPoint.x = (int) event.getX();
                    iCurPointerPoint.y = (int) event.getY();
                    this.invalidate();
                }

                break;
            default:
                result = true;
                break;
        }

        return true;//super.onTouchEvent(event);
    }
    private Paint mPaintNormal;
    private Paint mPaintError;
    private Paint mPaintFocus;
    private void init() {
        mPaintNormal = new Paint();

        mPaintError = new Paint();

        mPaintFocus = new Paint();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        isFirstInput = true;

        iPointArray = new int[iCount];
        for (int i = 0; i < iCount; i++) {
            iPointArray[i] = -1;
        }
        iCurCount = 0;

        DisplayMetrics dmDisplayMetrics = getResources().getDisplayMetrics();
        if (DisplayMetrics.DENSITY_HIGH > dmDisplayMetrics.densityDpi || 728 == dmDisplayMetrics.heightPixels) {
            mDensityLow = true;
            mPaint.setStrokeWidth(CommonUtil.dp2Px(getContext(), 2));
        }
        //meizu
        else if (960 == dmDisplayMetrics.heightPixels && 640 == dmDisplayMetrics.widthPixels) {
            mDensityLow = true;
            mPaint.setStrokeWidth(CommonUtil.dp2Px(getContext(), 4));
        } else {
            mPaint.setStrokeWidth(CommonUtil.dp2Px(getContext(), 4));
        }
    }
    private float gridWidth;
    private float gridHeight;
    private float gridMargin;
    private float gridRadius;
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        mGridWidth = mGridHeight = width / (2f * iCountPerLine - 1);
        mGridMargin = (width - mGridWidth * iCountPerLine) / (2 * iCountPerLine);
        mGridBetweenX = mGridWidth + 2 * mGridMargin;

        mGridBetweenY = mGridHeight + (mDensityLow ? 1 : 2) * mGridMargin;
        mGridRadius = mGridWidth / 2;
        float height = mGridBetweenY * (iCountPerLine - 1) + mGridHeight + (mDensityLow ? 0 : 2) * mGridMargin;
        setMeasuredDimension(width, (int) height);

        //适配新视觉
        gridWidth = gridHeight = Math.min(mGridWidth, CommonUtil.dp2Px(getContext(), 60));
        gridMargin = (mGridWidth - gridWidth) / 2;
        gridRadius = gridWidth / 2;

        Log.i("yglOnMeasure", "onMeasure  mGridWidth = " + mGridWidth
                + " 60dp = " + CommonUtil.dp2Px(getContext(), 60)
                + " mGridMargin = " + mGridMargin
                + " mGridBetweenX = " + mGridBetweenX
                + " mGridBetweenY = " + mGridBetweenY
                + " mGridRadius = " + mGridRadius
                + " height = " + height
                + " gridWidth = " + gridWidth
                + " gridMargin = " + gridMargin
                + " gridRadius = " + gridRadius);
//        mGridFocused.setBounds(gridMargin, gridMargin, gridMargin + gridWidth, gridMargin + gridWidth);
//        mGridNormal.setBounds(gridMargin, gridMargin, gridMargin + gridWidth, gridMargin + gridWidth);
//        mGridError.setBounds(gridMargin, gridMargin, gridMargin + gridWidth, gridMargin + gridWidth);

    }
    //检测是不是要主动补全
    private boolean checkDrawNeeded(float x, float y) {
        for (int i = 0; i < iCount; i++) {
            if (isAvailable(i)) {
                if (CheckInGrid(x, y, i)) {
                    if (iCurCount > 0) {
                        int indexMissing = detectCircleMissing(iPointArray[iCurCount - 1], i);
                        if (-1 != indexMissing) {
                            hit(indexMissing);
                        }
                    }

                    hit(i);
                    break;
                }
            }
        }

        return iCurCount > 0;
    }

    private void hit(int index) {
        iPointArray[iCurCount++] = index;
        iPointBit |= (1 << (index + 1));
    }

    private boolean isAvailable(int index) {
        return 0 == (iPointBit & (1 << (index + 1)));
    }
    //检测补全算法
    private int detectCircleMissing(int indexBefore, int curIndex) {
        final int xBefore = indexBefore % 3;
        final int yBefore = indexBefore / 3;
        final int xCur = curIndex % 3;
        final int yCur = curIndex / 3;

        if (0 == (xBefore - xCur) % 2 &&
                0 == (yBefore - yCur) % 2) {
            int tempIndex = (indexBefore + curIndex) / 2;
            if (isAvailable(tempIndex)) {
                return tempIndex;
            }
        }

        return -1;
    }

    private boolean CheckInGrid(float x, float y, int index) {
        float xPoint = mGridMargin + mGridBetweenX * (index % iCountPerLine) + mGridWidth / 2;
        float yPoint = (mDensityLow ? 0 : mGridMargin) + mGridBetweenY * (index / iCountPerLine) + mGridWidth / 2;

        int deltaX = (int) (xPoint - x);
        int deltaY = (int) (yPoint - y);

        double deltaR = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
        if (mDensityLow) {
            return deltaR <= mGridRadius;
        } else {
            return deltaR <= mGridRadius + CommonUtil.dp2Px(getContext(), 8);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < iCount; i++) {
            drawGrid(canvas, i);
        }

        for (int i = 0; i < iCurCount - 1; i++) {
            if (isCheckError) {
                mPaint.setColor(Color.parseColor("#FF3141"));
            }
            canvas.drawLine(GetGridX(iPointArray[i]), GetGridY(iPointArray[i]),
                    GetGridX(iPointArray[i + 1]), GetGridY(iPointArray[i + 1]), mPaint);
        }

        if (iDrawLineNeeded && iCurCount > 0) {
            if (isHideOrbit) {
                mPaint.setColor(Color.argb(0, 0, 0, 0));
            } else {
                mPaint.setColor(Color.parseColor("#1677ff"));
            }
            canvas.drawLine(GetGridX(iPointArray[iCurCount - 1]), GetGridY(iPointArray[iCurCount - 1]),
                    iCurPointerPoint.x, iCurPointerPoint.y, mPaint);
        }


        super.onDraw(canvas);
    }


    private int GetGridX(int index) {
        return (int)(mGridMargin + mGridBetweenX * (index % iCountPerLine) + mGridRadius);
    }

    private int GetGridY(int index) {
        return (int)((mDensityLow ? 0 : mGridMargin) + mGridBetweenY * (index / iCountPerLine) + mGridRadius);
    }


    private void drawGrid(Canvas canvas, int index) {
        float x = mGridMargin + mGridBetweenX * (index % iCountPerLine);
        float y = (mDensityLow ? 0 : mGridMargin) + mGridBetweenY * (index / iCountPerLine);

        int occupied = iPointBit & (1 << (index + 1));

        canvas.save();
        canvas.translate(x, y);
        if (occupied > 0) {
            if (isHideOrbit) {
                if (isCheckError) {
//                    mGridError.draw(canvas);
                    drawIcon(error, canvas);
                } else {
//                    mGridNormal.draw(canvas);
                    drawIcon(normal, canvas);
                }
            } else {
                if (isCheckError) {
//                    mGridError.draw(canvas);
                    drawIcon(error, canvas);
                } else {
//                    mGridFocused.draw(canvas);
                    drawIcon(focus, canvas);
                }
            }

            //首次点击9个元圈中第一个时触发
            if (isFirstInput && mOnFirstInputListener != null) {
                isFirstInput = false;
                mOnFirstInputListener.onFirstInput();
            }
        } else {
//            mGridNormal.draw(canvas);
            drawIcon(normal, canvas);
        }

        canvas.restore();
        //canvas.drawCircle(x, y, iPointSmallRadius, mPaint);
    }

    private final static int normal = 1; //内 #CBCBCB/#3E3E3E   外 #ECECEC/#262626
    private final static int focus = 2; //内 COLOR_BRAND1 外COLOR_WATHET #E7F1FF/#0D2543    浅蓝色(按钮)
    private final static int error = 3; //内 COLOR_RED FF3141/FF4A58 外 #FEDCDF/#391517
    private RectF oval;

    private void drawIcon(int tag, Canvas canvas) {
        if (null == oval) {
            oval = new RectF();
            oval.left = (gridMargin);
            oval.top = (gridMargin);
            oval.right = gridMargin + gridWidth;
            oval.bottom = gridMargin + gridHeight;
        }
        if (tag == normal) {
            //画整圆弧
            mPaintNormal.setAntiAlias(true);//防锯齿
            mPaintNormal.setColor(Color.parseColor("#ECECEC"));
            mPaintNormal.setStyle(Paint.Style.STROKE);
            mPaintNormal.setStrokeWidth(CommonUtil.dp2Px(getContext(), 1));

            canvas.drawArc(oval, 0, 360, false, mPaintNormal);
            mPaintNormal.reset();

            //画圆
            //画圆画笔设置
            mPaintNormal.setAntiAlias(true);//防锯齿
            mPaintNormal.setColor(Color.parseColor("#CBCBCB"));
            mPaintNormal.setStyle(Paint.Style.FILL);
            canvas.drawCircle(mGridWidth / 2f, mGridHeight / 2f, gridRadius * 0.46f, mPaintNormal);
            mPaintNormal.reset();

        } else if (tag == focus) {
            //画整圆弧
            mPaintFocus.setAntiAlias(true);//防锯齿
            mPaintFocus.setColor(Color.parseColor("#E7F1FF"));
            mPaintFocus.setStyle(Paint.Style.FILL_AND_STROKE);
            mPaintFocus.setStrokeWidth(CommonUtil.dp2Px(getContext(), 1));

            canvas.drawArc(oval, 0, 360, true, mPaintFocus);
            mPaintFocus.reset();

            mPaintFocus.setAntiAlias(true);//防锯齿
            mPaintFocus.setColor(Color.parseColor("#1677ff"));
            mPaintFocus.setStyle(Paint.Style.FILL);
            canvas.drawCircle(mGridWidth / 2f, mGridHeight / 2f, gridRadius * 0.46f, mPaintFocus);
            mPaintFocus.reset();
        } else if (tag == error) {
            //画整圆弧
            mPaintError.setAntiAlias(true);//防锯齿
            mPaintError.setColor(Color.parseColor("#FEDCDF"));
            mPaintError.setStyle(Paint.Style.FILL_AND_STROKE);
            mPaintError.setStrokeWidth(CommonUtil.dp2Px(getContext(), 1));

            canvas.drawArc(oval, 0, 360, true, mPaintError);
            mPaintError.reset();

            mPaintError.setAntiAlias(true);//防锯齿
            mPaintError.setColor(Color.parseColor("#FF3141"));
            mPaintError.setStyle(Paint.Style.FILL);
            canvas.drawCircle(mGridWidth / 2f, mGridHeight / 2f, gridRadius * 0.46f, mPaintError);
            mPaintError.reset();
        }
    }

    public void setIsHideOrbit(boolean isHideOrbit) {
        this.isHideOrbit = isHideOrbit;
    }

    public void setIsCheckError(boolean isCheckError) {
        this.isCheckError = isCheckError;
    }

    public boolean getIsCheckError() {
        return this.isCheckError;
    }

    public void setIsSetGesture(boolean isSetGesture) {
        this.isSetGesture = isSetGesture;
    }
}

util

public class CommonUtil {
    private final static String TAG = "CommonUtils";
    private static final int MIN_DELAY_TIME = 500;
    private static long sLastClickTime;
    private static final int UPLOADING_MIN_DELAY_TIME = 50;
    private static long sLastUploadTime;

    public static void performAddView(ViewGroup root, View child) {
        try {
            if (root == null || child == null) {
                return;
            }
            ViewGroup parent = (ViewGroup) child.getParent();
            if (parent != null) {
                parent.removeView(child);
            }
            root.addView(child);
        } catch (Exception e) {
            Log.e(TAG, "performAddView e" + e);
        }
    }


    public static int dp2Px(Context context, float dp) {
        int px = 0;
        if (context == null) return px;
        try {
            final float scale = context.getResources().getDisplayMetrics().density;
            px = (int) (dp * scale + 0.5f);
        } catch (Throwable e) {
            Log.e("CommonUtil", "dp2px e", e);
        }
        return px;
    }

    public static boolean isFastClick() {
        long currentClickTime = System.currentTimeMillis();
        boolean isFastClick = (currentClickTime - sLastClickTime) <= MIN_DELAY_TIME;
        Log.e(TAG, "log_common_FastClickUtil : " + (currentClickTime - sLastClickTime));
        sLastClickTime = currentClickTime;
        return isFastClick;
    }

    public static boolean isReUpload() {
        long currentUploadTime = System.currentTimeMillis();
        boolean isReLoad = (currentUploadTime - sLastUploadTime) <= UPLOADING_MIN_DELAY_TIME;
        Log.e(TAG, "log_common_ReUploadUtil : " + (currentUploadTime - sLastUploadTime));
        sLastUploadTime = currentUploadTime;
        return isReLoad;
    }

    /**
     * 判断是否为平板
     *
     * @return
     */
    public static boolean isPadInches(Context context) {
        try {
            WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics dm = new DisplayMetrics();
            display.getMetrics(dm);
            double x = Math.pow(dm.widthPixels / dm.xdpi, 2);
            double y = Math.pow(dm.heightPixels / dm.ydpi, 2);
            //屏幕尺寸
            double screenInches = Math.sqrt(x + y);
            //大于7尺寸则为Pad
            Log.e(TAG, "isPadInches = " + screenInches);
            return screenInches >= 7;
        } catch (Exception e) {
            Log.e(TAG, "isPadInches e ", e);
        }
        return false;
    }

    public static boolean isPadGoogle(Context context) {
        return (context.getResources().getConfiguration().screenLayout
                & Configuration.SCREENLAYOUT_SIZE_MASK)
                >= Configuration.SCREENLAYOUT_SIZE_LARGE;
    }

    /**
     * 获取已经安装app的信息列表
     *
     * @param context
     * @return
     */
    public static ArrayList<HashMap<String, Object>> getItems(Context context) {
        PackageManager pckMan = context.getPackageManager();
        ArrayList<HashMap<String, Object>> items = new ArrayList<HashMap<String, Object>>();
        List<PackageInfo> packageInfo = pckMan.getInstalledPackages(0);
        for (PackageInfo pInfo : packageInfo) {
            HashMap<String, Object> item = new HashMap<String, Object>();
            item.put("appimage", pInfo.applicationInfo.loadIcon(pckMan));
            item.put("packageName", pInfo.packageName);
            item.put("versionCode", pInfo.versionCode);
            item.put("versionName", pInfo.versionName);
            item.put("appName", pInfo.applicationInfo.loadLabel(pckMan).toString());
            items.add(item);
//            Log.i("ygl", " packageName: " + pInfo.packageName + " ,versionCode: " + pInfo.versionCode +
//                    " ,versionName: " + pInfo.versionName + " ,appName: " + pInfo.applicationInfo.loadLabel(pckMan).toString());
            System.out.println("ygl packageName: " + pInfo.packageName + " ,versionCode: " + pInfo.versionCode +
                    " ,versionName: " + pInfo.versionName + " ,appName: " + pInfo.applicationInfo.loadLabel(pckMan).toString());
        }
        return items;
    }

    /**
     * 获取指定app的版本code码
     *
     * @param context
     * @param packageName
     * @return
     */
    public static int getVersionCode(Context context, String packageName) {
        PackageManager manager = context.getPackageManager();
        int code = 0;
        try {
            PackageInfo info = manager.getPackageInfo(packageName, 0);
            code = info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return code;
    }

    /**
     * 获取指定app的版本号
     *
     * @param context
     * @param packageName
     * @return
     */
    public static String getVersionName(Context context, String packageName) {
        PackageManager manager = context.getPackageManager();
        String name = null;
        try {
            PackageInfo info = manager.getPackageInfo(packageName, 0);
            name = info.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        return name;
    }

    public static boolean isDarkNotificationTheme(Context context) {
        return !isSimilarColor(Color.BLACK, getNotificationColor(context));
    }

    /**
     * 获取通知栏颜色
     *
     * @param context
     * @return
     */
    public static int getNotificationColor(Context context) {
        DownloadNotificationFactory.DefaultNotification defaultNotification = new DownloadNotificationFactory.DefaultNotification(context);
        int layoutId = defaultNotification.getNotification().bigContentView.getLayoutId();
        ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null, false);
        if (viewGroup.findViewById(android.R.id.title) != null) {
            return ((TextView) viewGroup.findViewById(android.R.id.title)).getCurrentTextColor();
        }
        return findColor(viewGroup);
    }

    /**
     * @param baseColor
     * @param color
     * @return
     */
    private static boolean isSimilarColor(int baseColor, int color) {
        int simpleBaseColor = baseColor | 0xff000000;
        int simpleColor = color | 0xff000000;
        int baseRed = Color.red(simpleBaseColor) - Color.red(simpleColor);
        int baseGreen = Color.green(simpleBaseColor) - Color.green(simpleColor);
        int baseBlue = Color.blue(simpleBaseColor) - Color.blue(simpleColor);
        double value = Math.sqrt(baseRed * baseRed + baseGreen * baseGreen + baseBlue * baseBlue);
        if (value < 180.0) {
            return true;
        }
        return false;
    }

    /**
     * 获取颜色
     *
     * @param viewGroupSource
     * @return
     */
    private static int findColor(ViewGroup viewGroupSource) {
        int color = Color.TRANSPARENT;
        LinkedList<ViewGroup> viewGroups = new LinkedList<>();
        viewGroups.add(viewGroupSource);
        while (viewGroups.size() > 0) {
            ViewGroup viewGroup1 = viewGroups.getFirst();
            for (int i = 0; i < viewGroup1.getChildCount(); i++) {
                if (viewGroup1.getChildAt(i) instanceof ViewGroup) {
                    viewGroups.add((ViewGroup) viewGroup1.getChildAt(i));
                } else if (viewGroup1.getChildAt(i) instanceof TextView) {
                    if (((TextView) viewGroup1.getChildAt(i)).getCurrentTextColor() != -1) {
                        color = ((TextView) viewGroup1.getChildAt(i)).getCurrentTextColor();
                    }
                }
            }
            viewGroups.remove(viewGroup1);
        }
        return color;
    }

    /**
     * 获取资源Id
     *
     * @param context
     * @param type
     * @param name
     * @return
     */
    public static int getResourceId(Context context, String type, String name) {
        try {
            Class<?> clazz = Class.forName(context.getApplicationContext().getPackageName() + ".R$" + type);
            Field field = clazz.getDeclaredField(name);
            return (Integer) field.get(null);
        } catch (Exception globalException) {
            return -1;
        }
    }

    /**
     * 获取bitmap
     *
     * @param res
     * @param resId
     * @return
     */
    public static Bitmap getBitmapFromResId(Resources res, int resId) {
        return getBitmapFromDrawable(res.getDrawable(resId));
    }

    /**
     * 获取bitmap
     *
     * @param d
     * @return
     */
    public static Bitmap getBitmapFromDrawable(Drawable d) {
        if (d instanceof BitmapDrawable) return ((BitmapDrawable) d).getBitmap();
        Bitmap bm = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        d.setBounds(0, 0, c.getWidth(), c.getHeight());
        d.draw(c);
        return bm;
    }

}

在布局中使用 xml文件

<RelativeLayout
    android:id="@+id/rl_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_below="@+id/select_skin_from_center"
    android:layout_marginLeft="20dp"
    android:layout_marginRight="20dp"
    android:layout_marginBottom="50dp">

    <com.xxx.lock.LockView
        android:id="@+id/pattern_lock_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:gravity="center_horizontal"
        android:padding="12dp"
        component:gridError="@drawable/gesture_pattern_error"
        component:gridFocused="@drawable/gesture_pattern_selected"
        component:gridNormal="@drawable/gesture_pattern_normal" />
</RelativeLayout>