Android自定義view之下載控件,ProgressBar


請尊重別人的勞動成果,轉發文章請註明出處


概述


在開發過程中我們總會遇到一些不同於安卓自帶的控件,業內稱之爲自定義控件,一直沒有深入瞭解自定義VIEW,總覺得好像很厲害的樣子,最近公司業務需求(做一個APK文件的下載)需要個性化的展示下載進度條。於是嘗試着寫一個下載進度條的自定義控件

爲了不浪費大家的時間,先上效果圖,對於趕時間的哥們來說在這裏就是一個分水嶺了,如果大家奔着學習自定義控件來的,那你不妨接着看下去




效果如圖所示,只是小白不會製作動態圖,只能隨機截取一張示例


自定義VIEW


在網上看了一些博客,這裏拋磚引玉,先總結下自定義View的步驟:

1、自定義View的屬性

2、在View的構造方法中獲得我們自定義的屬性

3、#重寫onMesure #

4、重寫onDraw

第三點使用了不同的符號,想必有特殊的地方,別急,等一下會解釋。現在結合我們的需求:下載進度條  最簡單的進度條無非兩個部分組成

  • 未下載部分
  • 已下載部分

這下簡單了,我們使用兩種不同的顏色來繪製 已下載部分 和 未下載部分 不就可以了嗎?然後再根據下載進度實時重繪已經下載部分 。但是仔細想想一個完整的控件不應該只有兩種不同的顏色,而是需要具備:
  • 下載狀態文字
  • 文字顏色
  • 文字大小
  • 未下載部分顏色
  • 已經下載部分顏色
 基本的就這幾個,但是再仔細想想,我們在應用商店上面看到的下載控件不僅僅就一個狀態吧,而且這個下載的進度條要什麼形狀的呢?所以又有了下面的需求:
  • 矩形進度條  /  圓角矩形進度條
  • 控件其他狀態的默認顏色


基本的需求已經分析完了,接下來就按照這自定義VIEW的步驟走吧
1、自定義View的屬性首先在res/values/  下建立一個attrs.xml , 在裏面定義我們的屬性和聲明我們的整個樣式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 四周圓弧度 -->
    <attr name="cornerRadius" format="dimension" />
    <attr name="text" format="string" />
    <attr name="textColor" format="color" />
    <attr name="textSize" format="dimension" />
    <!-- 默認顏色 -->
    <attr name="defaultColor" format="color" />
    <!-- 未下載部分顏色 -->
    <attr name="undownloadColor" format="color" />
    <!-- 已經下載部分顏色 -->
    <attr name="downloadedColor" format="color" />
    
    <!-- RuffianProgressBarLine -->
    <declare-styleable name="RuffianProgressBarLine">
        <attr name="cornerRadius" />
        <attr name="text" />
        <attr name="textColor" />
        <attr name="textSize" />
        <attr name="defaultColor" />
        <attr name="undownloadColor" />
        <attr name="downloadedColor" />
    </declare-styleable>
</resources>
根據需求,我們定義了字體,字體顏色,字體大小,控件默認顏色,已經下載部分顏色,未下載部分顏色,控件的形狀[矩形,圓角矩形],一共7個屬性,format是值該屬性的取值類型:

一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。

然後在佈局中聲明我們的自定義View

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.ruffian.android.MainActivity$PlaceholderFragment" >

    <com.ruffian.android.view.RuffianProgressBarLine
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:id="@+id/progressBarLine1"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:padding="10dp"
        custom:cornerRadius="20dp"
        custom:defaultColor="#9ACF51"
        custom:downloadedColor="#ec7883"
        custom:text="下載"
        custom:textColor="@android:color/white"
        custom:textSize="16sp"
        custom:undownloadColor="#cdcdcd" />

    <com.ruffian.android.view.RuffianProgressBarLine
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:id="@+id/progressBarLine2"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:layout_alignParentRight="true"
        android:padding="10dp"
        custom:cornerRadius="0dp"
        custom:defaultColor="#f29b76"
        custom:downloadedColor="#e55a7f"
        custom:text="下載"
        custom:textColor="@android:color/white"
        custom:textSize="16sp"
        custom:undownloadColor="#fb9090" />

    <TextView
        android:id="@+id/progressText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/progressBarLine1"
        android:layout_centerHorizontal="true"
        android:padding="10dp"
        android:text="下載進度 " />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="再玩一次" />

