近期,笔者刚刚忙完手头上的项目,终于有时间整理一下项目中用到的技术,作为自己的工作笔记,也为有类似需求的读者提供参考。接下来的几篇文章,我将记录电商app中常见的部分功能点的实现。
本篇,我将介绍商品详情页规格(sku)选择弹框的实现。
我们先明确一下数据结构。首先,我们需要一个列表来展示每组规格的标题,以及每组规格的具体参数,其次,需要一个sku列表,sku应该包含库存、价格、skuId等信息。
public class Detail {
private List spec;
private List sku;
public List getSpec() {
return spec;
}
public void setSpec(List spec) {
this.spec = spec;
}
public List getSku() {
return sku;
}
public void setSku(List sku) {
this.sku = sku;
}
public static class SpecBean {
/**
* specName : 颜色
* specValue : ["黑色","红色","粉色","白色","蓝色"]
*/
private String specName;
private List specValue;
public String getSpecName() {
return specName;
}
public void setSpecName(String specName) {
this.specName = specName;
}
public List getSpecValue() {
return specValue;
}
public void setSpecValue(List specValue) {
this.specValue = specValue;
}
}
public static class SkuBean {
/**
* inventoryCount : 0
* id : 355
* spec : ["黑色","80g"]
*/
private int inventoryCount;
private int id;
private List 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 getSpec() {
return spec;
}
public void setSpec(List 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 lineList = new ArrayList();
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 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 viewList;//用于记录当前行所有TextView
int width;//用于记录当前line的宽,实际是当前所有子view的宽+水平间距
int height;//其实子view的高度
public Line() {
viewList = new ArrayList();
}
/**
* 记录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 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 {
private List data;
public SpecAdapter(List 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 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);
}
}
}
"1.0" encoding="utf-8"?>
"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">
"@+id/tv_title"
android:textSize="14sp"
android:textColor="#333"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
"@+id/fl_container"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
"match_parent"
android:layout_height="1px"
android:background="@color/grey_ccc"/>
可以看到,FlowLayout
有一个setAdapter(BaseAdapter adp)
方法,只需给它像ListView一样设置一个adapter就OK了,以下是adapter的代码:
class AttrAdapter extends BaseAdapter {
private int outPosition;//表示所在第几组参数
private List list;
public AttrAdapter( int position, List 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> outMap;记录每组参数的情况,键表示参数是第几组组
List 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 selectedMap = new HashMap<>();//已选的属性,key为属性序列,value为属性值
Iterator>> entries = outMap.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry> entry = entries.next();
List 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 matchedSku = new ArrayList<>();//筛选出符合 选中要求的sku
for (int i = 0; i < skuList.size(); i++) {
boolean matche = true;
Param.SkuBean sku = skuList.get(i);
Iterator> e = selectedMap.entrySet().iterator();
while (e.hasNext()) {
Map.Entry 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;
}
源代码已经上传github:https://github.com/VencentYChen/Mall