IBigerBiger的成长之路

Android 堆叠式布局实现(一)简单堆叠式布局

写在前面的话

堆叠式布局,顾明思议,堆在一起的布局,我百度了一下好像还真没有太多关于这个方面的讲解,刚好最近学习的时候有看到这个方面的知识,那就把学习相关的东西记录下来,方便别人的学习,也对自己后面知识的巩固和温习有帮助。

首先给大家看看使用到堆叠式布应用的场景,


图1 大街App堆叠式布应用场景

图2 易信App堆叠式布应用场景

图3 某个Gif设计图实现效果

看完以后大家是不是还是觉得蛮酷炫的,有的其实还蛮复杂的,但是其实实现一个简单的堆叠式的布局没有想象的复杂,实际上就是自定义的layout来实现的,从简单到复杂,这一篇文章呢就实现一个简单一点的堆叠式的布局,让大家对这个方面有一定的了解

首先看下最后实现的效果


图4 简单堆叠式布局的实现

看起来是不是感觉效果还蛮不错的,其实实现起来也不复杂,很简单

那么我们根据这个图来分析下怎么实现这个效果

抛开动画效果,我们发现当移动第一个item的时候后面的第二个item其实是已经加载好了的,同样的当第二个item移动的时候第三个也是加载好了的,什么触发第三个加载好呢?显然是第一个消失的时候取触发的,这个是不是有点像ListView/RecycleView的效果,同样实现呢也是用这种方式来实现的,可以理解为一个简易版本的ListView/RecycleView。

堆叠式布局的实现

接下来就一步一步实现上述的效果了

Step1.新建一个自定义的StackLayout与Adapter

StackLayout.Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StackLayout extends FrameLayout{
public StackLayout(Context context) {
this(context,null);
}
public StackLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}

StackAdapter.Class

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
public class StackAdapter extends ArrayAdapter{
public StackAdapter(Context context) {
super(context, R.layout.item_stack);
}
@Override
public View getView(int position, final View convertView, final ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_stack, parent, false);
}
final String name = (String)getItem(position);
((TextView)view.findViewById(R.id.name)).setText(name);
final View completeView = view.findViewById(R.id.completeCommand);
view.setTag(name);
completeView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
remove(name);
notifyDataSetChanged();
}
});
return view;
}
}

Adapter的方法很简单,就是显示了xml里面的布局,旁边的View按下的是时候会把当前的Item移除掉,然后notifyDataSetChanged 让Adapter去更新

Step2.StackLayout与Adapter建立连接

首先在StackLayout中添加连接SetAdapter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setAdapter(ArrayAdapter adapter){
if (adapter == null){
throw new IllegalArgumentException("adapter not null");
}
if (this.adapter != null){
this.adapter.unregisterDataSetObserver(dataSetObserver);
}
this.adapter = adapter;
this.adapter.registerDataSetObserver(dataSetObserver);
viewsBuffer = new View[adapter.getCount()];
attachChildViews();
}

这里面呢主要是两个方法,第一个是registerDataSetObserver,另一个是attachChildViews,首先我们看一下registerDataSetObserver,这个的作用是当Adpater产生变化的时候会在dataSetObserver中监听到,我们通过这里来更新当Adpater变化时候的需要进行改变的东西

我们来看下dataSetObserver中的东西

1
2
3
4
5
6
7
private DataSetObserver dataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
attachChildViews();
}
};

这里监听到以后,其实也是调用了attachChildViews方法,那我们看一下这里面应该做些什么

1
2
3
4
5
6
7
8
9
10
private void attachChildViews(){
removeAllViews();
for (int position = 0 ; position < adapter.getCount(); position ++ ){
if (position < 2){
viewsBuffer[position] = adapter.getView(position,viewsBuffer[position],this);
addViewInLayout(viewsBuffer[position], 0, viewsBuffer[position].getLayoutParams());
}
}
requestLayout();
}

这里主要是根据自己业务的需求去做不同的改变,因为这个小的Demo只需要加载两个item就好了,所以这里当position小于3的时候会去从Adapter中拿view,然后把view添加到Framelayout中。

最后呢将两者连接起来

1
2
3
4
5
6
7
8
9
10
StackLayout stackLayout = (StackLayout) findViewById(R.id.statck_layout);
final StackAdapter stackAdapter = new StackAdapter(this);
stackAdapter.add("1");
stackAdapter.add("2");
stackAdapter.add("3");
stackAdapter.add("4");
stackAdapter.add("5");
stackAdapter.add("6");
stackAdapter.add("7");
stackLayout.setAdapter(stackAdapter);

到这里呢一个简单的堆叠式的布局其实已经完成了

看看效果


图5 简单堆叠式效果一

我们可以通过hierarchyviewer来看一下布局的内容,是否StackLayout里面有两个子的layout


图6 StackLayout中布局层次

可以看到布局也是正确的

Step3.StackLayout添加动画效果

其实这个部分有点感觉是赠送的,哈哈,因为其实上面两步就已经实现了

在将item添加到layout的时候给item添加onTouchListener

