1. 程式人生 > >電商類app商品詳情引數選擇聯動的實現

電商類app商品詳情引數選擇聯動的實現

背景

近期,筆者剛剛忙完手頭上的專案,終於有時間整理一下專案中用到的技術,作為自己的工作筆記,也為有類似需求的讀者提供參考。接下來的幾篇文章,我將記錄電商app中常見的部分功能點的實現。

本篇,我將介紹商品詳情頁規格(sku)選擇彈框的實現。

效果圖:

sku選擇

分析:

  • 考慮到規格可能有多組,採用RecycleView來放置規格列表;
  • 單組規格有多個值,且個數不確定,每個規格值字數不確定,所以採用自定義的FlowLayout來放置;
  • 每個規格按鈕有3個要素:是否可選、是否已選、顯示的文字;
  • 每次按下按鈕,其他各組的每個按鈕都要重新計算其是否可選;
  • 需要新增按鈕點選的回撥函式。

實現:

我們先明確一下資料結構。首先,我們需要一個列表來展示每組規格的標題,以及每組規格的具體引數,其次,需要一個sku列表,sku應該包含庫存、價格、skuId等資訊。

public class Detail {

    private List<SpecBean> spec;
    private List<SkuBean> sku;

    public List<SpecBean> getSpec() {
        return spec;
    }

    public void setSpec(List<SpecBean> spec) {
        this
.spec = spec; } public List<SkuBean> getSku() { return sku; } public void setSku(List<SkuBean> sku) { this.sku = sku; } public static class SpecBean { /** * specName : 顏色 * specValue : ["黑色","紅色","粉色","白色","藍色"] */
private String specName; private List<String> specValue; public String getSpecName() { return specName; } public void setSpecName(String specName) { this.specName = specName; } public List<String> getSpecValue() { return specValue; } public void setSpecValue(List<String> specValue) { this.specValue = specValue; } } public static class SkuBean { /** * inventoryCount : 0 * id : 355 * spec : ["黑色","80g"] */ private int inventoryCount; private int id; private List<String> spec; public int getInventoryCount() { return inventoryCount; } public void setInventoryCount(int inventoryCount) { this.inventoryCount = inventoryCount; } public int getId() { return id; } public void setId(int id) { this.id = id; } public List<String> getSpec() { return spec; } public void setSpec(List<String> spec) { this.spec = spec; } } }

以下是部分json資料的截圖:

這裡寫圖片描述

其中,sku 中的spec 陣列所列的值是按外層的spec 所列順序排列的。
這裡寫圖片描述

這裡的先貼出FlowLayout的程式碼,其中註釋相當詳細,相信不難看懂。

public class FlowLayout extends ViewGroup {
    private int horizontalSpacing = 20;//水平間距
    private int verticalSpacing = 10;//垂直間距
    private AdapterView.OnItemClickListener itemClickListener;
    private DataSetObserver obv;
    private onSizeChangedCallBack callBack;
    private int height = 0;
    private int size = 0;

    private class ItemProxyClickListener implements OnClickListener {
        int pos;

        ItemProxyClickListener(int pos) {
            this.pos = pos;
        }

        @Override
        public void onClick(View v) {
            itemClickListener.onItemClick(null, v, pos, 0);
        }
    }

    //用於存放所有的line
    private ArrayList<Line> lineList = new ArrayList<Line>();
    private BaseAdapter adapter;

