Customize a Parody Multiple Address Selector

Keywords: Android xml encoding Google

Preface

The company is developing a mall project, because of the project needs, it has made an address selector which imitates a lot of spelling, but there is some discrepancy with the implementation method of spelling many, the general effect is similar.Say nothing but the last action map:

<figcaption></figcaption>

start

  1. Let's start with some of the concepts in this article.Regional level: such as provincial, municipal, county and town levels, then this is at most level 4.
  2. Okay, let's analyze a wave of effect maps. When a level of area is selected, a new Tab is created, and when the last level of area is selected, no new Tab is created.If you go back and reselect a level of area, you remove the subsequent tabs and create a new one.Once selected, if you tap Tab to switch to the corresponding region level and scroll to the previously selected region display, creating a new Tab will scroll to the location of the first position by default.
  3. Next, let's look at the layout of our interface:
<?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 Title -->
   <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"/>
   <!-- First horizontal line under heading -->
   <View
       android:layout_width="match_parent"
       android:layout_height="1dp"
       android:background="#e6e6e6"
       android:layout_marginTop="17dp"/>
   <!-- Top 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 Horizontal line below -->
   <View
       android:layout_width="match_parent"
       android:layout_height="1dp"
       android:background="#e6e6e6"/>
   <!-- Displaying regional data 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>
  1. From the layout, we can see that I mostly rely on TabLayout plus RecyclerView to achieve this effect, and a lot of people guess that TabLayout plus RecyclerView plus ViewPager, so a lot of RecyclerView can slide sideways to the previous Tab page or the next, which is also the difference from the spelling effect.

Start code

  1. Start with the code by laying out the list of individual regions:
<?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">
    <!-- Show Area Name -->
    <TextView
        android:id="@+id/user_tv_address_dialog"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <!-- Show the check icon behind -->
    <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>
  1. Create the region as an entity object:
public class AddressItem {
    // Area Name
    private String address;
    // Check
    private boolean isChecked;
    // Region ID, what I need for this project is int type, you can modify it according to your project needs
    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 +
                '}';
    }
}
  1. Write the adapter for RecyclerView:
public class AddressAdapter extends RecyclerView.Adapter<AddressAdapter.MyViewHolder> {
    // Save a list of regional data
    private List<AddressItem> list = new ArrayList<>();
    // Custom Item Clicked to Listen for Events
    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);
    }
}
  1. Start by writing two BaseDialog s yourself. It's not nutritious, and the code is simple:
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) {
            // Remove the default interior margin of DecorView so that the layout fills the entire horizontal screen
            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) {
            // Remove margin s from window s in order to fill the screen with layouts
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
            layoutParams.horizontalMargin = 0;
            window.setAttributes(layoutParams);
        }
    }
}
  1. Then the focus is on customizing the address selector Dialog:
public class AddressBottomSheetDialog extends CustomBaseBottomSheetDialog {

    private TabLayout tabLayout;
    private AddressAdapter addressAdapter;

