最近项目上用到一个密码加锁功能,需要一个数字密码界面,就想着封装成一个View来方便管理和使用。
废话不多说,先上最终效果图:
思路
整体可分为2个部分来实现,1.顶部是4个密码位的填充;2.数字键盘部分。整体可以是一个纵向LinearLayout,4个密码位用横向LinearLayout即可,键盘由于是宫格形式,因此可用GridLayout来布局。由于密码位和键盘数字都是以圆圈为背景,这里采用自定义一个圆形背景ImageView来使用。
实现
1.页面布局
首先定义一个圆形背景的ImageView,由于最终实现的效果是点击的时候要填充圆背景,非点击状态下是空心圆,因此可通过改变Paint的style来动态更改显示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
/**
* 圆形背景ImageView(设置实心或空心)
*/
public class CircleImageView extends ImageView{
private Paint mPaint;
private int mWidth;
private int mHeight;
public CircleImageView(Context context) {
this (context, null );
}
public CircleImageView(Context context, AttributeSet attrs) {
this (context, attrs, 0 );
}
public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
initView(context);
}
public void initView(Context context){
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mPanelColor);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setAntiAlias( true );
}
@Override
protected void onSizeChanged( int w, int h, int oldw, int oldh) {
super .onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
public void draw(Canvas canvas) {
canvas.drawCircle(mWidth/ 2 , mHeight/ 2 , mWidth/ 2 - 6 , mPaint);
super .draw(canvas);
}
/**
* 设置圆为实心状态
*/
public void setFillCircle(){
mPaint.setStyle(Paint.Style.FILL);
invalidate();
}
/**
* 设置圆为空心状态
*/
public void setStrokeCircle(){
mPaint.setStyle(Paint.Style.STROKE);
invalidate();
}
}
|
可以看到,在onDraw中绘制了一个圆,默认为空心状态,定义setFillCircle和setStrokeCircle这两个方法以便外界可以方便地切换圆为实心或者空心。
圆形ImageView定义好了,开始添加密码位,布局如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
inputResultView = new LinearLayout(context);
for ( int i= 0 ; i< 4 ; i++){
CircleImageView mResultItem = new CircleImageView(context);
mResultIvList.add(mResultItem);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mResultIvRadius, mResultIvRadius);
params.leftMargin = dip2px(context, 4 );
params.rightMargin = dip2px(context, 4 );
mResultItem.setPadding(dip2px(context, 2 ),dip2px(context, 2 ),dip2px(context, 2 ),dip2px(context, 2 ));
mResultItem.setLayoutParams(params);
inputResultView.addView(mResultItem);
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
params.bottomMargin = dip2px(context, 34 );
inputResultView.setLayoutParams(params);
addView(inputResultView);
|
接着添加数字键盘部分的布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
GridLayout numContainer = new GridLayout(context);
numContainer.setColumnCount( 3 );
for ( int i= 0 ; i<numArr.length; i++){
RelativeLayout numItem = new RelativeLayout(context);
numItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);
RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);
gridItemParams.addRule(CENTER_IN_PARENT);
final TextView numTv = new TextView(context);
numTv.setText(numArr[i]);
numTv.setTextColor(mPanelColor);
numTv.setTextSize( 30 );
numTv.setGravity(Gravity.CENTER);
numTv.setLayoutParams(gridItemParams);
final CircleImageView numBgIv = new CircleImageView(context);
numBgIv.setLayoutParams(gridItemParams);
numItem.addView(numBgIv);
numItem.addView(numTv);
numContainer.addView(numItem);
if (i == 9 ){
numItem.setVisibility(INVISIBLE);
}
}
//删除按钮
RelativeLayout deleteItem = new RelativeLayout(context);
deleteItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);
RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);
gridItemParams.addRule(CENTER_IN_PARENT);
//假如删除按钮是设置自定义图片资源的话,可用注释这段
//ImageView deleteIv = new ImageView(context);
//deleteIv.setImageResource(R.drawable.icn_delete_pw);
//deleteIv.setLayoutParams(gridItemParams);
//deleteItem.addView(deleteIv);
TextView deleteTv = new TextView(context);
deleteTv.setText( "Delete" );
deleteTv.setTextColor(mPanelColor);
deleteTv.setTextSize(dip2px(context, 8 ));
deleteTv.setLayoutParams(gridItemParams);
deleteTv.setGravity(Gravity.CENTER);
deleteItem.addView(deleteTv);
numContainer.addView(deleteItem);
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
gridParams.gravity = Gravity.CENTER_HORIZONTAL;
numContainer.setLayoutParams(gridParams);
addView(numContainer);
|
数字键盘这里用一个数组存数字内容,遍历添加,注意此处由于第10个的子View的时候是空白的,所以当遍历到第10个元素的时候,可以将其隐藏。遍历完后再单独添加删除按钮。
2.输入逻辑
页面布局完成了,接下来就是密码输入的逻辑部分,最终的效果是每点击一次数字,密码位就填充一个,每点击删除按钮一次,密码位就回退一个,输入4个数字之后,即完成输入,获取结果,并重置密码位。这里用一个StringBuilder变量来记录当前已输入的密码,每次添加就append进去,每次删除就调用deleteCharAt。
由于点击数字按下的时候填充,松开的时候为空心状态,所以可以在ACTION_DOWN和ACTION_UP事件中分别操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
numTv.setOnTouchListener( new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
numBgIv.setFillCircle();
numTv.setTextColor(Color.WHITE);
if (mPassWord.length() < 4 ){
mPassWord.append(numTv.getText());
mResultIvList.get(mPassWord.length()- 1 ).setFillCircle();
if (mInputListener!= null && mPassWord.length() == 4 ){
//已完整输入4个
}
}
break ;
case MotionEvent.ACTION_UP:
numBgIv.setStrokeCircle();
numTv.setTextColor(mPanelColor);
break ;
}
return true ;
}
});
|
每次点击的时候,判断当前已输入的密码位是否已经超过4位,如果没超过,就继续追加。如果等于4,就说明输入完成,此时的mPassWord的内容就是最终的密码,可以用一个接口将其回调出去方便Activity中获取输入的密码:
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* 监听输入完毕的接口
*/
private InputListener mInputListener;
public void setInputListener(InputListener mInputListener) {
his.mInputListener = mInputListener;
}
public interface InputListener{
void inputFinish(String result);
}
|
然后在上面的ACTION_DOWN中输入数字等于4的时候,回调该接口:
1
2
3
|
if (mInputListener!= null && mPassWord.length() == 4 ){
mInputListener.inputFinish(mPassWord.toString());
}
|
另外,删除的操作单独封装为一个方法:
1
2
3
4
5
6
7
8
9
10
|
/**
* 删除
*/
public void delete(){
if (mPassWord.length() == 0 ){
return ;
}
mResultIvList.get(mPassWord.length()- 1 ).setStrokeCircle();
mPassWord.deleteCharAt(mPassWord.length()- 1 );
}
|
注意点:当前无输入密码时,直接return不作任何操作,假如已有输入数字,就删除最尾部的那个数字。
最后,还要考虑一种情况,即用户输入密码错误时的一些反馈,参照平时的习惯,一般是4个密码位左右摆动并且手机震动效果,震动结束之后,当前存储的密码位重置为初始状态,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/**
* 输入错误的状态显示(包括震动,密码位左右摇摆效果,重置密码位)
*/
public void showErrorStatus(){
mVibrator.vibrate( new long []{ 100 , 100 , 100 , 100 },- 1 );
List<Animator> animators = new ArrayList<>();
ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(inputResultView, "translationX" , - 50 .0f, 50 .0f,- 50 .0f, 0 .0f);
translationXAnim.setDuration( 400 );
animators.add(translationXAnim);
AnimatorSet btnSexAnimatorSet = new AnimatorSet();
btnSexAnimatorSet.playTogether(animators);
btnSexAnimatorSet.start();
btnSexAnimatorSet.addListener( new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
resetResult();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
|
可以看到,在onAnimationEnd中调用了resetResult,即动画结束时重置密码,resetResult方法如下:
1
2
3
4
5
6
7
8
9
|
/**
* 重置密码输入
*/
public void resetResult(){
for ( int i= 0 ; i<mResultIvList.size(); i++){
mResultIvList.get(i).setStrokeCircle();
}
mPassWord.delete( 0 , 4 );
}
|
遍历所有密码位View设置为空心,并且删除当前mPassWord变量存储的所有内容。
完整代码
完整的自定义数字密码锁代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
|
package com.example.zjyang.viewtest.view;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Service;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import static android.widget.RelativeLayout.CENTER_HORIZONTAL;
import static android.widget.RelativeLayout.CENTER_IN_PARENT;
/**
* Created by IT_ZJYANG on 2018/1/22.
* 数字解锁键盘View
*/
public class NumLockPanel extends LinearLayout {
private String[] numArr = new String[]{ "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "" , "0" };
private int mPaddingLeftRight;
private int mPaddingTopBottom;
//4个密码位ImageView
private ArrayList<CircleImageView> mResultIvList;
private LinearLayout inputResultView;
//存储当前输入内容
private StringBuilder mPassWord;
//振动效果
private Vibrator mVibrator;
//整个键盘的颜色
private int mPanelColor;
//4个密码位的宽度
private int mResultIvRadius;
//数字键盘的每个圆的宽度
private int mNumRadius;
//每个圆的边界宽度
private int mStrokeWidth;
public NumLockPanel(Context context) {
this (context, null );
}
public NumLockPanel(Context context, AttributeSet attrs) {
this (context, attrs, 0 );
}
public NumLockPanel(Context context, AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
mPaddingLeftRight = dip2px(context, 21 );
mPaddingTopBottom = dip2px(context, 10 );
mPanelColor = Color.BLACK; //颜色代码可采用Color.parse("#000000");
mResultIvRadius = dip2px(context, 20 );
mNumRadius = dip2px(context, 66 );
mStrokeWidth = dip2px(context, 2 );
mVibrator = (Vibrator)context.getSystemService(Service.VIBRATOR_SERVICE);
mResultIvList = new ArrayList<>();
mPassWord = new StringBuilder();
setOrientation(VERTICAL);
setGravity(CENTER_HORIZONTAL);
initView(context);
}
public void initView(Context context){
//4个结果号码
inputResultView = new LinearLayout(context);
for ( int i= 0 ; i< 4 ; i++){
CircleImageView mResultItem = new CircleImageView(context);
mResultIvList.add(mResultItem);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mResultIvRadius, mResultIvRadius);
params.leftMargin = dip2px(context, 4 );
params.rightMargin = dip2px(context, 4 );
mResultItem.setPadding(dip2px(context, 2 ),dip2px(context, 2 ),dip2px(context, 2 ),dip2px(context, 2 ));
mResultItem.setLayoutParams(params);
inputResultView.addView(mResultItem);
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
params.bottomMargin = dip2px(context, 34 );
inputResultView.setLayoutParams(params);
addView(inputResultView);
//数字键盘
GridLayout numContainer = new GridLayout(context);
numContainer.setColumnCount( 3 );
for ( int i= 0 ; i<numArr.length; i++){
RelativeLayout numItem = new RelativeLayout(context);
numItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);
RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);
gridItemParams.addRule(CENTER_IN_PARENT);
final TextView numTv = new TextView(context);
numTv.setText(numArr[i]);
numTv.setTextColor(mPanelColor);
numTv.setTextSize( 30 );
numTv.setGravity(Gravity.CENTER);
numTv.setLayoutParams(gridItemParams);
final CircleImageView numBgIv = new CircleImageView(context);
numBgIv.setLayoutParams(gridItemParams);
numTv.setOnTouchListener( new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
numBgIv.setFillCircle();
numTv.setTextColor(Color.WHITE);
if (mPassWord.length() < 4 ){
mPassWord.append(numTv.getText());
mResultIvList.get(mPassWord.length()- 1 ).setFillCircle();
if (mInputListener!= null && mPassWord.length() == 4 ){
mInputListener.inputFinish(mPassWord.toString());
}
}
break ;
case MotionEvent.ACTION_UP:
numBgIv.setStrokeCircle();
numTv.setTextColor(mPanelColor);
break ;
}
return true ;
}
});
numItem.addView(numBgIv);
numItem.addView(numTv);
numContainer.addView(numItem);
if (i == 9 ){
numItem.setVisibility(INVISIBLE);
}
}
//删除按钮
RelativeLayout deleteItem = new RelativeLayout(context);
deleteItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);
RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);
gridItemParams.addRule(CENTER_IN_PARENT);
//假如删除按钮是设置自定义图片资源的话,可用注释这段
//ImageView deleteIv = new ImageView(context);
//deleteIv.setImageResource(R.drawable.icn_delete_pw);
//deleteIv.setLayoutParams(gridItemParams);
//deleteItem.addView(deleteIv);
TextView deleteTv = new TextView(context);
deleteTv.setText( "Delete" );
deleteTv.setTextColor(mPanelColor);
deleteTv.setTextSize(dip2px(context, 8 ));
deleteTv.setLayoutParams(gridItemParams);
deleteTv.setGravity(Gravity.CENTER);
deleteItem.addView(deleteTv);
numContainer.addView(deleteItem);
deleteTv.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
delete();
}
});
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
gridParams.gravity = Gravity.CENTER_HORIZONTAL;
numContainer.setLayoutParams(gridParams);
addView(numContainer);
}
/**
* 输入错误的状态显示(包括震动,密码位左右摇摆效果,重置密码位)
*/
public void showErrorStatus(){
mVibrator.vibrate( new long []{ 100 , 100 , 100 , 100 },- 1 );
List<Animator> animators = new ArrayList<>();
ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(inputResultView, "translationX" , - 50 .0f, 50 .0f,- 50 .0f, 0 .0f);
translationXAnim.setDuration( 400 );
animators.add(translationXAnim);
AnimatorSet btnSexAnimatorSet = new AnimatorSet();
btnSexAnimatorSet.playTogether(animators);
btnSexAnimatorSet.start();
btnSexAnimatorSet.addListener( new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
resetResult();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
/**
* 删除
*/
public void delete(){
if (mPassWord.length() == 0 ){
return ;
}
mResultIvList.get(mPassWord.length()- 1 ).setStrokeCircle();
mPassWord.deleteCharAt(mPassWord.length()- 1 );
}
/**
* 重置密码输入
*/
public void resetResult(){
for ( int i= 0 ; i<mResultIvList.size(); i++){
mResultIvList.get(i).setStrokeCircle();
}
mPassWord.delete( 0 , 4 );
}
/**
* 监听输入完毕的接口
*/
private InputListener mInputListener;
public void setInputListener(InputListener mInputListener) {
this .mInputListener = mInputListener;
}
public interface InputListener{
void inputFinish(String result);
}
/**
* dip/dp转像素
*
* @param dipValue
* dip或 dp大小
* @return 像素值
*/
public static int dip2px(Context context, float dipValue) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return ( int ) (dipValue * (metrics.density) + 0 .5f);
}
/**
* 圆形背景ImageView(设置实心或空心)
*/
public class CircleImageView extends ImageView{
private Paint mPaint;
private int mWidth;
private int mHeight;
public CircleImageView(Context context) {
this (context, null );
}
public CircleImageView(Context context, AttributeSet attrs) {
this (context, attrs, 0 );
}
public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
initView(context);
}
public void initView(Context context){
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mPanelColor);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setAntiAlias( true );
}
@Override
protected void onSizeChanged( int w, int h, int oldw, int oldh) {
super .onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
public void draw(Canvas canvas) {
canvas.drawCircle(mWidth/ 2 , mHeight/ 2 , mWidth/ 2 - 6 , mPaint);
super .draw(canvas);
}
/**
* 设置圆为实心状态
*/
public void setFillCircle(){
mPaint.setStyle(Paint.Style.FILL);
invalidate();
}
/**
* 设置圆为空心状态
*/
public void setStrokeCircle(){
mPaint.setStyle(Paint.Style.STROKE);
invalidate();
}
}
}
|
使用
在Activity的布局文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<? xml version = "1.0" encoding = "utf-8" ?>
< RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:tools = "http://schemas.android.com/tools"
android:id = "@+id/activity_main"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
android:background = "#ffffff"
tools:context = "com.example.zjyang.viewtest.MainActivity" >
< com.example.zjyang.viewtest.view.NumLockPanel
android:id = "@+id/num_lock"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:layout_marginTop = "30dp" >
</ com.example.zjyang.viewtest.view.NumLockPanel >
</ RelativeLayout >
|
在代码中监听输入的密码结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class MainActivity extends AppCompatActivity {
private NumLockPanel mNumLockPanel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNumLockPanel = (NumLockPanel) findViewById(R.id.num_lock);
mNumLockPanel.setInputListener( new NumLockPanel.InputListener() {
@Override
public void inputFinish(String result) {
//此处result即为输入结果
Toast.makeText(MainActivity. this , result, Toast.LENGTH_SHORT).show();
//错误效果示例
mNumLockPanel.showErrorStatus();
}
});
}
}
|
最后,在自定义View构造方法中初始化了圆圆和数字的颜色风格,以及空心圆的边界粗细大小,可根据需求自行更改。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/IT_ZJYANG/article/details/79132048