分享人:****
时 间:2020.5.15
问题:
xml 布局层级(尽量减少布局层级、尽量使用RelativeLayout)
修改前
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<LinearLayout
android:orientation="vertical" >
<HealthModuleTitleLinearLayout />
<LinearLayout
android:orientation="vertical">
<TextView />
<LinearLayout
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>
修改后:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<HealthModuleTitleView />
<LinearLayout
android:orientation="vertical">
<TextView />
<LinearLayout
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>
开发分享
组件复用
需求描述: 列表展示内容,
实现:
- RecycleView
- LinearLayout+Item
案例(主要讲述第二种实现):
第一版:
private void bindGoodView(LinearLayout container, List<Goods> goodsList) {
if (goodsList != null && !goodsList.isEmpty()) {
if (container.getVisibility() != View.VISIBLE) {
container.setVisibility(View.VISIBLE);
}
// 步骤 1: 清空容器内部子View
container.removeAllViews();
int size = goodsList.size();
for (int i = 0; i < size; i++) {
// 步骤 2: 创建子 View
View v = inflate(R.layout.item_order_good, holder.goodContainer);
// 步骤 3: 找到具体控件
RemoteImageView goodsImage = v.findViewById(R.id.goods_image);
TextView goodsName = v.findViewById(R.id.goods_name);
TextView goodsDesc = v.findViewById(R.id.goods_desc);
TextView goodsPriceDesc = v.findViewById(R.id.goods_price_desc);
// 步骤 4: 把数据设置到子View中
Goods goods = goodsList.get(i);
goodsImage.setCircleImageUri(goods.imgUrl, -1);
goodsName.setText(goods.productName);
goodsDesc.setText(goods.productDesc);
goodsPriceDesc.setText(String.format(Locale.CANADA, "%s 元", goods.unitAmount));
// 步骤 5:设置监听
v.setTag(goods);
v.setOnClickListener(this);
// 步骤 6:把子View添加到父容器中
container.addView(v);
}
} else {
holder.goodContainer.setVisibility(View.GONE);
}
}
第二版
子View复用:每次设置数据时 不清空容器内子View,而是取出复用.
private void bindGoodView(LinearLayout container, List<Goods> goodsList) {
if (goodsList != null && !goodsList.isEmpty()) {
if (container.getVisibility() != View.VISIBLE) {
container.setVisibility(View.VISIBLE);
}
int size = goodsList.size();
for (int i = 0; i < size; i++) {
// 步骤 1: 获取子View(创建新的View or 复用已有子View)
View v;
if (i < container.getChildCount()) {
// 复用已有子View
v = container.getChildAt(i);
if (v.getVisibility() != View.VISIBLE) {
v.setVisibility(View.VISIBLE);
}
} else {
// 创建新的View
v = inflate(R.layout.item_order_good, holder.goodContainer);
// 步骤 2: 把子View添加到父容器中
container.addView(v);
// 步骤 6:在创建新的View时 设置监听
v.setOnClickListener(this);
}
// 步骤 3: 找到具体控件
RemoteImageView goodsImage = v.findViewById(R.id.goods_image);
TextView goodsName = v.findViewById(R.id.goods_name);
TextView goodsDesc = v.findViewById(R.id.goods_desc);
TextView goodsPriceDesc = v.findViewById(R.id.goods_price_desc);
// 步骤 4: 把数据设置到子View中
Goods goods = goodsList.get(i);
goodsImage.setCircleImageUri(goods.imgUrl, -1);
goodsName.setText(goods.productName);
goodsDesc.setText(goods.productDesc);
goodsPriceDesc.setText(String.format(Locale.CANADA, "%s 元", goods.unitAmount));
// 步骤 5: 将数据与子View绑定
v.setTag(goods);
}
// 步骤 7: 将父容器中多余子View进行隐藏
for (int i = size; i < holder.goodContainer.getChildCount(); i++) {
holder.goodContainer.getChildAt(i).setVisibility(View.GONE);
}
} else {
holder.goodContainer.setVisibility(View.GONE);
}
}
第三版
思考: 第二版只是复用了一下子View 但是每次向容器中加子Item时还是需要为每个子View的每个控件findViewById(),这一步也很耗时,如何在复用子View时做到子View内部控件不再查找。
参考ListView的ViewHolder实现,把每个子View做成一个自定义的组件,组件内部处理。
自定义商品View :GoodsView.class
/**
* 商品View
* created by shenyonghui on 2020/5/15
*/
public class GoodsView extends LinearLayout {
private RemoteImageView goodsImage;
private TextView goodsName;
private TextView goodsDesc;
private TextView goodsPriceDesc;
public GoodsView(Context context) {
this(context, null);
}
public GoodsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GoodsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
inflate(getContext(), R.layout.item_order_good, this);
goodsImage = findViewById(R.id.goods_image);
goodsName = findViewById(R.id.goods_name);
goodsDesc = findViewById(R.id.goods_desc);
goodsPriceDesc = findViewById(R.id.goods_price_desc);
}
public void setImage(String url, @DrawableRes int defaultResource) {
goodsImage.setCircleImageUri(url, defaultResource);
}
public void setImage(String url) {
goodsImage.setCircleImageUri(url, -1);
}
public void setName(CharSequence str) {
if (str != null) {
goodsName.setText(str);
}
}
public void setDesc(CharSequence str) {
if (str != null) {
goodsDesc.setText(str);
}
}
public void setPrice(CharSequence str) {
if (str != null) {
goodsPriceDesc.setText(str);
}
}
}
private void bindGoodView(LinearLayout container, List<Goods> goodsList) {
if (order.orderItemInfoVo != null && !order.orderItemInfoVo.isEmpty()) {
if (holder.goodContainer.getVisibility() != View.VISIBLE) {
container.setVisibility(View.VISIBLE);
}
int goodsSize = goodsList.size();
for (int i = 0; i < goodsSize; i++) {
GoodsView goodsView;
if (i < container.getChildCount()) {
goodsView = (GoodsView) container.getChildAt(i);
if (goodsView.getVisibility() != View.VISIBLE) {
goodsView.setVisibility(View.VISIBLE);
}
} else {
goodsView = new GoodsView(container.getContext());
container.addView(goodsView);
}
BaseOrder.GoodItem good = goodsList.get(i);
goodsView.setImage(good.imgUrl);
goodsView.setName(good.productName);
goodsView.setDesc(good.productDesc);
goodsView.setPrice(String.format(Locale.CANADA, "%s 元", good.unitAmount));
}
for (int i = order.orderItemInfoVo.size(); i < holder.goodContainer.getChildCount(); i++) {
holder.goodContainer.getChildAt(i).setVisibility(View.GONE);
}
} else {
holder.goodContainer.setVisibility(View.GONE);
}
}
性能对比(时间)
一共69订单 每个订单共4个商品
最大时长 (ms) | 最小时长(ms) | 平均时长(ms) | |
---|---|---|---|
第一版 | 20 | 0 | 10 |
第三版 | 20 | 0 | 4 |
注:在内存维度的提升应该比时间维度更明显
登录流程代码优化
需求:
第一版
每次请求都是分开的 用不同的Callback
Login.class
// 判断用户状态
LoginService.getDefault().A(...,new Callback<Void> {
@Override
public void onSuccess(int type, Void data, boolean immediately) {
dismissLoadingDialog();
// 不需要隐私协议,使用手机号+验证码登录
B();
}
@Override
public void onFail(int type, String errorMsg) {
dismissLoadingDialog();
if (type == UserService.NEED_ACCEPT_AGREEMENT_ERROR) {
// 显示用户隐私协议
showAgreementPrivacyPolicyDialog();
} else {
ToastMaster.shortToast(errorMsg);
}
}
});
/**
* 验证码登录(获取token)
*/
private void B() {
UserService.getDefault().C(..., new Callback<ResultMultipleAccount>() {
@Override
public void onSuccess(int type, ResultMultipleAccount data, boolean immediately) {
if (data.loginResultList.size() == 1) {
// 该手机号只注册一次 不需要用户选择账号
ResultMultipleAccount.LoginResult loginResult = data.loginResultList.get(0);
loginSuccessBySMS(loginResult.token);
} else {
List<SelectAccountDialog.Account> accounts = new ArrayList<>();
for (ResultMultipleAccount.LoginResult account : data.loginResultList) {
accounts.add(new SelectAccountDialog.Account(account.token, account.loginType, account.childNameList));
}
// 有手机号有多个账号绑定 需用户进行选择
showSelectAccountDialog(accounts);
}
}
@Override
public void onFail(int type, String errorMsg) {
ToastMaster.shortToast(errorMsg);
}
});
}
//选择账号弹窗
private void showSelectAccountDialog(List<SelectAccountDialog.Account> accounts){
//.....
//用户选择一个账号
D(loginResult.token);
}
// 获取token成功后的操作
private void D(String token) {
// ...
}
LoginService.class
public void A(...,CallBack callback){
UserAPI.A(..., new JsonResponseListener<Boolean>() {
@Override
public void onSuccess(JsonResponse<Boolean> response) {
if (!response.getData()) {
// 需要用户隐私协议
callback.onFail(LOGIN_BY_NEED_ACCEPT_AGREEMENT, "");
}else{
// 不需要
callback.onSuccess(Callback.DEFAULT, null, true);
}
}
@Override
public void onError(HError error) {
// 请求失败
callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
}
});
}
/**
* 通过手机号登录/注册
*/
public void C(...., final Callback<ResultMultipleAccount> callback) {
UserAPI.C(..., new JsonResponseListener<ResultMultipleAccount>() {
@Override
public void onSuccess(JsonResponse<ResultMultipleAccount> response) {
if (response.getData() != null) {
if (response.getData().loginResultList == null || response.getData().loginResultList.isEmpty()) {
callback.onFail(LOGIN_BY_SMS_ERROR, "未获取到账号信息");
} else {
// 请求成功
callback.onSuccess(Callback.DEFAULT, response.getData(), true);
}
}
}
@Override
public void onError(HError error) {
callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
}
});
}
存在的问题: 登录主流程被各种中断 逻辑显得混乱
第二版
- 修改A()的Callback 增加一个
void onIntercept(int type, Chain chain);
回调 ,该回调用于执行被中断时回调。 - 新建Chain接口,用于从中断操作时从LoginService 获取数据及 通知LoginService继续操作
public interface Chain<T> { Object getData(); void process(T s); }
- LoginActivity 与Service交互
LoginActivity
UserService.getDefault().A(... , new SMSLoginCallback (){ @Override public void onSuccess(int type, Void data, boolean immediately) { //登录成功 进入首页 } @Override public void onFail(int type, String errorMsg) { // 登录失败 ToastMaster.shortToast(errorMsg); } @Override public void onIntercept(int type, Chain chain) { if (chain instanceof LoginAcceptAgreementChain) { dismissLoadingDialog(); LoginAcceptAgreementChain acceptAgreementChain = (LoginAcceptAgreementChain) chain; showAgreementPrivacyPolicyDialog(acceptAgreementChain); } else if (chain instanceof LoginMultipleAccountChain) { LoginMultipleAccountChain multipleAccountChain = (LoginMultipleAccountChain) chain; dismissLoadingDialog(); List<SelectAccountDialog.Account> accounts = new ArrayList<>(); for (ResultMultipleAccount.LoginResult account : multipleAccountChain.getData().loginResultList) { accounts.add(new SelectAccountDialog.Account(account.token, account.loginTypeStr, account.childNameList)); } showSelectAccountDialog(multipleAccountChain, accounts); } } }); /** * 用户协议弹窗 */ private void showAgreementPrivacyPolicyDialog(LoginAcceptAgreementChain chain) { // 弹窗显示用户协议 // 当用户点击同意时 回调到Service 通知继续执行 chain.process(null); } //选择账号弹窗 private void showSelectAccountDialog(LoginMultipleAccountChain multipleAccountChain){ //..... //用户选择一个账号token 回调到Service 通知继续执行 multipleAccountChain.process(token); }
Service
/** * 手机号 + 验证码 登录 * 第一步:判断手机号是否注册过 * 第二步:未注册,提示:同步用户协议,已注册,直接登录成过 */ public void A(..., final LoginCallback<Void> callback) { UserAPI.A(pnum, new JsonResponseListener<Boolean>() { @Override public void onSuccess(JsonResponse<Boolean> response) { if (!response.getData()) { if (callback != null) { //中断,同步用户协议 LoginAcceptAgreementChain chain = new LoginAcceptAgreementChain() { @Override public Object getData() { return null; } @Override public void process(String s) { innerLoginBySMS(pnum, vcode, vCodeToken, callback); } }; callback.onIntercept(LOGIN_INTERCEPT_NEED_ACCEPT_AGREEMENT, chain); } } else { //已注册,直接登录成过 B(pnum, vcode, vCodeToken, callback); } } @Override public void onError(HError error) { if (callback != null) { callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg()); } } }); /** * 通过手机号登录/注册 */ private void B(final String pnum, String vcode, String vCodeToken, final LoginCallback<Void> callback) { UserAPI.D(...., new JsonResponseListener<ResultMultipleAccount>() { @Override public void onSuccess(JsonResponse<ResultMultipleAccount> response) { final ResultMultipleAccount result = response.getData(); if (result != null) { if (result.loginResultList == null || result.loginResultList.isEmpty()) { callback.onFail(LOGIN_BY_SMS_ERROR, "未获取到账号信息"); } else if (result.loginResultList.size() == 1) { // 获取只有一个账号 不需要选账号 直接调回去用户接口 loadLoginInfo(callback); } else { LoginMultipleAccountChain chain = new LoginMultipleAccountChain() { @Override public ResultMultipleAccount getData() { return result; } @Override public void process(String token) { // 用户选择完账号后 回调 loadLoginInfo(callback); } }; callback.onIntercept(LOGIN_INTERCEPT_NEED_MULTIPLE_ACCOUNT, chain); } } else { callback.onFail(LOGIN_BY_SMS_ERROR, "未获取到账号信息"); } } @Override public void onError(HError error) { if (callback != null) { callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg()); } } }); }
在Service中 登录的几个接口是直线的调用 当被中断时把中断回调到Activity,当Activity处理完中断 再回调到Service 保证Service逻辑集中