公司正在開發一個商城項目,由於項目須要,作了一個仿拼多多的地址選擇器,可是與拼多多實現方法有些出入,大致效果是差很少的。android
(2019年04月22日更新)最後決定仍是單獨提取出來作個demo給你們參考參考,地址:github.com/cyixlq/Addr…git
廢話很少說,先上一張效果動圖: github
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="560dp"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<!-- Dialog的標題 -->
<TextView
android:id="@+id/user_tv_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_gravity="center_horizontal"/>
<!-- 標題下的第一條橫線 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e6e6e6"
android:layout_marginTop="17dp"/>
<!-- 頂部的TabLayout -->
<android.support.design.widget.TabLayout
android:id="@+id/user_tb_dialog_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabSelectedTextColor="@color/colorPrimary"
app:tabGravity="fill"
app:tabMode="scrollable"/>
<!-- TabLayout下方的橫線 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e6e6e6"/>
<!-- 顯示地區數據的RecyclerView -->
<android.support.v7.widget.RecyclerView
android:id="@+id/user_rv_dialog_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
tools:ignore="UseCompoundDrawables">
<!-- 顯示地區名稱 -->
<TextView
android:id="@+id/user_tv_address_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!-- 顯示後面的勾選圖標 -->
<ImageView
android:id="@+id/user_iv_address_dialog"
android:layout_width="13dp"
android:layout_height="9dp"
android:src="@drawable/user_icon_address_check"
android:layout_marginStart="11dp"
android:layout_gravity="center_vertical"
android:visibility="gone"
tools:ignore="ContentDescription" />
</LinearLayout>
複製代碼
public class AddressItem {
// 地區名
private String address;
// 是否勾選
private boolean isChecked;
// 地區的ID,我這邊項目須要的是int型,你們能夠根據本身項目須要進行修改
private int id;
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public boolean isChecked() {
return this.isChecked;
}
public void setChecked(boolean checked) {
this.isChecked = checked;
}
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "AddressItem{" +
"address='" + address + '\'' + ", isChecked=" + isChecked + ", id=" + id + '}'; } } 複製代碼
public class AddressAdapter extends RecyclerView.Adapter<AddressAdapter.MyViewHolder> {
// 保存地區數據的列表
private List<AddressItem> list = new ArrayList<>();
// 自定義的單項被點擊監聽事件
private ItemClickListener listener;
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.user_item_address_bottom_sheet_dialog, viewGroup, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
AddressItem item = list.get(i);
if (item.isChecked()) {
myViewHolder.tvAddress.setText(item.getAddress());
myViewHolder.tvAddress.setTextColor(Color.parseColor("#1F83FF"));
myViewHolder.ivChecked.setVisibility(View.VISIBLE);
} else {
myViewHolder.tvAddress.setText(item.getAddress());
myViewHolder.tvAddress.setTextColor(Color.BLACK);
myViewHolder.ivChecked.setVisibility(View.GONE);
}
}
@Override
public int getItemCount() {
return this.list == null ? 0 : list.size();
}
public void setList(List<AddressItem> list) {
if (this.list != null && list != null) {
this.list.clear();
this.list.addAll(list);
this.notifyDataSetChanged();
}
}
public void setOnItemClickListener(@NonNull ItemClickListener listener) {
this.listener = listener;
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tvAddress;
ImageView ivChecked;
MyViewHolder(@NonNull View itemView) {
super(itemView);
tvAddress = itemView.findViewById(R.id.user_tv_address_dialog);
ivChecked = itemView.findViewById(R.id.user_iv_address_dialog);
if (listener != null) {
itemView.setOnClickListener(v -> listener.onItemClick(getAdapterPosition()));
}
}
}
public interface ItemClickListener {
void onItemClick(int position);
}
}
複製代碼
public abstract class CustomBaseDialog extends Dialog {
protected Context context;
public CustomBaseDialog(@NonNull Context context) {
super(context);
this.context = context;
}
protected abstract Integer getLayout();
protected abstract Integer getGravity();
protected abstract Integer getBackgroundRes();
protected abstract Integer getWindowAnimations();
protected abstract void initView();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getLayout() != null)
setContentView(getLayout());
Window window = getWindow();
if (window != null) {
// 去除DecorView默認的內邊距,好讓佈局佔滿整個橫向屏幕
View decorView = window.getDecorView();
decorView.setPadding(0,0,0,0);
if (getGravity() != null)
window.setGravity(getGravity());
else
window.setGravity(Gravity.CENTER);
if (getWindowAnimations() != null)
window.setWindowAnimations(getWindowAnimations());
if (getBackgroundRes() != null)
decorView.setBackgroundResource(getBackgroundRes());
}
initView();
}
protected void setClickListener(int id, View.OnClickListener listener) {
findViewById(id).setOnClickListener(listener);
}
}
public abstract class CustomBaseBottomSheetDialog extends CustomBaseDialog {
public CustomBaseBottomSheetDialog(@NonNull Context context) {
super(context);
}
@Override
protected Integer getGravity() {
return Gravity.BOTTOM;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
if (null != window) {
// 去除window的margin,目的也是爲了讓佈局佔滿屏幕
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.horizontalMargin = 0;
window.setAttributes(layoutParams);
}
}
}
複製代碼
public class AddressBottomSheetDialog extends CustomBaseBottomSheetDialog {
private TabLayout tabLayout;
private AddressAdapter addressAdapter;
private int maxLevel; // 最大有多少級的地區,能夠經過setMaxLevel方法進行自定義
private SparseArray<List<AddressItem>> levelList; // 級別列表數據
private SparseIntArray levelPosition; // 各個級別選中的列表position
private SparseIntArray levelIds; // 各個級別選擇的地址ID
private String title; // 標題
private String tabText = "請選擇"; // 新的Tab默認顯示的文本
private TabSelectChangeListener changeListener; // Tab的選擇被改變的監聽
public AddressBottomSheetDialog(@NonNull Context context) {
super(context);
}
@Override
protected Integer getLayout() {
return R.layout.user_layout_address_bottom_sheet_dialog;
}
@Override
protected Integer getBackgroundRes() {
return R.drawable.bg_dialog_bottom;
}
@Override
protected Integer getWindowAnimations() {
return R.style.DialogBottom;
}
@Override
protected void initView() {
levelList = new SparseArray<>();
levelPosition = new SparseIntArray();
levelIds = new SparseIntArray();
((TextView)findViewById(R.id.user_tv_dialog_title)).setText(title);
tabLayout = findViewById(R.id.user_tb_dialog_tab);
final RecyclerView recyclerView = findViewById(R.id.user_rv_dialog_list);
tabLayout.addOnTabSelectedListener(new TabLayout.BaseOnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
final int position = tab.getPosition();
List<AddressItem> list = levelList.get(position);
if (null != list && !list.isEmpty()) { // 若是選中級別的List沒有數據就經過執行回調來獲取,不然直接複用
addressAdapter.setList(list);
final int lastClickPositon = levelPosition.get(position, -1); // 獲取上一次選中的地區的position,若是找不到,默認返回-1
if (lastClickPositon >= 0) recyclerView.smoothScrollToPosition(lastClickPositon); // 若是上一次有選擇,RecyclerView滾動到指定position
} else if (changeListener != null) {
// 參數position表明的當前地區級別,父級地區ID應該選當前級別的上一個級別,若是沒有默認返回-1
changeListener.onSelectChange(position, levelIds.get(position -1, -1));
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {}
});
addressAdapter = new AddressAdapter();
// 列表單項點擊事件
addressAdapter.setOnItemClickListener(position -> {
final int selectedTabPosition = tabLayout.getSelectedTabPosition(); // 選中的Tab的position
levelIds.put(selectedTabPosition, levelList.get(selectedTabPosition).get(position).getId()); // 更新選中的地區的ID
changeSelect(selectedTabPosition, position);
levelPosition.put(selectedTabPosition, position); // 更新選中的地區在列表中的position
setTabText(selectedTabPosition, levelList.get(selectedTabPosition).get(position).getAddress()); // 將選中的地區的名字顯示在Tab上
if (selectedTabPosition < maxLevel - 1 && selectedTabPosition == tabLayout.getTabCount() - 1) { // 若是沒達到MaxLevel而且選中的Tab是最後一個就添加一個Tab,而且RecyclerView滾動到最頂部
tabLayout.addTab(createTab(), true);
recyclerView.smoothScrollToPosition(0);
}
});
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setAdapter(addressAdapter);
tabLayout.addTab(createTab(), true); // 默認添加一個Tab
}
// 建立一個請選擇的tab並返回
private TabLayout.Tab createTab() {
return tabLayout.newTab().setText(tabText);
}
// 當點擊了RecyclerView條目的時候執行的方法
private void changeSelect(int selectedTabPosition, int nowClickPosition) {
// 保存下來的當前列表上一個點擊位置.若是找不到該值,默認返回-1
final int lastPosition = levelPosition.get(selectedTabPosition, -1);
// 若是上一個點擊位置和下一個點擊位置相同,則不作改變
if (nowClickPosition == lastPosition) {
return;
}
// 若是不是最後一個而且又從新選擇了級別地區,移除後面的Tab
final int count = tabLayout.getTabCount();
// 這裏要倒過來移除Tab,否則會出現這樣的狀況,假如你有四個Tab,你移除第0個,接着移除第一個的話,第一個不是原來的第一個。由於你把第0個移除,原來的第一個就到了第0個的位置上。因此倒過來移除是明智的作法
if (selectedTabPosition < count - 1) {
TabLayout.Tab nowTab = tabLayout.getTabAt(selectedTabPosition);
if (null != nowTab) nowTab.setText(tabText);
for (int i = count - 1; i > selectedTabPosition; i--) {
// 將相應地區級別的列表數據移除
levelList.remove(i);
// 將以前選中的position重置爲-1
levelPosition.put(i, -1);
// 將以前記錄的地區ID重置爲-1
levelIds.put(i, -1);
tabLayout.removeTabAt(i);
}
}
// 將如今選擇的地區設置爲已經選中
levelList.get(selectedTabPosition).get(nowClickPosition).setChecked(true);
// 經過adapter更新列表單個對象
addressAdapter.notifyItemChanged(nowClickPosition);
if (lastPosition >= 0) {
// 將上一個選中的地區標記爲未選中
levelList.get(selectedTabPosition).get(lastPosition).setChecked(false);
// 經過adapter更新列表單個對象
addressAdapter.notifyItemChanged(lastPosition);
}
}
// 設置第幾個tab的文字
private void setTabText(int tabPosition, String text) {
TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
if (null != tab) tab.setText(text);
}
// ----------------------------- 如下是對外公開方法與接口 --------------------------
/**
* 設置Dialog的標題
* @param title 標題文字
*/
public void setDialogTitle(String title) {
this.title = title;
}
/**
* 設置在當前tab下還未選擇區域時候tab默認顯示的文字
* @param tabDefaultText 默認顯示的文字
*/
public void setTabDefaultText(String tabDefaultText) {
this.tabText = tabDefaultText;
}
/**
* 設置地址最大級別(如:省,市,縣,鎮的話就是最大4級)
* @param level 最大級別
*/
public void setMaxLevel(int level) {
this.maxLevel = level;
}
/**
* 設置當前級別列表須要顯示的列表數據
* @param list 列表數據
* @param level 地區級別
*/
public void setCurrentAddressList(List<AddressItem> list, int level) {
levelList.put(level, list);
addressAdapter.setList(list);
}
/**
* 設置Dialog中Tab點擊切換的監聽
* @param listener tab切換監聽實現
*/
public void setTabSelectChangeListener(@NonNull TabSelectChangeListener listener) {
this.changeListener = listener;
}
/**
* 自定義的Tab切換監聽接口
*/
public interface TabSelectChangeListener {
void onSelectChange(int level, int parentId);
}
}
複製代碼
private void init() {
mDialog = new AddressBottomSheetDialog(this);
mDialog.setDialogTitle("配送至");
mDialog.setMaxLevel(4);
mDialog.setTabDefaultText("請選擇");
mDialog.setTabSelectChangeListener((level, parentId) ->
mDialog.setCurrentAddressList(requestAddress(level, parentId), level)
);
binding.userIvSelectAddress.setOnClickListener(v -> mDialog.show());
}
private List<AddressItem> requestAddress(int level, int parentID) {
List<AddressItem> list = new ArrayList<>();
String levelTxt = "未知";
switch (level) {
case 0:
levelTxt = "省級";
break;
case 1:
levelTxt = "市級";
break;
case 2:
levelTxt = "縣級";
break;
case 3:
levelTxt = "鎮級";
}
for (int i = 0; i < 32; i++) {
AddressItem item = new AddressItem();
item.setChecked(false);
item.setAddress(levelTxt + i);
list.add(item);
}
return list;
}
複製代碼
雖然上面的代碼已經有很詳細的註釋,可是仍是有一些東西沒細講,好比SparseArray是什麼等等。bash
SparseArray<T>
,能夠理解爲是HashMap<Integer, T>。可是爲何不用HashMap而使用這個東西?SparseArray是谷歌專門爲安卓打造的Map,優勢是省內存,佔用內存沒HashMap大。以前個人作法是省級列表數據一個list,市級一個list。。。這種寫法,不但耦合度高,用戶也不能自定義最大的地區級別是多少,並且在寫法過程當中少不了各類switch判斷。後來靈機一動,Tab選中的position就是表明的一個級別,直接經過Map來取對應級別的list出來不就行了。SparseArray<Integer>
,谷歌還爲咱們封裝了其餘基本數據類型的SparseArray,它們就是SparseBooleanArray和SparseLongArray,用法都是類似的。