logo头像
Snippet 博客主题

Android仿拼多多拼团堆叠头像

本文于335天之前发表,文中内容可能已经过时。

序言

做电商的都知道,作为商品的一种售卖方式,拼团是是提供商品售卖的一种及时有效的方式,而在拼团市场中,拼多多无疑是做的最好的一家。于是,研究拼多多的售卖方式之后,我们的产品也开始了这方面的开发。本文将要给大家介绍的就是通过自定义的方式实现堆叠头像,这种效果在直播app中非常常见。下面是部分效果:
这里写图片描述

通过分析,上面是一个使用ViewPager实现的一个可以左右无线循环的Galllery,相关实现可以访问我之前的介绍:PageTransformer使用简介
下面是一个列表的方式,可以通过下拉来加载更多的Cell数据,也比较简单。对于组合头像的实现也是比较简单的,其实就是一个简单的流式布局,在本篇实现上,本文也参考了张鸿洋的FlowLayout,对于流式布局来说,只要按照某种线性规则依次排列即可。

我相信很多朋友之前一定遇到过这种需求:富文本自动换行,如下所示:
这里写图片描述
要实现这种富文本换行,最重要的就是对onMeasure方法,通常的做法是,测量出子View的宽度,当大于屏幕的宽度的时候就换行(当然,需要考虑文字本来就很长,一行显示不下的情况)。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//AT_MOST
int width = 0;
int height = 0;
int rawWidth = 0;//当前行总宽度
int rawHeight = 0;// 当前行高
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if(child.getVisibility() == GONE){
if(i == count - 1){
//最后一个child
height += rawHeight;
width = Math.max(width, rawWidth);
}
continue;
}
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
Log.e("=====", "childWidth 1: " + childWidth);
if(rawWidth + childWidth > widthSpecSize - getPaddingLeft() - getPaddingRight()){
//换行
width = Math.max(width, rawWidth);
rawWidth = childWidth;
height += rawHeight;
rawHeight = childHeight;
} else {
rawWidth += childWidth;
rawHeight = Math.max(rawHeight, childHeight);
}
if(i == count - 1){
width = Math.max(rawWidth, width);
height += rawHeight;
}
}
setMeasuredDimension(
widthSpecMode == MeasureSpec.EXACTLY ? widthSpecSize : width + getPaddingLeft() + getPaddingRight(),
heightSpecMode == MeasureSpec.EXACTLY ? heightSpecSize : height + getPaddingTop() + getPaddingBottom()
);
}

实现

说了这么多,那么具体怎么实现的呢?由于时间关系,这里我就直接贴代码了。首先自定义ViewGrop,实现后一个头像会覆盖一部分到前一个头像上,为了方便使用者控制堆叠头像的重叠大小,我们通过自定义属性来解决。