    public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context) {
        super(context);
    }

    /**
     * 設定水平間距
     *
     * @param horizontalSpacing
     */
    public void setHorizontalSpacing(int horizontalSpacing) {
        this.horizontalSpacing = horizontalSpacing;
    }

    /**
     * 設定垂直間距
     *
     * @param verticalSpacing
     */
    public void setVerticalSpacing(int verticalSpacing) {
        this.verticalSpacing = verticalSpacing;
    }

    public void setAdapter(BaseAdapter adp) {
        adapter = adp;
        FlowLayout.this.removeAllViews();
        int total = adapter.getCount();
        for (int i = 0; i < total; i++) {
            View v = adapter.getView(i, null, this);
            if (getItemClickListener() != null) {
                v.setOnClickListener(new ItemProxyClickListener(i));
            }
            addView(v);
        }
        obv = new DataSetObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                removeAllViews();
                int total = adapter.getCount();
                for (int i = 0; i < total; i++) {
                    View v = adapter.getView(i, null, FlowLayout.this);
                    if (getItemClickListener() != null) {
                        v.setOnClickListener(new ItemProxyClickListener(i));
                    }
                    addView(v);
                }
            }
        };
        adapter.registerDataSetObserver(this.obv);
    }

    public void setOnItemClickListener(
            AdapterView.OnItemClickListener itemClickListener) {
        this.itemClickListener = itemClickListener;
    }

    public AdapterView.OnItemClickListener getItemClickListener() {
        return itemClickListener;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        lineList.clear();
        //獲取總寬度,是包含paddingLeft和paddingRight
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //獲取用於比較的寬度,就是減去左右padding的寬度
        int noPaddingWidth = width - getPaddingLeft() - getPaddingRight();

        //遍歷所有的子TextView,根據寬度進行比較和分行
        Line line = null;
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //先測量childView,目的是保證能夠獲取到寬高
            childView.measure(0, 0);//系統發現傳的是0,0等非法值,則會按照TextView自己的寬高測量

            if (line == null) {
                line = new Line();//只要不換行,是同一個line,如果換行則是新的line
            }
            //1.如果當前line沒有子view,則直接將childView放入line中,不用判斷
            //因為要保證每行至少有一個子view
            if (line.getViewList().size() == 0) {
                line.addView(childView);
            } else if (line.getWidth() + childView.getMeasuredWidth() + horizontalSpacing > noPaddingWidth) {
                //2.說明childView換行,先儲存當前line,再建立新的line
                lineList.add(line);

                //將childView放入新的line中
                line = new Line();
                line.addView(childView);

            } else {
                //3.說明childView需要加到當前行
                line.addView(childView);
            }

            //如果當前childView是最後一個,則需要將最後的line儲存到lineList,否則會造成最後的line丟失
            if (i == (getChildCount() - 1)) {
                lineList.add(line);//將最後的line儲存起來
            }
        }

        //for迴圈結束後,我們有了存放好每行資料的lineList
        //計算FlowLayout需要的高度
        int height = getPaddingTop() + getPaddingBottom();//首先算上paddingTop和paddingBottom
        for (int i = 0; i < lineList.size(); i++) {
            height += lineList.get(i).getHeight();//再算上所有line的高度
        }

        height += (lineList.size() - 1) * verticalSpacing;//再加上所有行的垂直

        setMeasuredDimension(width, height);//向父view申請寬高的

    }

    /**
     * 遍歷所有的line,將每個line中的子TextView放置到對應的位置上
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        if (size == 0) {
            size = lineList.size();
        }
        for (int i = 0; i < lineList.size(); i++) {

            Line line = lineList.get(i);//獲取每個line

            if (i > 0) {
                //除去第一行之後的每行的top,都比上一行多一個行高和verticalSpacing
                paddingTop += line.getHeight() + verticalSpacing;
            }

            ArrayList<View> viewList = line.getViewList();//獲取每個line中的子view
            //1.計算留白區域
            float remainSpacing = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - line.getWidth();
            //2.計算每個子view可得到的spacing
            float perSpacing = remainSpacing / viewList.size();

            for (int j = 0; j < viewList.size(); j++) {
                View childView = viewList.get(j);//獲取每個子view
                //3.將perSpacing分到childView的寬度上面,就是需要重新測量childView
                int widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (childView.getMeasuredWidth()), MeasureSpec.EXACTLY);
                childView.measure(widthMeasureSpec, 0);//高度傳0,系統會按照它本身高度測量
                if (j == 0) {
                    //第一個子view的left是靠左和靠上擺放的
                    childView.layout(paddingLeft, paddingTop
                            , paddingLeft + childView.getMeasuredWidth()
                            , paddingTop + childView.getMeasuredHeight());
                } else {
                    View preChildView = viewList.get(j - 1);//獲取前一個子view
                    int left = preChildView.getRight() + horizontalSpacing;//childView的左邊
                    childView.layout(left, preChildView.getTop(),
                            left + childView.getMeasuredWidth(),
                            preChildView.getBottom());
                }
            }
        }
    }

    /**
     * 封裝每一行的TextView,
     *
     * @author Administrator
     */
    class Line {
        ArrayList<View> viewList;//用於記錄當前行所有TextView
        int width;//用於記錄當前line的寬,實際是當前所有子view的寬+水平間距
        int height;//其實子view的高度

        public Line() {
            viewList = new ArrayList<View>();
        }

        /**
         * 記錄view
         *
         * @param view
         */
        public void addView(View view) {
            //如果不包含才新增
            if (!viewList.contains(view)) {

                //每次addView的時候更新width
                if (viewList.size() == 0) {
                    //第一次新增
                    width = view.getMeasuredWidth();
                } else {
                    width += view.getMeasuredWidth() + horizontalSpacing;
                }
                //給高度賦值,在這裡高度都是一樣的
                height = Math.max(height, view.getMeasuredHeight());

                viewList.add(view);
            }
        }

        public ArrayList<View> getViewList() {
            return viewList;
        }

        public int getWidth() {
            return width;
        }

        public int getHeight() {
            return height;
        }
    }

    public interface onSizeChangedCallBack {
        void onSizeChanged(int height);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (callBack != null)
            if (height == 0) {
                callBack.onSizeChanged(h);
                height = h;
            }
    }

    public void setOnSizeChangedCallBack(onSizeChangedCallBack callBack) {
        this.callBack = callBack;
    }

    public int getlineSize() {
        return size;
    }
}

