分享人:沈永輝
時 間:2020.5.15
問題:
xml 佈局層級(儘量減少佈局層級、儘量使用RelativeLayout)
修改前
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<LinearLayout
android:orientation="vertical" >
<com.threegene.module.health.ui.widget.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">
<com.threegene.module.health.ui.widget.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().checkRegister(...,new Callback<Void> {
@Override
public void onSuccess(int type, Void data, boolean immediately) {
dismissLoadingDialog();
// 不需要隱私協議,使用手機號+驗證碼登錄
registerBySMS();
}
@Override
public void onFail(int type, String errorMsg) {
dismissLoadingDialog();
if (type == UserService.NEED_ACCEPT_AGREEMENT_ERROR) {
// 顯示用戶隱私協議
showAgreementPrivacyPolicyDialog();
} else {
ToastMaster.shortToast(errorMsg);
}
}
});
/**
* 驗證碼登錄(獲取token)
*/
private void registerBySMS() {
UserService.getDefault().loginBySMS(..., 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){
//.....
//用戶選擇一個賬號
loginSuccessBySMS(loginResult.token);
}
// 獲取token成功後的操作
private void loginSuccessBySMS(String token) {
// 保存token 拉取用戶信息
UserService.getDefault().loadLoginInfo(new new Callback<Void> {
@Override
public void onSuccess(int type, Void data, boolean immediately) {
//跳轉到首頁
}
@Override
public void onFail(int type, String errorMsg) {
// 提示用戶登錄失敗
}
})
}
LoginService.class
public void checkRegister(...,CallBack callback){
UserAPI.checkRegister(..., 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 loginBySMS(...., final Callback<ResultMultipleAccount> callback) {
UserAPI.loginByCode(..., 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());
}
});
}
存在的問題: 登錄主流程被各種中斷 邏輯顯得混亂
第二版
- 修改checkRegister()的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().loginBySMS(... , 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 loginBySMS(..., final LoginCallback<Void> callback) { UserAPI.checkRegister(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 { //已註冊,直接登錄成過 innerLoginBySMS(pnum, vcode, vCodeToken, callback); } } @Override public void onError(HError error) { if (callback != null) { callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg()); } } }); /** * 通過手機號登錄/註冊 */ private void innerLoginBySMS(final String pnum, String vcode, String vCodeToken, final LoginCallback<Void> callback) { UserAPI.loginByCode(pnum, vcode, vCodeToken, 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邏輯集中