    private int maxLevel;   // The maximum number of levels can be customized by the setMaxLevel method
    private SparseArray<List<AddressItem>> levelList;     // Level List Data
    private SparseIntArray levelPosition;                 // List position selected at each level
    private SparseIntArray levelIds;                      // Address ID s selected at each level
    private String title;  // Title
    private String tabText = "Please select";                    // Text displayed by default for new Tab
    private TabSelectChangeListener changeListener;       // Tab's Selection Changed Listening

    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()) {   // If the selected level of List has no data, it is obtained by executing callbacks, otherwise it is directly multiplexed
                    addressAdapter.setList(list);
                    final int lastClickPositon = levelPosition.get(position, -1); // Gets the position of the last selected region, and returns -1 by default if it cannot be found
                    if (lastClickPositon >= 0) recyclerView.smoothScrollToPosition(lastClickPositon); // If last selected, RecyclerView scrolls to the specified position
                } else if (changeListener != null) {
                    changeListener.onSelectChange(position, levelIds.get(position));
                }
            }
            @Override
            public void onTabUnselected(TabLayout.Tab tab) {}
            @Override
            public void onTabReselected(TabLayout.Tab tab) {}
        });
        addressAdapter = new AddressAdapter();
        // List Single Click Events
        addressAdapter.setOnItemClickListener(position -> {
            final int selectedTabPosition = tabLayout.getSelectedTabPosition(); // position of selected Tab
            levelIds.put(selectedTabPosition, levelList.get(selectedTabPosition).get(position).getId()); // Update the ID of the selected area
            changeSelect(selectedTabPosition, position);
            levelPosition.put(selectedTabPosition, position); // Update the position of the selected area in the list
            setTabText(selectedTabPosition, levelList.get(selectedTabPosition).get(position).getAddress()); // Display the name of the selected area on the Tab
            if (selectedTabPosition < maxLevel - 1 && selectedTabPosition == tabLayout.getTabCount() - 1) { // Add a Tab if MaxLevel is not reached and the selected Tab is the last one, and RecyclerView scrolls to the top
                tabLayout.addTab(createTab(), true);
                recyclerView.smoothScrollToPosition(0);
            }
        });
        recyclerView.setLayoutManager(new LinearLayoutManager(context));
        recyclerView.setAdapter(addressAdapter);
        tabLayout.addTab(createTab(), true); // Add a Tab by default
    }

    // Create a tab of your choice and return
    private TabLayout.Tab createTab() {
        return tabLayout.newTab().setText(tabText);
    }

    // Method to execute when the RecyclerView entry is clicked
    private void changeSelect(int selectedTabPosition, int nowClickPosition) {
        // The last click on the current list saved. If the value is not found, return to -1 by default
        final int lastPosition = levelPosition.get(selectedTabPosition, -1);
        // If the previous and next click locations are the same, do not change
        if (nowClickPosition == lastPosition) {
            return;
        }
        // If it is not the last one and the level region is re-selected, remove the following Tab
        final int count = tabLayout.getTabCount();
        // If you have four Tab s, you remove the tenth one, and then the first one is not the first one.Because you removed the zero, the first one was in the zero position.So it would be wise to remove it upside down
        if (selectedTabPosition < count - 1) {
            TabLayout.Tab nowTab = tabLayout.getTabAt(selectedTabPosition);
            if (null != nowTab) nowTab.setText(tabText);
            for (int i = count - 1; i > selectedTabPosition; i--) {
                // Remove list data at the corresponding region level
                levelList.remove(i);
                // Reset the previously selected position to -1
                levelPosition.put(i, -1);
                // Reset previously recorded region ID to -1
                levelIds.put(i, -1);
                tabLayout.removeTabAt(i);
            }
        }
        // Set the currently selected region to already selected
        levelList.get(selectedTabPosition).get(nowClickPosition).setChecked(true);
        // Update List Individual Objects via adapter
        addressAdapter.notifyItemChanged(nowClickPosition);
        if (lastPosition >= 0) {
            // Mark the last selected area as unselected
            levelList.get(selectedTabPosition).get(lastPosition).setChecked(false);
            // Update List Individual Objects via adapter
            addressAdapter.notifyItemChanged(lastPosition);
        }
    }
    // Set the text of the tab number
    private void setTabText(int tabPosition, String text) {
        TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
        if (null != tab) tab.setText(text);
    }

    // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    /**
     *  Set Dialog Title
     * @param title Title Text
     */
    public void setDialogTitle(String title) {
        this.title = title;
    }

    /**
     *  Set the text that tab displays by default when no region has been selected under the current tab
     * @param tabDefaultText Text displayed by default
     */
    public void setTabDefaultText(String tabDefaultText) {
        this.tabText = tabDefaultText;
    }

    /**
     *  Set the maximum level of address (for example, province, city, county, town is the maximum level 4)
     * @param level Maximum Level
     */
    public void setMaxLevel(int level) {
        this.maxLevel = level;
    }

    /**
     *  Set the list data to display for the current level list
     * @param list List data
     * @param level Regional Level
     */
    public void setCurrentAddressList(List<AddressItem> list, int level) {
        levelList.put(level, list);
        addressAdapter.setList(list);
    }

    /**
     *  Set up monitoring for Tab click switching in Dialog
     * @param listener tab Switch Listening Implementation
     */
    public void setTabSelectChangeListener(@NonNull TabSelectChangeListener listener) {
        this.changeListener = listener;
    }

    /**
     *  Customized Tab Switch Listening Interface
     */
    public interface TabSelectChangeListener {
        void onSelectChange(int level, int parentId);
    }
}
  1. Usage method:
private void init() {
    mDialog = new AddressBottomSheetDialog(this);
    mDialog.setDialogTitle("Delivery to");
    mDialog.setMaxLevel(4);
    mDialog.setTabDefaultText("Please select");
    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 = "Unknown";
    switch (level) {
        case 0:
            levelTxt = "provincial level";
            break;
        case 1:
            levelTxt = "City level";
            break;
        case 2:
            levelTxt = "County-level";
            break;
        case 3:
            levelTxt = "Town level";
    }
    for (int i = 0; i < 32; i++) {
        AddressItem item = new AddressItem();
        item.setChecked(false);
        item.setAddress(levelTxt + i);
        list.add(item);
    }
    return list;
}  

summary

Although the code above has been commented in great detail, there are still some things you haven't covered in detail, such as what SparseArray is, and so on.

  1. What is SparseArray?A generic, SparseArray, is needed behind SparseArray, which can be interpreted as HashMap<Integer, T>.But why not use this with HashMap?SparseArray is Google's Map for Android, which saves memory and takes up less memory than HashMap.Previously, my practice was to have a list of provincial list data and a list of municipal data.Not only is this writing highly coupled, but users cannot customize what the largest regional level is, and switch ing judgments are necessary in the writing process.Later on, the position selected by Tab represents a level. It is not good to get the list of corresponding levels directly from Map.
  2. What is SparseIntArray?It's actually equivalent to SparseArray, and Google has encapsulated SparseArray for us with other basic data types, SparseBoolean Array and SpaseLongArray, all of which are used similarly.
  3. Why not use a member variable to record the position of the currently selected tab and update the member variable in onTabSelected?I did this before, but the odd thing is that TabLayout's line doesn't move up to the town level after a city reselect, after removing the tabs that follow and then reselecting the county level.For reasons unknown, guess the onTabSelected trigger timing may have caused the selected Tab's position to be updated in time.If you have a friend who knows about it, you can expect to be generous with your advice.As follows:
    Problems that arise.gif

[Appendix] Relevant Architecture and Data

Brochure Release Desk

Dot Zan+Group for Free Android IOC Architecture Design

Get Android Advanced Architecture materials, source code, notes, videos in advance.Advanced UI, Performance Optimization, Architect Course, NDK, ReactNative+Weex Wechat Applet, Flutter's all-round advanced Android practices, as well as technical bulls within the group to discuss communication and solve problems.

Posted by danger2oo6 on Sun, 19 May 2019 16:16:03 -0700