附上RecycleView的Adapter:

class SpecAdapter extends RecyclerView.Adapter<SpecAdapter.ViewHolder> {
        private List<Param.SpecBean> data;

        public SpecAdapter(List<Param.SpecBean> data) {
            this.data = data;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            ViewHolder holder = new ViewHolder(LayoutInflater.from(
                    context).inflate(R.layout.item_param_choice, parent,
                    false));
            return holder;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            Param.SpecBean specBean = data.get(position);
            holder.tvTile.setText(specBean.getSpecName());
            holder.flContainer.setHorizontalSpacing(30);
            holder.flContainer.setVerticalSpacing(20);
            ArrayList<ParameterEntity> list = new ArrayList<>();
            for (int i = 0; i < specBean.getSpecValue().size(); i++) {
                ParameterEntity entity = new ParameterEntity(specBean.getSpecValue().get(i));
                entity.enable = computEnable(position, entity.name);
                entity.selected = outMap.get(position).get(i).selected;
                list.add(entity);
            }
            AttrAdapter attrAdapter = new AttrAdapter(position, list);
            holder.flContainer.setAdapter(attrAdapter);
        }

        @Override
        public int getItemCount() {
            return data.size();
        }

        class ViewHolder extends RecyclerView.ViewHolder {
            TextView tvTile;
            FlowLayout flContainer;

            public ViewHolder(View itemView) {
                super(itemView);
                tvTile = (TextView) itemView.findViewById(R.id.tv_title);
                flContainer = (FlowLayout) itemView.findViewById(R.id.fl_container);
            }
        }
    }
<?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="wrap_content"
    android:background="@color/white"
    android:paddingLeft="15dp"
    android:paddingRight="15dp"
    android:paddingTop="15dp"
    android:orientation="vertical">
<TextView
    android:id="@+id/tv_title"
    android:textSize="14sp"
    android:textColor="#333"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    <com.wangku.demo.mall.FlowLayout
        android:id="@+id/fl_container"
        android:layout_marginTop="15dp"
        android:layout_marginBottom="15dp"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="@color/grey_ccc"/>
