看完这篇文章你可以学到什么知识呢?
- 自定义组合控件
- 封装view,只暴露接口,模块独立,设计思想。
- 会用到正则表达式
- 如何禁止EditText在获取到焦点的时候拉起键盘
- 学会登录中获取验证码的倒计时效果
- EditText的内容添加与删除
效果图
分析
首先分析一下!其实对于UI来说,也是可以封装起来的,比如说,我们这里面涉及到了手机号码的检查、涉及到验证码的检查,涉及到是否同意协议来控制按钮是否可用,这些动作我们都可以隐藏起来。把这些所有看得见的,都当成一个View,这个View具备这些功能。
然后使用暴露接口:1、获取验证码按钮被点击了;2、协议内容被点击了;3、登录按钮被点击了。
除此之外,应该没有其他动作给使用者使用了。
问题点:
1、因为这个界面是平板的界面,我们自己写了一个键盘输入,所以当我们的输入框获取到焦点的时候,不弹出键盘出来
2、做一个倒计时的效果
3、获取到焦点的edittext内容输入
实现
我们要实现这个类,要用一个很大的坑来填充这些内容,所以我们要写一个LoginPageView,继承自FrameLayout。这个时候,需要实现一些方法:
有没有注意到,前两个构造函数都是this,表示调用自己的构造函数。不同的参数而已。这样子就可以确保不管是哪种方式创建这个类的,都会进入第三个构造函数,也就是同一个入口。
这个类的全部代码如下:
package com.lwm.customview.customview.loginpage;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Message;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.lwm.customview.R;
import com.lwm.customview.customview.App;
import java.lang.reflect.Field;
/**
* 点击获取验证码条件
* 1、手机号码是正确
* 点击登录条件
* 1、正确的手机号码 + 验证码 + 同意了协议
*/
public class LoginPageView extends FrameLayout {
private static final String TAG = "LoginPageView";
public static final int SIZE_VERIFY_CODE_DEFAULT = 4;
private int mColor;
private int mVerifyCodeSize = SIZE_VERIFY_CODE_DEFAULT;
private CheckBox mIsConfirm; // 同意协议的选择框
private OnLoginPageActionListener mActionListener = null;
private LoginKeyboardView mLoginKeyboardView; // 登录键盘
private EditText mVerifyCodeInput; // 验证码输入框
private EditText mPhoneNumInp; // 输入框
private TextView mGetVerifyCodeBtn;
private boolean isPhoneNumOk = false;
private boolean isAgreementOk = false;
private boolean isVerifyCodeOk = false;
private boolean isCountDowning = false;
// 手机号码的规则
public static final String REGEX_MOBILE_EXACT = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|(147))\\d{8}$";
private View mLoginBtn; // 确定按钮
private TextView mAgreementView; // 服务条款的文字
/**
* 倒计时关注点:
* 1、倒计时多长时间:duration
* 2、时间间隔:dt
* 3、通知 UI 更新
*/
public static final int DURATION_DEFAULT = 60 * 1000;
// duration ms
private int mCountDownDuration = DURATION_DEFAULT;
// dt ms
public static final int D_TIME_DEFAULT = 1000;
private int restTime = mCountDownDuration;
private Handler mHandler;
private CountDownTimer mCountDownTimer;
// 倒计时方式一:
public void startCountDown() {
mHandler = App.getHandler();
mHandler.post(new Runnable() {
@Override
public void run() {
restTime -= D_TIME_DEFAULT;
Log.d(TAG, "rest = " + restTime);
if (restTime > 0) {
mHandler.postDelayed(this, D_TIME_DEFAULT);
// 直接更新 UI
mGetVerifyCodeBtn.setText("(" + restTime / 1000 + ")秒");
mGetVerifyCodeBtn.setEnabled(false);
isCountDowning = true;
} else {
restTime = mCountDownDuration;
mGetVerifyCodeBtn.setEnabled(true);
isCountDowning = false;
mGetVerifyCodeBtn.setText("获取验证码");
updateAllBtnState();
}
}
});
}
// 倒计时方式二:
private void beginCountDown() {
isCountDowning = true;
mGetVerifyCodeBtn.setEnabled(false);
mCountDownTimer = new CountDownTimer(mCountDownDuration, D_TIME_DEFAULT) {
public void onTick(long millisUntilFinished) {
// 通知 UI 更新
int res = (int) (millisUntilFinished / 1000);
mGetVerifyCodeBtn.setText("( " + res + " )秒");
}
public void onFinish() {
// 倒计时结束
mGetVerifyCodeBtn.setEnabled(true);
isCountDowning = false;
mGetVerifyCodeBtn.setText("获取验证码");
updateAllBtnState();
mCountDownTimer = null;
}
}.start();
}
/**
* 验证码错误
*/
public void onVerifyCodeError() {
// 第二步:清空验证码输入框里的内容
mVerifyCodeInput.getText().clear();
// 第三步:停止倒计时
if (isCountDowning && mCountDownTimer != null) {
isCountDowning = false;
mCountDownTimer.cancel(); // 取消倒计时
mCountDownTimer.onFinish();
}
}
public LoginPageView(@NonNull Context context) {
this(context, null);
}
public LoginPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoginPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化属性
// 设置 get和 set 方法
initAttrs(context, attrs);
// 初始化控件
initView();
disableEdtFocus2Keypad(); // 禁止拉起软键盘
// 监听点击事件
initEvent();
}
// 禁止拉起软键盘
@SuppressLint("NewApi")
private void disableEdtFocus2Keypad() {
mVerifyCodeInput.setShowSoftInputOnFocus(false); // 禁止拉起软键盘
mPhoneNumInp.setShowSoftInputOnFocus(false); // 禁止拉起软键盘
}
private void initEvent() {
mLoginKeyboardView.setOnKeyPressListener(new LoginKeyboardView.OnKeyPressListener() {
@Override
public void onNumberPress(int number) {
// 数字被点击
// 插入数字( EditText谁有焦点就插入谁)
EditText focusEdt = getFocusEdt();
if (focusEdt != null) {
Editable text = focusEdt.getText();
int index = focusEdt.getSelectionEnd(); // 获取光标的最后位置
text.insert(index, String.valueOf(number));
}
}
@Override
public void onBackPress() {
// 退格键被点击
EditText focusEdt = getFocusEdt();
if (focusEdt != null) {
int index = focusEdt.getSelectionEnd(); // 获取光标的最后位置
Log.d(TAG, "index ==" + index);
Editable editable = focusEdt.getText();
if (index > 0) {
editable.delete(index - 1, index);
}
}
}
});
mGetVerifyCodeBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// todo:get verify code.
if (mActionListener != null) {
// 拿到手机号码
String phoneNum = mPhoneNumInp.getText().toString().trim();
Log.d(TAG, "phoneNum = " + phoneNum);
mActionListener.onGetVerifyCodeClick(phoneNum);
// 开启倒计时
// startCountDown(); // 倒计时方式一
beginCountDown(); // 倒计时方式二
} else {
throw new IllegalArgumentException("no action to get verify code");
}
}
});
// 手机号码输入监听
mPhoneNumInp.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// 变化的时候去检查手机号码是否符合格式
// TODO:
Log.d(TAG, "content:" + s);
String phoneNum = s.toString();
boolean isMatch = phoneNum.matches(REGEX_MOBILE_EXACT);
isPhoneNumOk = phoneNum.length() == 11 && isMatch;
updateAllBtnState();
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 验证码输入监听
mVerifyCodeInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
isVerifyCodeOk = s.length() == 4;
updateAllBtnState();
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 协议按钮
mIsConfirm.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
isAgreementOk = isChecked;
updateAllBtnState();
}
});
// 协议文字
mAgreementView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "mIsConfirm --> click...");
}
});
// 登录按钮
mLoginBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// todo:根据产品经理的要求,点击登录以后是否禁止按钮
// TODO:UI 上防止重复提交
// 登录
if (mActionListener != null) {
// 拿到手机号码和验证码
mActionListener.onConfirmClick(getVerifyCode(), getPhoneNum());
}
}
});
}
// 获取手机号码
private String getPhoneNum() {
return mPhoneNumInp.getText().toString().trim();
}
// 获取验证码
private String getVerifyCode() {
return mVerifyCodeInput.getText().toString().trim();
}
private void initAttrs(@NonNull Context context, @Nullable AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoginPageView);
mColor = a.getColor(R.styleable.LoginPageView_mainColor, -1);
mVerifyCodeSize = a.getInt(R.styleable.LoginPageView_verifyCodeSize, SIZE_VERIFY_CODE_DEFAULT);
mCountDownDuration = a.getInt(R.styleable.LoginPageView_countDownDuration, DURATION_DEFAULT);
a.recycle();
}
private void initView() {
LayoutInflater.from(getContext()).inflate(R.layout.login_page_view, this);
mIsConfirm = this.findViewById(R.id.report_check_box);
mVerifyCodeInput = this.findViewById(R.id.verify_code_input_box);
if (mColor != -1) {
mIsConfirm.setTextColor(mColor);
}
mVerifyCodeInput.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mVerifyCodeSize)});
mLoginKeyboardView = this.findViewById(R.id.number_key_pad);
mPhoneNumInp = this.findViewById(R.id.phone_num_input_box);
mPhoneNumInp.requestFocus(); // 设置焦点在手机号码哪里
mGetVerifyCodeBtn = this.findViewById(R.id.get_verify_code_btn);
disableCopyAndPaste(mPhoneNumInp); // 禁止 EditText 选中复制粘贴
disableCopyAndPaste(mVerifyCodeInput); // 禁止 EditText 选中复制粘贴
mLoginBtn = this.findViewById(R.id.login_btn);
mAgreementView = this.findViewById(R.id.agreement_text_tips);
this.updateAllBtnState();
}
private void updateAllBtnState() {
if (!isCountDowning) {
mGetVerifyCodeBtn.setEnabled(isPhoneNumOk);
}
// 修改文字颜色
mAgreementView.setTextColor(isAgreementOk ? getResources().getColor(R.color.mainColor) : getResources().getColor(R.color.mainDeepColor));
mLoginBtn.setEnabled(isPhoneNumOk && isAgreementOk && isVerifyCodeOk);
}
/**
* 获取当前有焦点的输入框
* <p>
* 使用要注意判空
*
* @return null or EditText instanceof
*/
private EditText getFocusEdt() {
View view = this.findFocus();
if (view instanceof EditText) {
return (EditText) view;
}
return null;
}
public int getmColor() {
return mColor;
}
public void setmColor(int mColor) {
this.mColor = mColor;
}
public int getmVerifyCodeSize() {
return mVerifyCodeSize;
}
public void setmVerifyCodeSize(int mVerifyCodeSize) {
this.mVerifyCodeSize = mVerifyCodeSize;
}
public void setOnLoginPageActionListener(OnLoginPageActionListener listener) {
this.mActionListener = listener;
}
public interface OnLoginPageActionListener {
void onGetVerifyCodeClick(String phoneNum);
void onOpenAgreementClick();
void onConfirmClick(String verifyCode, String phoneNum);
}
// 禁止 EditText 选中复制粘贴
@SuppressLint("ClickableViewAccessibility")
public void disableCopyAndPaste(final EditText editText) {
try {
if (editText == null) {
return;
}
editText.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});
editText.setLongClickable(false);
editText.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// setInsertionDisabled when user touches the view
setInsertionDisabled(editText);
}
return false;
}
});
editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void setInsertionDisabled(EditText editText) {
try {
Field editorField = TextView.class.getDeclaredField("mEditor");
editorField.setAccessible(true);
Object editorObject = editorField.get(editText);
// if this view supports insertion handles
Class editorClass = Class.forName("android.widget.Editor");
Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
mInsertionControllerEnabledField.setAccessible(true);
mInsertionControllerEnabledField.set(editorObject, false);
// if this view supports selection handles
Field mSelectionControllerEnabledField = editorClass.getDeclaredField("mSelectionControllerEnabled");
mSelectionControllerEnabledField.setAccessible(true);
mSelectionControllerEnabledField.set(editorObject, false);
} catch (Exception e) {
e.printStackTrace();
}
}
public int getmCountDownDuration() {
return mCountDownDuration;
}
// 暴露出去进行设置
public void setmCountDownDuration(int mCountDownDuration) {
this.mCountDownDuration = mCountDownDuration;
}
}
代码讲解:
一开始,我们把view绑定到这个坑里头,也就是这句代码:
LayoutInflater.from(getContext()).inflate(R.layout.login_page_view, this); // 默认为true
// LayoutInflater.from(getContext()).inflate(R.layout.login_page_view, this,true); // 与上面等同
inflate,后面两个是重点,this,表示当前这个容器,也就是我们创建的LoginPageView,true表示绑定到这个容器里。所以,这行代码的意思是把这个布局文件里的内容,加载进来,放到这个ViewGrop里面,也就是放到LoginPageView里面。
键盘的输入等会再说,接着我们是初始化事件:
我们有什么事件呢?
1、监听两个输入框内容的变化,第一个输入框,只有匹配了手机号码的规则,获取验证码才可以适用,否则是disable,也就是不能点击,除非手机号码是对的;监听第二个输入框是用于判断有没有输入验证码,如果没有输入,那么,登录按钮不可以点击。
2、获取验证码这个按钮,点击以后要disable,并且开始倒计时,调用暴露的接口。
3、checkbox这个被点击以后,更新登录按钮是否可以点击,其实登录按钮要满足三个条件才可以点击:手机号码是对的,验证码有输入,同意适用协议。
4、登录按钮不需要做太多的动作,因为前面的动作已经做了相关的检查了。
布局文件的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:drawableLeft="@mipmap/user"
android:drawablePadding="20dp"
android:text="账号登录"
android:textColor="@color/mainColor"
android:textSize="30sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="10dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:orientation="vertical"
android:padding="15dp">
<EditText
android:id="@+id/phone_num_input_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_edit_text_bg"
android:drawableLeft="@mipmap/phone"
android:drawablePadding="10dp"
android:hint="请输入11位手机号码"
android:inputType="number"
android:maxLength="11"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp"
android:singleLine="true"
android:textColor="@color/white_bai"
android:textColorHint="#7E7E7E"
android:textCursorDrawable="@drawable/shape_edt_cursor"
android:textSize="24sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<EditText
android:id="@+id/verify_code_input_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_edit_text_bg"
android:drawableLeft="@mipmap/password"
android:drawablePadding="10dp"
android:hint="请输入验证码"
android:inputType="number"
android:paddingVertical="10dp"
android:paddingLeft="10dp"
android:paddingRight="160dp"
android:singleLine="true"
android:textColor="@color/white_bai"
android:textColorHint="#7E7E7E"
android:textCursorDrawable="@drawable/shape_edt_cursor"
android:textSize="24sp" />
<TextView
android:id="@+id/get_verify_code_btn"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="20dp"
android:gravity="center"
android:text="@string/text_get_verifyt_code"
android:textColor="@drawable/selector_text_color"
android:textSize="20sp" />
<View
android:layout_width="1dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginRight="6dp"
android:layout_toLeftOf="@id/get_verify_code_btn"
android:background="@drawable/shape_line_bg" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<CheckBox
android:id="@+id/report_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@null"
android:drawableLeft="@drawable/selector_check_box"
android:drawablePadding="10dp" />
<TextView
android:id="@+id/agreement_text_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_user_report"
android:textColor="@color/mainDeepColor"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/login_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/selector_btn_bg"
android:gravity="center"
android:padding="15dp"
android:text="@string/text_confirm"
android:textColor="@color/white_bai" />
</LinearLayout>
<com.lwm.customview.customview.loginpage.LoginKeyboardView
android:id="@+id/number_key_pad"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:padding="15dp" />
</LinearLayout>
</LinearLayout>
效果是这样子的:
这里面有一个键盘,它的代码其实是个view来的,跟上面一样,编写一个组合控件。
代码如下:
package com.lwm.customview.customview.loginpage;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.lwm.customview.R;
public class LoginKeyboardView extends LinearLayout implements View.OnClickListener {
private static final String TAG = "LoginKeyboard";
private OnKeyPressListener mKeyPressListener = null;
public LoginKeyboardView(Context context) {
this(context, null);
}
public LoginKeyboardView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoginKeyboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//
initView();
}
private void initView() {
LayoutInflater.from(getContext()).inflate(R.layout.num_key_pad, this);
this.findViewById(R.id.number_1).setOnClickListener(this);
this.findViewById(R.id.number_2).setOnClickListener(this);
this.findViewById(R.id.number_3).setOnClickListener(this);
this.findViewById(R.id.number_4).setOnClickListener(this);
this.findViewById(R.id.number_5).setOnClickListener(this);
this.findViewById(R.id.number_6).setOnClickListener(this);
this.findViewById(R.id.number_7).setOnClickListener(this);
this.findViewById(R.id.number_8).setOnClickListener(this);
this.findViewById(R.id.number_9).setOnClickListener(this);
this.findViewById(R.id.number_0).setOnClickListener(this);
this.findViewById(R.id.back).setOnClickListener(this);
}
@Override
public void onClick(View v) {
int viewId = v.getId();
if (mKeyPressListener == null) {
Log.d(TAG, "mKeyPressListener is null need not callback..");
return;
}
if (viewId == R.id.back) {
// 走back(处理删除按钮被点击了)
mKeyPressListener.onBackPress();
} else {
// 走数字结果通知
String text = ((TextView) v).getText().toString();
Log.d(TAG, "click text = " + text);
mKeyPressListener.onNumberPress(Integer.parseInt(text));
}
}
public void setOnKeyPressListener(OnKeyPressListener listener) {
this.mKeyPressListener = listener;
}
public interface OnKeyPressListener {
void onNumberPress(int number);
void onBackPress();
}
}
使用
使用很简单,直接把登录的View的全路径名称,复制到要适用的地方即可。
接着,我们在Activity的代码里找到这个View,给它设置相关的接口实现即可。
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); // 去掉标题栏,设置为全屏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_login);
LoginPageView loginPageView = this.findViewById(R.id.login_page_view);
loginPageView.setOnLoginPageActionListener(new LoginPageView.OnLoginPageActionListener() {
@Override
public void onGetVerifyCodeClick(String phoneNum) {
// todo:去获取验证码
Toast.makeText(LoginActivity.this, "验证码已发送", Toast.LENGTH_SHORT).show();
}
@Override
public void onOpenAgreementClick() {
// todo:打开协议页面
}
@Override
public void onConfirmClick(String verifyCode, String phoneNum) {
// todo:登录
App.getHandler().postDelayed(new Runnable() {
@Override
public void run() {
// 第一步:给个 Toast 提示
Toast.makeText(LoginActivity.this, "验证码错误", Toast.LENGTH_SHORT).show();
loginPageView.onVerifyCodeError();
}
},4000);
}
});
}
}
总结
- 点击获取验证码把号码给到外面即可。
- 点击确定以后,把所输入的号码和验证码告诉外面即可。
- 其他的号码校验,按键处理,内容检查内部处理好。
- 这就是自定义组合控件,把别人不关心的封装起来,把别人关心的暴露出去。