自定義VIew之APP常用手勢密碼對程序加鎖,解鎖控件

  最近公司有個需求,需要給app設置一個手勢密碼,增強安全性,所以呢我就想着自定義一個控件來實現這個功能。以下是一個demo的界面,我不是搞UI的,只是看下效果,哈哈:

ezgif.com-video-to-gif(2).gif

  我呢在主界面將之前設置的手勢數據清除掉了,不然當我們第二次運行的時候,就不會顯示設置手勢密碼的界面。在第一個界面設置一個手勢,跳到第二個界面,去驗證剛剛設置的手勢,在第二個界面成功驗證手勢密碼後就可以到第三個界面了。

  下面來講一下思路,具體代碼的話後面會給出整個自定義View的實現,還會給出整個demo的下載鏈接地址,如有興趣,自己去下載demo看。此demo僅供學習使用哦!千萬不要放到項目中,千萬不要放到項目中,千萬不要放到項目中,重要的事情說三遍。

  實現的關鍵主要分成三部分,第一部分就是繪製的部分,我們要將整個View分成幾個不同的部分,第一個是手勢操作的區域,就是可以滑動設置和解鎖的那個區域,這個區域是要加上事件監聽的,還有信息的展示區域,這個通過drawText來實現,把當前的信息畫在畫布的頂端,在設置手勢的時候還有一個設置手勢的簡單的預覽圖,這個是根據當前的手勢設置情況來顯示已設置手勢包含幾個點;第二部分就是對每個圖標的位置以及線條的起始位置和最後位置的計算,我們需要進行細緻的計算來得到每個”按鈕”的位置,然後將數據存儲起來,通過重繪來講這些數據展示到畫布上;第三部分就是事件監聽,這個也是比較重要的一環,我們需要對ACTION_DOWN,ACTION_MOVE以及ACTION_UP三個事件進行監聽,在ACTION_DOWN和ACTION_MOVE事件中完成動態的效果的繪製,在ACTION_UP事件中進行數據的處理,判斷手勢識別的結果。

  先說下手勢的設置階段,我的想法是在ACTION_DOWN和ACTION_MOVE事件中,將正方形的觸控區域分成九個小的部分,也就是一個3X3的二維數組,分別放置九張空心的圖片,當手指滑到每一個小的正方形中的時候,通過對指尖的位置的判斷,將其轉化成這個點在二維數組中的位置,比如下圖:

模型圖

 當手指在第一個區域的時候,也就是0<=event.getX()<=40&&0<=event.getY()<=40的這個矩形的時候,我們就將其轉換成(0,0)這個點,具體的就是:
int x = event.getX()%40;
int y = event.getY()%40;

 然後將這個點加到集合中去,說明手勢中已經含有這一點了,再一次將後面觸控到的點加到集合中去就OK了,至於手勢的順序我們根據集合的順序即可,還有一點很重要,我們需要在添加之前判集合中是否已經包含這個點,不要重複的添加點到集合中去,因爲ACTION_MOVE事件的很快的,可能在你手指只動了很小的距離的時候就已經往集合中添加了若干個點了已經。將點加到集合中去之後,我們實時的去對整個View進行重繪,將集合中的所有點的背景圖換成實心的就OK了。還有一點就是那個在手指滑動的時候,會有一個效果就是:
一條線跟隨手指
  這個是怎麼實現的呢,我們在ACTION_MOVE事件中,實時的取集合中的最後一個數據,然後通過drawLine方法畫線就行了,線條的起點取集合的最後一個值,終點就是手指的當前的位置。

lastGestrue = listDatas.get(listDatas.size() - 1);
    canvas.drawLine((float) (mLineHeight * (lastGestrue.getX() + 0.5)), (float) (mLineHeight * (lastGestrue.getY() + 0.5) + panelHeight), currX, currY, mPaint);

  在最後的ACTION_UP的事件中,將集合數據存起來,然後將整個頁面的內容清楚掉,重複以上操作,只不過是這次操作是將數據存到另一個集合中,在第二次設置時的ACTION_UP事件中,將兩個兩個集合的數據進行比對,如果數據一樣,則手勢密碼設置成功,將已設置好的手勢密碼的數據存儲到SharedPrefference中去,否則,重新整個操作即可。

  然後是手勢密碼的驗證階段,我們在之前的操作中已經完成了手勢密碼的設置,也就是說通過之前的操作我們已經將手勢密碼的信息存儲在了SharedPrefference之中,在驗證階段,就是取出之前存入的數據與當前用戶輸入的手勢進行比對,如果一致則驗證成功,否則驗證失敗。在驗證的過程中,還有一個點,就是在用戶錯誤嘗試次數達到一定次數的時候進行整個界面的事件攔截,效果就是,錯誤次數達到5次了顯示30秒內不能再輸入手勢密碼了,在這裏面我們需要啓動一個Timer來進行剩餘時間的實時反饋,定義一個最大時間maxTime 30秒,啓動Timer 進行maxTime–並顯示就行了。

  最後就是定義一個接口,來實現當手勢設置成功以及手勢驗證成功後的回調操作,給調用者自定義屬於自己的操作即可。以下是代碼實現:

GestureView.java
package com.example.cretin.secondtest.views;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.example.cretin.secondtest.R;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Created by cretin on 16/7/1.
 */
public class GestureView extends View {
    public static final int STATE_REGISTER = 101;
    public static final int STATE_LOGIN = 100;
    private static int panelHeight = 300;
    private int mPanelWidth;
    private Bitmap selectedBitmap;
    private Bitmap unSelectedBitmap;
    private Bitmap selectedBitmapSmall;
    private Bitmap unSelectedBitmapSmall;
    private float pieceWidth;
    private float pieceWidthSmall;
    private float mLineHeight;
    private Paint mPaint;
    private float currX;
    private float currY;
    private List<GestureBean> listDatas;
    private List<GestureBean> listDatasCopy;
    private GestureBean lastGestrue = null;
    private int tryCount;
    private Vibrator vibrate;

    private Timer mTimer;
    private TimerTask mTimerTask;

    private boolean mError;
    private String message = "請繪製手勢";
    //失敗嘗試次數
    private int tempCount = 5;

    //剩餘等待時間
    private int leftTime = 30;

    //記錄是否嘗試次數超過限制
    private boolean mTimeout;

    private int minPointNums = 4;

    //設置一個參數記錄當前是出於初始化階段還是使用階段
    private int stateFlag = STATE_LOGIN;

    private GestureCallBack gestureCallBack;

    private Context mContext;