</RelativeLayout>
佈局中展示不同的控件形狀,同時展示下載進度百分比

注意:一定要引入 xmlns:custom="http://schemas.android.com/apk/res/res-auto"我們的命名空間,後面也可以是包路徑:com.ruffian.android.view

2、在View的構造方法中,獲得我們的自定義的樣式

// 默認
	public static final String STATE_DEFAULT = "DEFAULT";
	// 安裝
	public static final String STATE_INSTALL = "INSTALL";
	// 暫停
	public static final String STATE_STOP = "STOP";
	// 下載
	public static final String STATE_DOWNLOAD = "DOWNLOAD";
	// 打開
	public static final String STATE_OPEN = "OPEN";
	// 最大值100
	private static final float MAX_PROGRESS = 100;

	/**
	 * 控件四周圓弧角度,0:矩形<br/>
	 * 不設置或者設置爲0的情況是矩形,其他情況是圓角矩形
	 * 
	 */
	private float mCornerRadius;
	/**
	 * 文字
	 */
	private String mText = "";
	/**
	 * 字體顏色
	 */
	private int mTextColor;
	/**
	 * 字體大小
	 */
	private int mTextSize;
	/**
	 * 控件默認顏色
	 */
	private int mDefaultColor;
	/**
	 * 默認顏色
	 */
	private final String DEF_DEFAULTCOLOR = "#9ACF51";
	/**
	 * 未下載部分顏色
	 */
	private int mUnDownloadColor;
	/**
	 * 默認顏色-下載進度條背景
	 */
	private final String DEF_BACKGROUDCOLOR = "#cdcdcd";
	/**
	 * 已經下載部分顏色
	 */
	private int mDownloadedColor;
	/**
	 * 默認顏色-下載進度
	 */
	private final String DEF_DOWNLOADCOLOR = "#ec7883";

	/**
	 * 矩形,繪製文字需要用
	 */
	private Rect mRect;

	/**
	 * 圓角矩形
	 */
	private RectF mRectF;

	/**
	 * 畫筆,屬性值可能改變
	 */
	private Paint mPaint;
	/**
	 * 文字畫筆,初始化之後屬性不再改變
	 */
	private Paint mTextPaint;

	/**
	 * 控件狀態
	 */
	private String mState = STATE_DEFAULT;

	/**
	 * 下載進度{這裏根據需求定基礎類型,也可以是float[0.0f,1.0f]}
	 */
	private int mProgress;

	public RuffianProgressBarLine(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public RuffianProgressBarLine(Context context) {
		this(context, null);
	}

	public RuffianProgressBarLine(Context context, AttributeSet attrs,
			int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		// 獲取自定義的控件
		TypedArray typedArray = getContext().obtainStyledAttributes(attrs,
				R.styleable.RuffianProgressBarLine, defStyleAttr, 0);
		int parameterCount = typedArray.getIndexCount();
		for (int i = 0; i < parameterCount; i++) {

			int attr = typedArray.getIndex(i);
			switch (attr) {
			case R.styleable.RuffianProgressBarLine_cornerRadius:
				mCornerRadius = typedArray.getDimensionPixelSize(attr, 0);
				break;
			case R.styleable.RuffianProgressBarLine_text:
				mText = typedArray.getString(attr);
				break;
			case R.styleable.RuffianProgressBarLine_textColor:
				mTextColor = typedArray.getColor(attr, 0);
				break;
			case R.styleable.RuffianProgressBarLine_textSize:
				mTextSize = typedArray.getDimensionPixelSize(attr, 12);
				break;
			case R.styleable.RuffianProgressBarLine_defaultColor:
				mDefaultColor = typedArray.getColor(attr,
						Color.parseColor(DEF_DEFAULTCOLOR));
				break;
			case R.styleable.RuffianProgressBarLine_undownloadColor:
				mUnDownloadColor = typedArray.getColor(attr,
						Color.parseColor(DEF_BACKGROUDCOLOR));
				break;
			case R.styleable.RuffianProgressBarLine_downloadedColor:
				mDownloadedColor = typedArray.getColor(attr,
						Color.parseColor(DEF_DOWNLOADCOLOR));
				break;
			}

		}

		typedArray.recycle();

		mPaint = new Paint();
		mRect = new Rect();
		mRectF = new RectF();

		// 初始化之後不再改變,直接設置屬性
		mTextPaint = new Paint();
		// 設置抗鋸齒,圓滑處理
		mTextPaint.setAntiAlias(true);
		// 設置畫筆類型
		mTextPaint.setStyle(Style.FILL);
		// 設置畫筆顏色
		mTextPaint.setColor(mTextColor);
		// 設置字體大小
		mTextPaint.setTextSize(mTextSize);

	}
我們重寫了3個構造方法,默認的佈局文件調用的是兩個參數的構造方法,所以記得讓所有的構造調用我們的三個參數的構造,我們在三個參數的構造中獲得自定義屬性。

3、我們重寫 onDraw,onMesure 調用系統提供的:

/**
	 * 重寫計算控件寬高函數
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		// 獲取寬高的設置模式
		int withMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		// 獲取寬高的大小
		int withSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		// 最終寬高
		int height = getSizeInMode(heightSize, heightMode, 1);
		int width = getSizeInMode(withSize, withMode, 0);

		// 最終設置寬高
		setMeasuredDimension(width, height);

	}

	/**
	 * 獲取不同mode下寬高的實際值<br/>
	 * type[0:寬,1:高]
	 * 
	 * @param size初始值
	 * @param mode設置類型
	 * @param type
	 * @return
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private int getSizeInMode(int size, int mode, int type) {
		// 返回值
		int sizeValue = 0;

		switch (mode) {
		case MeasureSpec.EXACTLY:

			// 設置了明確的值,直接使用
			sizeValue = size;

			break;
		case MeasureSpec.AT_MOST:

			// WARP_CONTENT時候,先計算繪製文本的大小
			mTextPaint.setTextSize(mTextSize);
			mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);

			// 再計算[左右,上下]的padding值
			int desired = 0;
			if (type == 0) {

				// 文本寬度+左右padding
				float textWidth = mRect.width();
				desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());

			} else if (type == 1) {

				// 文本寬度+上下padding
				float textHeight = mRect.height();
				desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
			}

			sizeValue = desired;

			break;
		case MeasureSpec.UNSPECIFIED:
			// 不處理
			break;
		}

		return sizeValue;

	}

	/**
	 * 重寫繪製函數onDraw
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		// 設置抗鋸齒,圓滑處理
		mPaint.setAntiAlias(true);
		// 設置畫筆類型
		mPaint.setStyle(Style.FILL);

		// 繪製控件
		canvasViewOnLogic(canvas);

	}

	/**
	 * 根據業務邏輯繪製控件
	 * 
	 * @param canvas
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void canvasViewOnLogic(Canvas canvas) {

		/**
		 * 下載中和暫停狀態是特殊情況,需要畫兩層視圖,其他情況只需要一層
		 */
		if (mState.equals(STATE_DOWNLOAD) || mState.equals(STATE_STOP)) {
			// 暫停狀態--下載中狀態

			// 繪製時mProgress要轉化成float類型,區間[0.0f,1.0f]
			drawDownloadView(canvas, mDownloadedColor, 0,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()));

			drawDownloadView(canvas, mUnDownloadColor,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth());

		} else {
			// 其他狀態

			// 設置默認畫筆顏色
			mPaint.setColor(mDefaultColor);
			// 設置矩形,寬度是控件大小
			mRectF = new RectF(0, 0, getWidth(), getHeight());
			// 畫底部矩形
			canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);

		}

		// 計算文字
		mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);
		// 繪製文字居中
		canvas.drawText(mText, getWidth() / 2 - mRect.width() / 2, getHeight()
				/ 2 + mRect.height() / 2, mTextPaint);

	}

	/**
	 * 繪製下載狀態的view<br/>
	 * 理解:繪製兩次相同的view,不同顏色區分,一個繪製前半部分,一部分繪製後半部分
	 * 
	 * @param canvas
	 * @param color
	 * @param startX開始繪製的X
	 * @param endX結束繪製的X
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void drawDownloadView(Canvas canvas, int color, int startX, int endX) {

		mPaint.setColor(color);
		// 設置矩形,寬度是控件大小
		mRectF = new RectF(0, 0, getWidth(), getHeight());

		canvas.save(Canvas.CLIP_SAVE_FLAG);
		canvas.clipRect(startX, 0, endX, getMeasuredHeight());
		canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);
		canvas.restore();

	}

文章開頭說 onMeasure 函數存在特殊情況。這種特殊情況是針對,佈局文件中 layout_width , layout_height。

當我們設置明確的寬度和高度時,系統幫我們測量的結果就是我們設置的結果,當我們設置爲WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。

所以,當設置了WRAP_CONTENT時,我們需要自己進行測量,即重寫onMesure方法”:

重寫之前先了解MeasureSpec的specMode,一共三種類型:

EXACTLY:一般是設置了明確的值或者是MATCH_PARENT

AT_MOST:表示子佈局限制在一個最大值內,一般爲WARP_CONTENT

UNSPECIFIED:表示子佈局想要多大就多大,很少使用


/**
	 * 重寫計算控件寬高函數
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		// 獲取寬高的設置模式
		int withMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		// 獲取寬高的大小
		int withSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		// 最終寬高
		int height = getSizeInMode(heightSize, heightMode, 1);
		int width = getSizeInMode(withSize, withMode, 0);

		// 最終設置寬高
		setMeasuredDimension(width, height);

	}

	/**
	 * 獲取不同mode下寬高的實際值<br/>
	 * type[0:寬,1:高]
	 * 
	 * @param size初始值
	 * @param mode設置類型
	 * @param type
	 * @return
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private int getSizeInMode(int size, int mode, int type) {
		// 返回值
		int sizeValue = 0;

		switch (mode) {
		case MeasureSpec.EXACTLY:

			// 設置了明確的值,直接使用
			sizeValue = size;

			break;
		case MeasureSpec.AT_MOST:

			// WARP_CONTENT時候,先計算繪製文本的大小
			mTextPaint.setTextSize(mTextSize);
			mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);

			// 再計算[左右,上下]的padding值
			int desired = 0;
			if (type == 0) {

				// 文本寬度+左右padding
				float textWidth = mRect.width();
				desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());

			} else if (type == 1) {

				// 文本寬度+上下padding
				float textHeight = mRect.height();
				desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
			}

			sizeValue = desired;

			break;
		case MeasureSpec.UNSPECIFIED:
			// 不處理
			break;
		}

		return sizeValue;

	}


這裏特別說明一下 onDraw方法

如果是在矩形的情況下是很簡答的一種實現:

先畫一個底部的矩形(表示未下載),然後再重新設置畫筆顏色再畫一個(表示進度)矩形。看起來就能達到下載進度的效果

但是當我們設置屬性爲 圓角矩形(cornerRadius>0)的時候,我發現效果不是我想要的

運行結果是:這樣的,這樣的



但是我們想要的是:這樣的,這樣的



由於剛開始自定義控件,很多屬性和用法都不知道怎麼用,折騰了好久,後來在網上看到說  canvas 有個 clipRect 的方法,good ,那麼修改一下繪製部分的代碼就可以了

起初代碼

	// 暫停狀態--下載中狀態
		
					// 設置底部矩形顏色
					mPaint.setColor(mBackgroudColor);
					// 設置矩形,寬度是控件大小
					mRectF = new RectF(0, 0, getWidth(), getHeight());
					// 畫底部矩形
					canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);

					// 設置進度矩形顏色
					mPaint.setColor(mDownloadColor);

					// 設置矩形,寬度是實際進度
					mRectF = new RectF(0, 0, (mProgress / MAX_PROGRESS) * getWidth(),
							getHeight());
					// 畫進度矩形
					canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);



修改關鍵方法

/**
	 * 繪製下載狀態的view<br/>
	 * 理解:繪製兩次相同的view,不同顏色區分,一個繪製前半部分,一部分繪製後半部分
	 * 
	 * @param canvas
	 * @param color
	 * @param startX開始繪製的X
	 * @param endX結束繪製的X
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void drawDownloadView(Canvas canvas, int color, int startX, int endX) {

		mPaint.setColor(color);
		// 設置矩形,寬度是控件大小
		mRectF = new RectF(0, 0, getWidth(), getHeight());

		canvas.save(Canvas.CLIP_SAVE_FLAG);
		canvas.clipRect(startX, 0, endX, getMeasuredHeight());
		canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);
		canvas.restore();

	}
// 暫停狀態--下載中狀態

			// 繪製時mProgress要轉化成float類型,區間[0.0f,1.0f]
			drawDownloadView(canvas, mDownloadedColor, 0,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()));

			drawDownloadView(canvas, mUnDownloadColor,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth());

恩恩,這下好了,終於是我們想要的效果了,接下來看看activity模擬下載整個流程,使用自定義的下載控件

activity代碼

package com.ruffian.android;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

import com.ruffian.android.view.RuffianProgressBarLine;

@SuppressLint("HandlerLeak")
public class MainActivity extends Activity {

	private RuffianProgressBarLine mProgressBarLine;
	private Button mButton;
	private TextView mProgressText;// 下載進度

	int mProgress = 0;
	boolean isLoading = false;
	private String viewState;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mProgressBarLine = (RuffianProgressBarLine) findViewById(R.id.progressBarLine1);
		mProgressText = (TextView) findViewById(R.id.progressText);
		mButton = (Button) findViewById(R.id.button);

		mProgressBarLine.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {

				viewState = mProgressBarLine.getState();

				if (viewState.equals(RuffianProgressBarLine.STATE_DEFAULT)) {
					// 下載中
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("暫停");
					isLoading = true;
					download();

				} else if (viewState
						.equals(RuffianProgressBarLine.STATE_DOWNLOAD)) {
					// 暫停
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_STOP);
					mProgressBarLine.setText("繼續");
					isLoading = false;
					// download();

				} else if (viewState.equals(RuffianProgressBarLine.STATE_STOP)) {
					// 繼續
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("暫停");
					isLoading = true;
					// download();

				} else if (viewState
						.equals(RuffianProgressBarLine.STATE_INSTALL)) {
					// 安裝
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_INSTALL);
					mProgressBarLine.setText("安裝");

				} else if (viewState.equals(RuffianProgressBarLine.STATE_OPEN)) {
					// 運行
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("運行");
				}

			}
		});

		mButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {
				mProgress = 0;
				isLoading = false;
				mProgressBarLine.setState(RuffianProgressBarLine.STATE_DEFAULT);
				mProgressBarLine.setText("下載");
				mProgressText.setText("下載進度 ");
			}
		});

	}

	/**
	 * 下載,暫停
	 * 
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	public void download() {
		new Thread() {
			public void run() {

				while (mProgress <= 100) {

					if (mProgress == 100) {
						// 進度滿100,狀態改爲安裝
						mProgressBarLine
								.setState(RuffianProgressBarLine.STATE_INSTALL);
						mProgressBarLine.setText("安裝");
					}

					// 是否正在下載
					if (isLoading) {
						// 更新UI
						uiHandler.sendMessage(uiHandler.obtainMessage(1001,
								mProgress));
						mProgressBarLine.setProgress(mProgress);
						mProgress++;
					}
					// Log.w("sss", "" + mProgress);
					try {

						Thread.sleep(80);// 進度改變速度
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				}

			};
		}.start();
	}

	/**
	 * 更新UI
	 */
	Handler uiHandler = new Handler() {
		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			case 1001:
				int progress = (int) msg.obj;
				if (progress == 100) {
					mProgressText.setText("下載完成");
				} else {
					mProgressText.setText(String.valueOf(progress) + "%");
				}
				break;
			}
		};
	};

}


小弟第一次嘗試自定義控件的實現,前輩們要是看見哪裏有不正確的地方,還懇請耐心指正,同時小夥伴們在學習過程中遇到什麼問題可以留言討論,緊急情況發送郵件:[email protected] 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章