如下

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
addViewInLayout(viewsBuffer[position], 0, viewsBuffer[position].getLayoutParams());
private void initEvent(final View item)
{
//设置item的重心,主要是旋转的中心
item.setPivotX(getScreenWidth(getContext()) / 2);
item.setPivotY(getScreenHeight(getContext()) * 2);
item.setOnTouchListener(new View.OnTouchListener() {
float touchX, distanceX;//手指按下时的坐标以及手指在屏幕移动的距离
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
distanceX = event.getRawX() - touchX;
item.setRotation(distanceX * mRotateFactor);
//alpha scale 1~0.1
//item的透明度为从1到0.1
item.setAlpha(1 - (float) Math.abs(mItemAlphaFactor * distanceX));
break;
case MotionEvent.ACTION_UP:
if (Math.abs(distanceX) > mLimitTranslateX) {
//移除view
removeViewWithAnim(item, distanceX < 0);
} else {
//复位
item.setRotation(0);
item.setAlpha(1);
}
break;
}
return true;
}
});
}
public void removeViewWithAnim( final View view, boolean isLeft)
{
view.animate()
.alpha(0)
.rotation(isLeft ? -90 : 90)
.setDuration(400).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
adapter.remove(view.getTag());
adapter.notifyDataSetChanged();
}
});
}

那这里的代码就不做解释了,因为大家应该也都懂得是做了什么的

效果呢如下


图7 简单堆叠式效果二

最后呢贴上整个代码

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
150
151
152
153
154
155
156
157
158
159
package com.demo.stackdemo;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.Toast;
/**
* Created by xinyuanhxy on 16/4/8.
*/
public class StackLayout extends FrameLayout{
private ArrayAdapter adapter;
private View[] viewsBuffer;
private float mRotateFactor;//控制item旋转范围
private double mItemAlphaFactor;//控制item透明度变化范围
private int mLimitTranslateX = 100;//限制移动距离,当超过这个距离的时候,删除该item
private DataSetObserver dataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
attachChildViews();
}
};
public StackLayout(Context context) {
this(context,null);
}
public StackLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
int screenWidth = getScreenWidth(getContext());
mRotateFactor = 60 * 1.0f / screenWidth;
//左滑,透明度最少到0.1f
mItemAlphaFactor = 0.9 * 1.0f / screenWidth / 2;
}
public void setAdapter(ArrayAdapter adapter){
if (adapter == null){
throw new IllegalArgumentException("adapter not null");
}
if (this.adapter != null){
this.adapter.unregisterDataSetObserver(dataSetObserver);
}
this.adapter = adapter;
this.adapter.registerDataSetObserver(dataSetObserver);
viewsBuffer = new View[adapter.getCount()];
attachChildViews();
}
private void attachChildViews(){
removeAllViews();
for (int position = 0 ; position < adapter.getCount(); position ++ ){
if (position < 2){
viewsBuffer[position] = adapter.getView(position,viewsBuffer[position],this);
viewsBuffer[position].setRotation(0);
viewsBuffer[position].setAlpha(1);
addViewInLayout(viewsBuffer[position], 0, viewsBuffer[position].getLayoutParams());
initEvent(adapter.getView(position,viewsBuffer[position],this));
}
}
requestLayout();
}
private void initEvent(final View item)
{
//设置item的重心,主要是旋转的中心
item.setPivotX(getScreenWidth(getContext()) / 2);
item.setPivotY(getScreenHeight(getContext()) * 2);
item.setOnTouchListener(new View.OnTouchListener() {
float touchX, distanceX;//手指按下时的坐标以及手指在屏幕移动的距离
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
distanceX = event.getRawX() - touchX;
item.setRotation(distanceX * mRotateFactor);
//alpha scale 1~0.1
//item的透明度为从1到0.1
item.setAlpha(1 - (float) Math.abs(mItemAlphaFactor * distanceX));
break;
case MotionEvent.ACTION_UP:
if (Math.abs(distanceX) > mLimitTranslateX) {
//移除view
removeViewWithAnim(item, distanceX < 0);
} else {
//复位
item.setRotation(0);
item.setAlpha(1);
}
break;
}
return true;
}
});
}
public void removeViewWithAnim( final View view, boolean isLeft)
{
view.animate()
.alpha(0)
.rotation(isLeft ? -90 : 90)
.setDuration(400).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
adapter.remove(view.getTag());
adapter.notifyDataSetChanged();
}
});
}
public static int getScreenWidth(Context context)
{
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
public static int getScreenHeight(Context context)
{
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
}
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
package com.demo.stackdemo;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.content_main);
StackLayout stackLayout = (StackLayout) findViewById(R.id.statck_layout);
final StackAdapter stackAdapter = new StackAdapter(this);
stackAdapter.add("1");
stackAdapter.add("2");
stackAdapter.add("3");
stackAdapter.add("4");
stackAdapter.add("5");
stackAdapter.add("6");
stackAdapter.add("7");
stackLayout.setAdapter(stackAdapter);
}
public class StackAdapter extends ArrayAdapter{
public StackAdapter(Context context) {
super(context, R.layout.item_stack);
}
@Override
public View getView(int position, final View convertView, final ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_stack, parent, false);
}
final String name = (String)getItem(position);
((TextView)view.findViewById(R.id.name)).setText(name);
final View completeView = view.findViewById(R.id.completeCommand);
view.setTag(name);
completeView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
remove(name);
notifyDataSetChanged();
}
});
return view;
}
}
}

大工告成

写在后面的几句话

到这里呢,一个简单的堆叠式布局就实现拉,但是,如果要实现更复杂的效果该怎么做呢?下一篇会对這方面进行详细的讲解,ps:语言组织能力一般,见谅~~~