    Handler handler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            leftTime--;
            Log.e("HHHHHHHH", "lefttime" + leftTime);
            if (leftTime == 0) {
                if (mTimer != null)
                    mTimerTask.cancel();
                mTimeout = false;
                message = "請繪製手勢";
                mError = false;
                invalidate();
                reset();
                return;
            }
            message = "嘗試次數達到最大," + leftTime + "s後重試";
            mError = true;
            invalidate();
        }

    };

    public GestureView(Context context) {
        super(context);
        init(context);
    }

    public GestureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public GestureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public GestureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        mContext = context;

        try {
            gestureCallBack = (GestureCallBack) context;
        } catch (final ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement GestureCallBack");
        }

        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#7ec059"));
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(20);
        mPaint.setStyle(Paint.Style.STROKE);
        setBackgroundResource(R.mipmap.bg_gesture);
        listDatas = new ArrayList<>();
        listDatasCopy = new ArrayList<>();

        selectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected);
        unSelectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected);
        selectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected);
        unSelectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected);

        //獲取振動器
        vibrate = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
        mTimer = new Timer();
        stateFlag = getState();
        if (stateFlag == STATE_REGISTER) {
            message = "請設置手勢密碼";
        } else {
            message = "請輸入手勢密碼以解鎖";
        }
    }

    public void setGestureCallBack(GestureCallBack gestureCallBack) {
        this.gestureCallBack = gestureCallBack;
    }

    //重置一些操作
    private void reset() {
        leftTime = 30;
        tempCount = 5;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int width = Math.min(widthSize, heightSize);
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            width = heightSize;
        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
            width = widthSize;
        }

        mLineHeight = width / 3;
        setMeasuredDimension(width, width + panelHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPanelWidth = Math.min(w, h);

        pieceWidth = (int) (mLineHeight * 0.6f);
        pieceWidthSmall = (int) (mLineHeight * 0.15f);
        selectedBitmap = Bitmap.createScaledBitmap(selectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        unSelectedBitmap = Bitmap.createScaledBitmap(unSelectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        selectedBitmapSmall = Bitmap.createScaledBitmap(selectedBitmap, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
        unSelectedBitmapSmall = Bitmap.createScaledBitmap(unSelectedBitmap, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
    }

    private boolean saveState() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", stateFlag);
        return edit.commit();
    }

    private int getState() {
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        return mSharedPreference.getInt("state", STATE_REGISTER);
    }

    public boolean clearCache() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", STATE_REGISTER);
        stateFlag = STATE_REGISTER;
        invalidate();
        return edit.commit();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        GestureBean firstGestrue = null;
        GestureBean currGestrue = null;

        if (stateFlag == STATE_REGISTER) {
            //繪製上面的提示點
            drawTipsPoint(canvas);
        } else {
            drawTipsText(canvas);
        }

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                canvas.drawBitmap(unSelectedBitmap, (float) (mLineHeight * (j + 0.5) - pieceWidth / 2), (float) (mLineHeight * (i + 0.5) - pieceWidth / 2 + panelHeight), mPaint);
            }
        }
        if (!listDatas.isEmpty()) {
            firstGestrue = listDatas.get(0);
            for (int i = 1; i < listDatas.size(); i++) {
                currGestrue = listDatas.get(i);
                canvas.drawLine((float) (mLineHeight * (firstGestrue.getX() + 0.5)), (float) (mLineHeight * (firstGestrue.getY() + 0.5) + panelHeight), (float) (mLineHeight * (currGestrue.getX() + 0.5)), (float) (mLineHeight * (currGestrue.getY() + 0.5) + panelHeight), mPaint);
                firstGestrue = currGestrue;
            }

            lastGestrue = listDatas.get(listDatas.size() - 1);
            canvas.drawLine((float) (mLineHeight * (lastGestrue.getX() + 0.5)), (float) (mLineHeight * (lastGestrue.getY() + 0.5) + panelHeight), currX, currY, mPaint);
            for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmap, (float) (mLineHeight * (bean.getX() + 0.5) - pieceWidth / 2), (float) (mLineHeight * (bean.getY() + 0.5) + panelHeight - pieceWidth / 2), mPaint);
            }
        }
    }

    //繪製提示語
    private void drawTipsText(Canvas canvas) {
        float widthMiddleX = mPanelWidth / 2;
        mPaint.setStrokeWidth(2);
        mPaint.setStyle(Paint.Style.FILL);
        //設置文字的大小
        mPaint.setTextSize(50);
        int widthStr1 = (int) mPaint.measureText(message);
        if (mError) {
            mPaint.setColor(Color.parseColor("#FF0000"));
        } else {
            mPaint.setColor(Color.parseColor("#FFFFFF"));
        }
        float baseX = widthMiddleX - widthStr1 / 2;
        float baseY = panelHeight / 2 + 50;
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
        float offY = fontTotalHeight / 2 - fontMetrics.bottom - 30;
        float newY = baseY + offY;
        canvas.drawText(message, baseX, newY, mPaint);
        mPaint.setColor(Color.parseColor("#7ec059"));
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(20);
    }

    private void drawMessage(Canvas canvas, String message, boolean errorFlag) {
        float widthMiddleX = mPanelWidth / 2;
        float firstY = (float) (panelHeight / 2 - pieceWidthSmall / 2 + pieceWidthSmall * 1.25 + 90);
        mPaint.setStrokeWidth(2);
        mPaint.setStyle(Paint.Style.FILL);
        //設置文字的大小
        mPaint.setTextSize(50);
        int widthStr1 = (int) mPaint.measureText(message);
        if (errorFlag) {
            mPaint.setColor(Color.parseColor("#FF0000"));
        } else {
            mPaint.setColor(Color.parseColor("#FFFFFF"));
        }
        float baseX = widthMiddleX - widthStr1 / 2;
        float baseY = firstY + 40;
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
        float offY = fontTotalHeight / 2 - fontMetrics.bottom - 30;
        float newY = baseY + offY;
        canvas.drawText(message, baseX, newY, mPaint);
        mPaint.setColor(Color.parseColor("#7ec059"));
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(20);
    }

    //繪製提示點
    private void drawTipsPoint(Canvas canvas) {
        float widthMiddleX = mPanelWidth / 2;
        float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;
        float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                canvas.drawBitmap(unSelectedBitmapSmall, (float) (firstX + j * (pieceWidthSmall * 1.25)), (float) (firstY + i * (pieceWidthSmall * 1.25)), mPaint);
            }
        }

        if (listDatasCopy != null && !listDatasCopy.isEmpty()) {
            for (GestureBean bean : listDatasCopy) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        } else if (listDatas != null && !listDatas.isEmpty()) {
            for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        }

        drawMessage(canvas, message, mError);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mTimeout) {
            return true;
        }
        if (event.getY() >= 0) {
            int x = (int) ((event.getY() - panelHeight) / mLineHeight);
            int y = (int) (event.getX() / mLineHeight);
            currX = event.getX();
            currY = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastGestrue = null;
                    if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
                        if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
                                currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                                vibrate.vibrate(50);//震半秒鐘
                            }
                        }
                    }
                    invalidate();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
                        //縮小響應範圍 在此處需要注意的是 x跟currX在物理方向上是反的哦
                        if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
                                currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                                vibrate.vibrate(50);//震半秒鐘
                            }
                        }
                    }
                    invalidate();
                    break;
                case MotionEvent.ACTION_UP:
                    if (lastGestrue != null) {
                        currX = (float) ((lastGestrue.getX() + 0.5) * mLineHeight);
                        currY = (float) ((lastGestrue.getY() + 0.5) * mLineHeight);
                    }
                    if (stateFlag == STATE_LOGIN) {
                        if (listDatas.equals(loadSharedPrefferenceData())) {
                            mError = false;
                            message = "手勢驗證成功";
                            postListener(true);
                            invalidate();
                            listDatas.clear();
                            return true;
                        } else {
                            if (--tempCount == 0) {
                                mError = true;
                                message = "嘗試次數達到最大,30s後重試";
                                mTimeout = true;
                                listDatas.clear();
                                mTimerTask = new MyTimerTask(handler);
                                mTimer.schedule(mTimerTask, 0, 1000);
                                invalidate();
                                return true;
                            }
                            mError = true;
                            message = "手勢錯誤,還可以再輸入" + (tempCount) + "次";
                            listDatas.clear();
                        }
                    } else if (stateFlag == STATE_REGISTER) {
                        if (listDatasCopy == null || listDatasCopy.isEmpty()) {
                            if (listDatas.size() < minPointNums) {
                                listDatas.clear();
                                mError = true;
                                message = "點數不能小於" + minPointNums + "個";
                                invalidate();
                                return true;
                            }
                            listDatasCopy.addAll(listDatas);
                            saveToSharedPrefference(listDatas);
                            listDatas.clear();
                            mError = false;
                            message = "請再一次繪製";
                        } else {
                            loadSharedPrefferenceData();
                            if (listDatas.equals(listDatasCopy)) {
                                mError = false;
                                message = "手勢設置成功";
                                stateFlag = STATE_LOGIN;
                                postListener(true);
                                saveState();
                            } else {
                                mError = true;
                                message = "兩次手勢繪製不一致,請重新設置";
                            }
                            listDatas.clear();
                            listDatasCopy.clear();
                            invalidate();
                            return true;
                        }
                    }
                    invalidate();
                    break;
            }
        }
        return true;
    }

    //給接口傳遞數據
    private void postListener(boolean success) {
        if (gestureCallBack != null) {
            gestureCallBack.gestureVerifySuccessListener(stateFlag, listDatas, message, success);
        }
    }

    private boolean saveToSharedPrefference(List<GestureBean> data) {
        SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("data_size", data.size()); /*sKey is an array*/
        for (int i = 0; i < data.size(); i++) {
            edit.remove("data_" + i);
            edit.putString("data_" + i, data.get(i).getX() + " " + data.get(i).getY());
        }

        return edit.commit();
    }

    public List<GestureBean> loadSharedPrefferenceData() {
        List<GestureBean> list = new ArrayList<>();
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        int size = mSharedPreference.getInt("data_size", 0);

        for (int i = 0; i < size; i++) {
            String str = mSharedPreference.getString("data_" + i, "0 0");
            list.add(new GestureBean(Integer.parseInt(str.split(" ")[0]), Integer.parseInt(str.split(" ")[1])));
        }
        return list;
    }

    class MyTimerTask extends TimerTask {
        Handler handler;

        public MyTimerTask(Handler handler) {
            this.handler = handler;
        }

        @Override
        public void run() {
            handler.sendMessage(handler.obtainMessage());
        }

    }

    public class GestureBean {
        private int x;
        private int y;

        @Override
        public String toString() {
            return "GestureBean{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }

        public GestureBean(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        @Override
        public boolean equals(Object o) {
            return ((GestureBean) o).getX() == x && ((GestureBean) o).getY() == y;
        }
    }

    public int getMinPointNums() {
        return minPointNums;
    }

    public void setMinPointNums(int minPointNums) {
        if (minPointNums <= 3)
            this.minPointNums = 3;
        if (minPointNums >= 9)
            this.minPointNums = 9;
    }

    public interface GestureCallBack {
        void gestureVerifySuccessListener(int stateFlag, List<GestureBean> data, String message, boolean success);
    }
}

使用的時候:
添加所需要的圖片資源,在佈局文件中直接調用即可。
最後附上整個demo的下載鏈接,請注意,不要積分就可以下載哦,重在分享,哥不稀罕那點積分:
http://download.csdn.net/detail/u010998327/9567416
github地址:
https://github.com/MZCretin/GestureViewDemo

發佈了61 篇原創文章 · 獲贊 65 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章