</LinearLayout>

可以看到,FlowLayout有一個setAdapter(BaseAdapter adp)方法,只需給它像ListView一樣設定一個adapter就OK了,以下是adapter的程式碼:

class AttrAdapter extends BaseAdapter {
        private int outPosition;//表示所在第幾組引數
        private List<ParameterEntity> list;
        public AttrAdapter( int position, List<ParameterEntity> list) {
            this.outPosition = position;
            this.list = list;
        }

        @Override
        public int getCount() {
            return list.size();
        }

        @Override
        public Object getItem(int position) {
            return list.isEmpty() ? null : list.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            View vi = convertView;
            final Holder holder;
            if (vi == null) {
                vi = LayoutInflater.from(context).inflate(R.layout.item_choice_button, null);
                holder = new Holder(vi);
                vi.setTag(holder);
            } else {
                holder = (Holder) vi.getTag();
            }
            ParameterEntity param = list.get(position);
            holder.tv.setText(param.name);
            holder.tv.setEnabled(param.enable);
            holder.tv.setSelected(param.selected);
            holder.tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (!v.isEnabled()) return;
                    //private HashMap<Integer, List<ParameterEntity>> outMap;記錄每組引數的情況,鍵表示引數是第幾組組
                    List<ParameterEntity> innerList = outMap.get(outPosition);
                    for (int i = 0; i < innerList.size(); i++) {
                        innerList.get(i).selected = false;
                    }
                    innerList.get(position).selected = !holder.tv.isSelected();
                    //通知RecycleView重新整理,重新計算每個TextView是否可以點選
                    adapter.notifyDataSetChanged();
                    //檢查是否所有引數都選好了
                    checkAllChecked();
                }
            });
            return vi;
        }
    }
public class ParameterEntity {
    public String name;
    public boolean enable = true;
    public boolean selected = false;

    public ParameterEntity(String name) {
        this.name = name;
    }
}

現在的難點在於,如何判斷FlowLayout中的每個TextView能否被點選。為此,我專門寫了一個方法:

/**
     * 計算是否可以點選
     *
     * @return
     */
    private boolean computEnable(int position, String spacValue) {
        boolean result = false;
        HashMap<Integer, String> selectedMap = new HashMap<>();//已選的屬性,key為屬性序列,value為屬性值
        Iterator<Map.Entry<Integer, List<ParameterEntity>>> entries = outMap.entrySet().iterator();
        while (entries.hasNext()) {
            Map.Entry<Integer, List<ParameterEntity>> entry = entries.next();
            List<ParameterEntity> value = entry.getValue();
            String selected = null;
            for (int i = 0; i < value.size(); i++) {
                if (value.get(i).selected) {
                    selected = value.get(i).name;
                }
            }
            if (selected != null && entry.getKey() != position) {//後一個條件使選中的屬性的兄弟屬性得以選擇
                selectedMap.put(entry.getKey(), selected);
            }
        }
        ArrayList<Param.SkuBean> matchedSku = new ArrayList<>();//篩選出符合 選中要求的sku
        for (int i = 0; i < skuList.size(); i++) {
            boolean matche = true;
            Param.SkuBean sku = skuList.get(i);
            Iterator<Map.Entry<Integer, String>> e = selectedMap.entrySet().iterator();
            while (e.hasNext()) {
                Map.Entry<Integer, String> next = e.next();
                if (!sku.getSpec().get(next.getKey()).equals(next.getValue())) {
                    matche = false;
                }
            }
            if (matche) {
                matchedSku.add(sku);
            }
        }
        //遍歷符合要求的sku,如果sku中有該選項,且庫存不為零,則可選
        for (int i = 0; i < matchedSku.size(); i++) {
            Param.SkuBean sku = matchedSku.get(i);
            if (sku.getSpec().get(position).equals(spacValue) && sku.getInventoryCount() >= qpl) {
                result = true;
            }
        }
        return result;
    }