廢話不多說了,先給大家展示下自定義view效果圖,如果大家覺得還不錯的話,請繼續往下閱讀。
怎么樣,這種驗證碼是不是很常見呢,下面我們就自己動手實現這種效果,自己動手,豐衣足食,哈哈~
一、 自定義view的步驟
自定義view一直被認為android進階通向高手的必經之路,其實自定義view好簡單,自定義view真正難的是如何繪制出高難度的圖形,這需要有好的數學功底(后悔沒有好好學數學了~),因為繪制圖形經常要計算坐標點及類似的幾何變換等等。自定義view通常只需要以下幾個步驟:
寫一個類繼承View類;
重新View的構造方法;
測量View的大小,也就是重寫onMeasure()方法;
重新onDraw()方法。
其中第三步不是必須的,只有當系統無法確定自定義的view的大小的時候需要我們自己重寫onMeasure()方法來完成自定義view大小的測量,因為如果用戶(程序員)在使用我們的自定義view的時候沒有指定其精確大?。▽挾然蚋叨龋?,如:布局文件中layout_width或layout_heigth屬性值為wrap_content而不是match_parent或某個精確的值,那么系統就不知道我們自定義view在onDraw()中繪制的圖形的大小,所以通常要讓我們自定義view支持wrap_content那么我們就必須重寫onMeasure方法來告訴系統我們要繪制的view的大?。▽挾群透叨龋?/p>
還有,如果我們自定義view需要一些特殊的屬性,那么我們還需要自定義屬性,這篇文章將會涉及到自定義屬性和上面的四個步驟的內容。
二、 自定義view的實現
要實現這種驗證碼控件,我們需要先分析一下它要怎么實現。通過看上面的效果圖,我們可以知道要實現這種效果,首先需要在繪制驗證碼字符串,即圖中的文本部分,然后繪制一些干擾點,再就是繪制干擾線了,分析完畢。下面我們根據分析結果一步步實現這種效果。
1. 繼承View,重寫構造方法
寫一個類繼承View,然后重新它的構造方法
/*** Created by lt on 2016/3/2.*/public class ValidationCode extends View{/*** 在java代碼中創建view的時候調用,即new* @param context*/public ValidationCode(Context context) {this(context,null);}/*** 在xml布局文件中使用view但沒有指定style的時候調用* @param context* @param attrs*/public ValidationCode(Context context, AttributeSet attrs) {this(context, attrs, 0);}/*** 在xml布局文件中使用view并指定style的時候調用* @param context* @param attrs* @param defStyleAttr*/public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// 做一些初始化工作init();}}
View有三個構造方法,一般的做法都是讓一個參數和兩個參數的構造方法調用三個構造參數的方法,這三個構造方法的調用情況看方法上面的注釋。在這個構造方法里面我們先做一些初始化隨機驗證碼字符串,畫筆等工作:
/*** 初始化一些數據*/private void init() {// 生成隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);// 初始化文字畫筆mTextPaint = new Paint();mTextPaint.setStrokeWidth(3); // 畫筆大小為3mTextPaint.setTextSize(mTextSize); // 設置文字大小// 初始化干擾點畫筆mPointPaint = new Paint();mPointPaint.setStrokeWidth(6);mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設置斷點處為圓形// 初始化干擾線畫筆mPathPaint = new Paint();mPathPaint.setStrokeWidth(5);mPathPaint.setColor(Color.GRAY);mPathPaint.setStyle(Paint.Style.STROKE); // 設置畫筆為空心mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設置斷點處為圓形// 取得驗證碼字符串顯示的寬度值mTextWidth = mTextPaint.measureText(mCodeString);}
到這里,我們就完成了自定義View步驟中的前面的兩小步了,接下來就是完成第三步,即重寫onMeasure()進行我們自定義view大?。▽捀撸┑臏y量了:
2. 重寫onMeasure(),完成View大小的測量
/*** 要像layout_width和layout_height屬性支持wrap_content就必須重新這個方法* @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 分別測量控件的寬度和高度,基本為模板方法int measureWidth = measureWidth(widthMeasureSpec);int measureHeight = measureHeight(heightMeasureSpec);// 其實這個方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);// 將測量出來的寬高設置進去完成測量setMeasuredDimension(measureWidth, measureHeight);}
測量寬度的方法:
/*** 測量寬度* @param widthMeasureSpec*/private int measureWidth(int widthMeasureSpec) {int result = (int) (mTextWidth*1.8f);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);if(widthMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局文件中layout_width或layout_height一般為精確的值或match_parentresult = widthSize; // 既然是精確模式,那么直接返回測量的寬度即可}else{if(widthMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局文件中layout_width或layout_height一般為wrap_contentresult = Math.min(result,widthSize);}}return result;}
測量高度的方法:
/*** 測量高度* @param heightMeasureSpec*/private int measureHeight(int heightMeasureSpec) {int result = (int) (mTextWidth/1.6f);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);if(heightMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局文件中layout_width或layout_height一般為精確的值或match_parentresult = heightSize; // 既然是精確模式,那么直接返回測量的寬度即可}else{if(heightMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局文件中layout_width或layout_height一般為wrap_contentresult = Math.min(result,heightSize);}}return result;}
說明:其實onMeasure()方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);將測量出來的寬高設置進去完成測量,而我們要做的就是測量得到寬度和高度的值,測量寬度和高度的方法最重要的就是得到當用戶(程序員)沒有給我們的控件指定精確的值(具體數值或match_parent)時合適的寬度和高度,所以,以上測量寬度和高度的方法基本上是一個模板方法,要做的就是得到result的一個合適的值,這里我們無需關注給result的那個值,因為這個值根據控件算出來的一個合適的值(也許不是很合適)。
完成了控件的測量,那么接下來我們還要完成控件的繪制這一大步,也就是自定義view的核心的一步重寫onDraw()方法繪制圖形。
3. 重寫onDraw(),繪制圖形
根據我們上面的分析,我們需要繪制驗證碼文本字符串,干擾點,干擾線。由于干擾點和干擾線需要坐標和路徑來繪制, 所以在繪制之前先做一些初始化隨機干擾點坐標和干擾線路徑:
private void initData() {// 獲取控件的寬和高,此時已經測量完成mHeight = getHeight();mWidth = getWidth();mPoints.clear();// 生成干擾點坐標for(int i=0;i<150;i++){PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);mPoints.add(pointF);}mPaths.clear();// 生成干擾線坐標for(int i=0;i<2;i++){Path path = new Path();int startX = mRandom.nextInt(mWidth/3)+10;int startY = mRandom.nextInt(mHeight/3)+10;int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;path.moveTo(startX,startY);path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);mPaths.add(path);}}
有了這些數據之后,我們可以開始繪制圖形了。
(1)繪制驗證碼文本字符串
由于驗證碼文本字符串是隨機生成的,所以我們需要利用代碼來隨機生成這種隨機驗證碼:
/*** java生成隨機數字和字母組合* @param length[生成隨機數的長度]* @return*/public static String getCharAndNumr(int length) {String val = "";Random random = new Random();for (int i = 0; i < length; i++) {// 輸出字母還是數字String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";// 字符串if ("char".equalsIgnoreCase(charOrNum)) {// 取得大寫字母還是小寫字母int choice = random.nextInt(2) % 2 == 0 ? 65 : 97;val += (char) (choice + random.nextInt(26));} else if ("num".equalsIgnoreCase(charOrNum)) { // 數字val += String.valueOf(random.nextInt(10));}}return val;}
這種代碼是java基礎,相信大家都看得懂,看不懂也沒關系,這種代碼網上隨便一搜就有,其實我也是直接從網上搜的,嘿嘿~。
android的2D圖形api canvas提供了drawXXX()方法來完成各種圖形的繪制,其中就有drawText()方法來繪制文本,同時還有drawPosText()在給定的坐標點上繪制文本,drawTextOnPath()在給定途徑上繪制圖形。仔細觀察上面的效果圖,發現文本有的不是水平的,即有的被傾斜了,這就可以給我們的驗證碼提升一定的識別難度,要實現文字傾斜效果,我們可以通過drawTextOnPath()在給定路徑繪制文本達到傾斜效果,然而這種方法實現比較困難(坐標點和路徑難以計算),所以,我們可以通過canvas提供的位置變換方法rorate()結合drawText()實現文本傾斜效果。
int length = mCodeString.length();float charLength = mTextWidth/length;for(int i=1;i<=length;i++){int offsetDegree = mRandom.nextInt(15);// 這里只會產生0和1,如果是1那么正旋轉正角度,否則旋轉負角度offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;canvas.save();canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);// 給畫筆設置隨機顏色,+20是為了去除一些邊界值mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);canvas.restore();}
這段代碼通過for循環分別繪制驗證碼字符串中的每個字符,每繪制一個字符都將畫布旋轉一個隨機的正負角度,然后通過drawText()方法繪制字符,每個字符的繪制起點坐標根據字符的長度和位置不同而不同,這個自己計算,這里也許也不是很合適。要注意的是,每次對畫布canvas進行位置變換的時候都要先調用canvas.save()方法保存好之前繪制的圖形,繪制結束后調用canvas.restore()恢復畫布的位置,以便下次繪制圖形的時候不會由于之前畫布的位置變化而受影響。
(2)繪制干擾點
// 產生干擾效果1 -- 干擾點for(PointF pointF : mPoints){mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);canvas.drawPoint(pointF.x,pointF.y,mPointPaint);}
給干擾點畫筆設置隨機顏色,然后根據隨機產生的點的坐標利用canvas.drawPoint()繪制點。
(3)繪制干擾線
// 產生干擾效果2 -- 干擾線for(Path path : mPaths){mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawPath(path, mPathPaint);}
給干擾線畫筆設置隨機顏色,然后根據隨機產生路徑利用canvas.drawPath()繪制貝塞爾曲線,從而繪制出干擾線。
4. 重寫onTouchEvent,定制View事件
這里做這一步是為了實現當我們點擊我們的自定義View的時候,完成一些操作,即定制View事件。這里,我們需要當用戶點擊驗證碼控件的時候,改變驗證碼的文本字符串。
@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:// 重新生成隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);invalidate();break;default:break;}return super.onTouchEvent(event);}
OK,到這里我們的這個自定義View就基本完成了,可能大家會問,這個自定義View是不是擴展性太差了,定制性太低了,說好的自定義屬性呢?跑哪里去了。不要急,下面我們就來自定義我們自己View的屬性,自定義屬性。
5. 自定義屬性,提高自定義View的可定制性
(1)在資源文件attrs.xml文件中定義我們的屬性(集)
<?xml version="1.0" encoding="utf-8"?><resources><declare-styleable name="IndentifyingCode"><attr name="codeCount" format="integer|reference"></attr><attr name="textSize" format="dimension"></attr></declare-styleable></resources>
說明:
在attrs.xml文件中的attr節點中定義我們的屬性,定義屬性需要name屬性表示我們的屬性值,同時需要format屬性表示屬性值的格式,其格式有很多種,如果屬性值可以使多種格式,那么格式間用”|”分開;
declare-styleable節點用來定義我們自定義屬性集,其name屬性指定了該屬性集的名稱,可以任意,但一般為自定義控件的名稱;
如果屬性已經定義了(如layout_width),那么可以直接引用該屬性,不要指定格式了。
(2)在布局文件中引用自定義屬性,注意需要引入命名空間
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:lt="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><com.lt.identifyingcode.ValidationCodeandroid:id="@+id/validationCode"android:layout_width="wrap_content"android:layout_centerInParent="true"lt:textSize="25sp"android:background="@android:color/darker_gray"android:layout_height="wrap_content"/></RelativeLayout>
引入命名空間在現在只需要添加xmlns:lt="http://schemas.android.com/apk/res-auto"即可(lt換成你自己的命名空間名稱),而在以前引入命名空間方式為xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01",res后面的包路徑指的是項目的package`
(3)在構造方法中獲取自定義屬性的值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 獲取布局中驗證碼位數屬性值,默認為5個// 獲取布局中驗證碼文字的大小,默認為20spmTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));// 一個好的習慣是用完資源要記得回收,就想打開數據庫和IO流用完后要記得關閉一樣typedArray.recycle();
OK,自定義屬性也完成了,值也獲取到了,那么我們只需要將定制的屬性值在我們onDraw()繪制的時候使用到就行了,自定義屬性就是這么簡單~,看到這里,也許有點混亂了,看一下完整代碼整理一下。
package com.lt.identifyingcode;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PointF;import android.util.AttributeSet;import android.util.TypedValue;import android.view.MotionEvent;import android.view.View;import java.util.ArrayList;import java.util.Random;/*** Created by lt on 2016/3/2.*/public class ValidationCode extends View{/*** 控件的寬度*/private int mWidth;/*** 控件的高度*/private int mHeight;/*** 驗證碼文本畫筆*/private Paint mTextPaint; // 文本畫筆/*** 干擾點坐標的集合*/private ArrayList<PointF> mPoints = new ArrayList<PointF>();private Random mRandom = new Random();;/*** 干擾點畫筆*/private Paint mPointPaint;/*** 繪制貝塞爾曲線的路徑集合*/private ArrayList<Path> mPaths = new ArrayList<Path>();/*** 干擾線畫筆*/private Paint mPathPaint;/*** 驗證碼字符串*/private String mCodeString;/*** 驗證碼的位數*/private int mCodeCount;/*** 驗證碼字符的大小*/private float mTextSize;/*** 驗證碼字符串的顯示寬度*/private float mTextWidth;/*** 在java代碼中創建view的時候調用,即new* @param context*/public ValidationCode(Context context) {this(context,null);}/*** 在xml布局文件中使用view但沒有指定style的時候調用* @param context* @param attrs*/public ValidationCode(Context context, AttributeSet attrs) {this(context, attrs, 0);}/*** 在xml布局文件中使用view并指定style的時候調用* @param context* @param attrs* @param defStyleAttr*/public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);getAttrValues(context, attrs);// 做一些初始化工作init();}/*** 獲取布局文件中的值* @param context*/private void getAttrValues(Context context,AttributeSet attrs) {TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 獲取布局中驗證碼位數屬性值,默認為5個// 獲取布局中驗證碼文字的大小,默認為20spmTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));// 一個好的習慣是用完資源要記得回收,就想打開數據庫和IO流用完后要記得關閉一樣typedArray.recycle();}/*** 要像layout_width和layout_height屬性支持wrap_content就必須重新這個方法* @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 分別測量控件的寬度和高度,基本為模板方法int measureWidth = measureWidth(widthMeasureSpec);int measureHeight = measureHeight(heightMeasureSpec);// 其實這個方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);// 將測量出來的寬高設置進去完成測量setMeasuredDimension(measureWidth, measureHeight);}@Overrideprotected void onDraw(Canvas canvas) {// 初始化數據initData();int length = mCodeString.length();float charLength = mTextWidth/length;for(int i=1;i<=length;i++){int offsetDegree = mRandom.nextInt(15);// 這里只會產生0和1,如果是1那么正旋轉正角度,否則旋轉負角度offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;canvas.save();canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);// 給畫筆設置隨機顏色mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);canvas.restore();}// 產生干擾效果1 -- 干擾點for(PointF pointF : mPoints){mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);canvas.drawPoint(pointF.x,pointF.y,mPointPaint);}// 產生干擾效果2 -- 干擾線for(Path path : mPaths){mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawPath(path, mPathPaint);}}private void initData() {// 獲取控件的寬和高,此時已經測量完成mHeight = getHeight();mWidth = getWidth();mPoints.clear();// 生成干擾點坐標for(int i=0;i<150;i++){PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);mPoints.add(pointF);}mPaths.clear();// 生成干擾線坐標for(int i=0;i<2;i++){Path path = new Path();int startX = mRandom.nextInt(mWidth/3)+10;int startY = mRandom.nextInt(mHeight/3)+10;int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;path.moveTo(startX,startY);path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);mPaths.add(path);}}/*** 初始化一些數據*/private void init() {// 生成隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);// 初始化文字畫筆mTextPaint = new Paint();mTextPaint.setStrokeWidth(3); // 畫筆大小為3mTextPaint.setTextSize(mTextSize); // 設置文字大小// 初始化干擾點畫筆mPointPaint = new Paint();mPointPaint.setStrokeWidth(6);mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設置斷點處為圓形// 初始化干擾線畫筆mPathPaint = new Paint();mPathPaint.setStrokeWidth(5);mPathPaint.setColor(Color.GRAY);mPathPaint.setStyle(Paint.Style.STROKE); // 設置畫筆為空心mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設置斷點處為圓形// 取得驗證碼字符串顯示的寬度值mTextWidth = mTextPaint.measureText(mCodeString);}/*** java生成隨機數字和字母組合* @param length[生成隨機數的長度]* @return*/public static String getCharAndNumr(int length) {String val = "";Random random = new Random();for (int i = 0; i < length; i++) {// 輸出字母還是數字String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";// 字符串if ("char".equalsIgnoreCase(charOrNum)) {// 取得大寫字母還是小寫字母int choice = random.nextInt(2) % 2 == 0 ? 65 : 97;val += (char) (choice + random.nextInt(26));} else if ("num".equalsIgnoreCase(charOrNum)) { // 數字val += String.valueOf(random.nextInt(10));}}return val;}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:// 重新生成隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);invalidate();break;default:break;}return super.onTouchEvent(event);}/*** 測量寬度* @param widthMeasureSpec*/private int measureWidth(int widthMeasureSpec) {int result = (int) (mTextWidth*1.8f);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);if(widthMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局文件中layout_width或layout_height一般為精確的值或match_parentresult = widthSize; // 既然是精確模式,那么直接返回測量的寬度即可}else{if(widthMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局文件中layout_width或layout_height一般為wrap_contentresult = Math.min(result,widthSize);}}return result;}/*** 測量高度* @param heightMeasureSpec*/private int measureHeight(int heightMeasureSpec) {int result = (int) (mTextWidth/1.6f);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);if(heightMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局文件中layout_width或layout_height一般為精確的值或match_parentresult = heightSize; // 既然是精確模式,那么直接返回測量的寬度即可}else{if(heightMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局文件中layout_width或layout_height一般為wrap_contentresult = Math.min(result,heightSize);}}return result;}/*** 獲取驗證碼字符串,進行匹配的時候只需要字符串比較即可(具體比較規則自己決定)* @return 驗證碼字符串*/public String getCodeString() {return mCodeString;}}
總結:這里與其說自定義View到不如說是繪制圖形,關鍵在于坐標點的計算,這里在計算坐標上也許不太好,以上是給大家分享Android自定義view制作絢麗的驗證碼,希望對大家有所幫助!大家有什么好的思路或者建議希望可以留言告訴我,感激不盡~。
新聞熱點
疑難解答