PileView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public class PileView extends ViewGroup {
protected float vertivalSpace;//垂直间隙
protected float pileWidth=0;//重叠宽度
public PileView(Context context) {
this(context, null, 0);
}
public PileView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PileView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
}
private void initAttr(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PileLayout);
vertivalSpace = ta.getDimension(R.styleable.PileLayout_PileLayout_vertivalSpace, dp2px(4));
pileWidth = ta.getDimension(R.styleable.PileLayout_PileLayout_pileWidth, dp2px(10));
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//AT_MOST
int width = 0;
int height = 0;
int rawWidth = 0;//当前行总宽度
int rawHeight = 0;// 当前行高
int rowIndex = 0;//当前行位置
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if(child.getVisibility() == GONE){
if(i == count - 1){
//最后一个child
height += rawHeight;
width = Math.max(width, rawWidth);
}
continue;
}
//调用measureChildWithMargins 而不是measureChild
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if(rawWidth + childWidth - (rowIndex > 0 ? pileWidth : 0)> widthSpecSize - getPaddingLeft() - getPaddingRight()){
//换行
width = Math.max(width, rawWidth);
rawWidth = childWidth;
height += rawHeight + vertivalSpace;
rawHeight = childHeight;
rowIndex = 0;
} else {
rawWidth += childWidth;
if(rowIndex > 0){
rawWidth -= pileWidth;
}
rawHeight = Math.max(rawHeight, childHeight);
}
if(i == count - 1){
width = Math.max(rawWidth, width);
height += rawHeight;
}
rowIndex++;
}
setMeasuredDimension(
widthSpecMode == MeasureSpec.EXACTLY ? widthSpecSize : width + getPaddingLeft() + getPaddingRight(),
heightSpecMode == MeasureSpec.EXACTLY ? heightSpecSize : height + getPaddingTop() + getPaddingBottom()
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int viewWidth = r - l;
int leftOffset = getPaddingLeft();
int topOffset = getPaddingTop();
int rowMaxHeight = 0;
int rowIndex = 0;//当前行位置
View childView;
for( int w = 0, count = getChildCount(); w < count; w++ ){
childView = getChildAt(w);
if(childView.getVisibility() == GONE) continue;
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
// 如果加上当前子View的宽度后超过了ViewGroup的宽度,就换行
int occupyWidth = lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
if(leftOffset + occupyWidth + getPaddingRight() > viewWidth){
leftOffset = getPaddingLeft(); // 回到最左边
topOffset += rowMaxHeight + vertivalSpace; // 换行
rowMaxHeight = 0;
rowIndex = 0;
}
int left = leftOffset + lp.leftMargin;
int top = topOffset + lp.topMargin;
int right = leftOffset+ lp.leftMargin + childView.getMeasuredWidth();
int bottom = topOffset + lp.topMargin + childView.getMeasuredHeight();
childView.layout(left, top, right, bottom);
// 横向偏移
leftOffset += occupyWidth;
// 试图更新本行最高View的高度
int occupyHeight = lp.topMargin + childView.getMeasuredHeight() + lp.bottomMargin;
if(rowIndex != count - 1){
leftOffset -= pileWidth;
}
rowMaxHeight = Math.max(rowMaxHeight, occupyHeight);
rowIndex++;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
public float dp2px(float dpValue) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
}
}

自定义的属性如下:

1
2
3
4
<declare-styleable name="PileLayout">
<attr name="PileLayout_vertivalSpace" format="dimension"/>
<attr name="PileLayout_pileWidth" format="dimension"/>
</declare-styleable>

为了方便用户使用,我们在PileView的基础上再封装一下,封装完成后,只需要用户提供数据源即可实现头像堆叠。例如,下面的代码给控件设置数据源即可:

1
2
3
4
5
6
7
List<String> urls=new ArrayList<>();
urls.clear();
urls.add("http://ohe65w0xx.bkt.clouddn.com/u=2263418180,3668836868&fm=206&gp=0.jpg");
urls.add("http://ohe65w0xx.bkt.clouddn.com/u=3637404049,2821183587&fm=214&gp=0.jpg");
urls.add("http://ohe65w0xx.bkt.clouddn.com/avert.png");
//设置数据源
itemAvertView.setAvertImages(urls);

要完成上面的封装,主要会涉及如下的代码:
PileAvertView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class PileAvertView extends LinearLayout {
@BindView(R.id.pile_view)
PileView pileView;
private Context context = null;
public static final int VISIBLE_COUNT = 3;//默认显示个数
public PileAvertView(Context context) {
this(context, null);
this.context = context;
}
public PileAvertView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView();
}
private void initView() {
View view = LayoutInflater.from(context).inflate(R.layout.layout_group_pile_avert, this);
ButterKnife.bind(view);
}
public void setAvertImages(List<String> imageList) {
setAvertImages(imageList,VISIBLE_COUNT);
}
//如果imageList>visiableCount,显示List最上面的几个
public void setAvertImages(List<String> imageList, int visibleCount) {
List<String> visibleList = null;
if (imageList.size() > visibleCount) {
visibleList = imageList.subList(imageList.size() - 1 - visibleCount, imageList.size() - 1);
}
pileView.removeAllViews();
for (int i = 0; i < imageList.size(); i++) {
CircleImageView image= (CircleImageView) LayoutInflater.from(context).inflate(R.layout.item_group_round_avert, pileView, false);
CommonImageUtil.loadImage(imageList.get(i), image);
pileView.addView(image);
}
}
}

相关的布局:
layout_group_pile_avert.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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="match_parent"
android:gravity="center_horizontal">
<com.lanshan.shihuicommunity.grouppurchase.view.PileView
android:id="@+id/pile_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:PileLayout_pileWidth="10dp"/>
</LinearLayout>

item_group_round_avert.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<com.makeramen.rounded.CircleImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/circle_iamge"
android:layout_width="30dp"
android:layout_height="30dp"
android:orientation="vertical"
app:round_borderColor="#ffffff"
app:round_borderWidth="1dp" />

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者

上一篇