電商類app商品詳情引數選擇聯動的實現
阿新 • • 發佈:2019-02-07
背景
近期,筆者剛剛忙完手頭上的專案,終於有時間整理一下專案中用到的技術,作為自己的工作筆記,也為有類似需求的讀者提供參考。接下來的幾篇文章,我將記錄電商app中常見的部分功能點的實現。
本篇,我將介紹商品詳情頁規格(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;
}