使用RecyclerView写树形结构的TreeRecyclerView
简介
Android是不提供树形控件的,如果需要使用树形控件,我们应该怎么做呢?
先看效果
上图是一个明显的树形结构
实现原理
在逻辑上,它们是包含关系,数据结构上是多叉树,这是毋庸置疑的。但是,显示的时候,我们有必要嵌套ListView或RecyclerView吗?当然没有必要!
- 每一而Item,在显示的时候,都是平级的,只是它们marginLeft不同而已。
- 更新marginLeft来体现它们的层级关系。marginLeft的值与item在逻辑上的深度有线性关系。
- 展开一个Item的时候,是动态的添加一系列的item。
- 收起一个Item的时候,我们是删除一系列的item.
好了,原理已经说明白了,那就看看源码怎么写吧。
注:
- 我们以android的文件系统的树形结构为例
- 为了动画的流畅性,我们使用RecyclerView,注意,ListView在添加和删除item时,是直接突变的。
Code
- 数据模型ItemData
public class ItemData implements Comparable<ItemData> {
public static final int ITEM_TYPE_PARENT = 0;
public static final int ITEM_TYPE_CHILD = 1;
private String uuid;
private int type;// 显示类型
private String text;
private String path;// 路径
private int treeDepth = 0;// 路径的深度
private List<ItemData> children;
private boolean expand;// 是否展开
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 父节点对应的ViewHolder
/**
* @Author Zheng Haibo
* @PersonalWebsite http://www.mobctrl.net
* @Description
*/
public class ParentViewHolder extends BaseViewHolder {
public ImageView image;
public TextView text;
public ImageView expand;
public TextView count;
public RelativeLayout relativeLayout;
private int itemMargin;
public ParentViewHolder(View itemView) {
super(itemView);
image = (ImageView) itemView.findViewById(R.id.image);
text = (TextView) itemView.findViewById(R.id.text);
expand = (ImageView) itemView.findViewById(R.id.expand);
count = (TextView) itemView.findViewById(R.id.count);
relativeLayout = (RelativeLayout) itemView.findViewById(R.id.container);
itemMargin = itemView.getContext().getResources()
.getDimensionPixelSize(R.dimen.item_margin);
}
public void bindView(final ItemData itemData, final int position,
final ItemDataClickListener imageClickListener) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) expand
.getLayoutParams();
params.leftMargin = itemMargin * itemData.getTreeDepth();
expand.setLayoutParams(params);
text.setText(itemData.getText());
if (itemData.isExpand()) {
expand.setRotation(45);
List<ItemData> children = itemData.getChildren();
if (children != null) {
count.setText(String.format("(%s)", itemData.getChildren()
.size()));
}
count.setVisibility(View.VISIBLE);
} else {
expand.setRotation(0);
count.setVisibility(View.GONE);
}
relativeLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (imageClickListener != null) {
if (itemData.isExpand()) {
imageClickListener.onHideChildren(itemData);
itemData.setExpand(false);
rotationExpandIcon(45, 0);
count.setVisibility(View.GONE);
} else {
imageClickListener.onExpandChildren(itemData);
itemData.setExpand(true);
rotationExpandIcon(0, 45);
List<ItemData> children = itemData.getChildren();
if (children != null) {
count.setText(String.format("(%s)", itemData
.getChildren().size()));
}
count.setVisibility(View.VISIBLE);
}
}
}
});
image.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
Toast.makeText(view.getContext(), "longclick",
Toast.LENGTH_SHORT).show();
return false;
}
});
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void rotationExpandIcon(float from, float to) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(from, to);
valueAnimator.setDuration(150);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
expand.setRotation((Float) valueAnimator.getAnimatedValue());
}
});
valueAnimator.start();
}
}
}
- 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
- 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
- 子节点对应的ViewHolder
/**
* @Author Zheng Haibo
* @PersonalWebsite http://www.mobctrl.net
* @Description
*/
public class ChildViewHolder extends BaseViewHolder {
public TextView text;
public ImageView image;
public RelativeLayout relativeLayout;
private int itemMargin;
private int offsetMargin;
public ChildViewHolder(View itemView) {
super(itemView);
text = (TextView) itemView.findViewById(R.id.text);
image = (ImageView) itemView.findViewById(R.id.image);
relativeLayout = (RelativeLayout) itemView.findViewById(R.id.container);
itemMargin = itemView.getContext().getResources()
.getDimensionPixelSize(R.dimen.item_margin);
offsetMargin = itemView.getContext().getResources()
.getDimensionPixelSize(R.dimen.expand_size);
}
public void bindView(final ItemData itemData, int position) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) image
.getLayoutParams();
params.leftMargin = itemMargin * itemData.getTreeDepth() + offsetMargin;
image.setLayoutParams(params);
text.setText(itemData.getText());
relativeLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
//TODO
}
});
}
}
- 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
- 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
- RecyclerView的Adapter
该部分处理item点击之后的展开和收起,实质上就是将其所有的Children节点动态的添加或删除。添加的位置就是item当前的位置。实现代码在onExpandChildren和onHideChildren方法中。
/**
* @Author Zheng Haibo
* @PersonalWebsite http://www.mobctrl.net
* @Description
*/
public class RecyclerAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private Context mContext;
private List<ItemData> mDataSet;
private OnScrollToListener onScrollToListener;
public void setOnScrollToListener(OnScrollToListener onScrollToListener) {
this.onScrollToListener = onScrollToListener;
}
public RecyclerAdapter(Context context) {
mContext = context;
mDataSet = new ArrayList<ItemData>();
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = null;
switch (viewType) {
case ItemData.ITEM_TYPE_PARENT:
view = LayoutInflater.from(mContext).inflate(
R.layout.item_recycler_parent, parent, false);
return new ParentViewHolder(view);
case ItemData.ITEM_TYPE_CHILD:
view = LayoutInflater.from(mContext).inflate(
R.layout.item_recycler_child, parent, false);
return new ChildViewHolder(view);
default:
view = LayoutInflater.from(mContext).inflate(
R.layout.item_recycler_parent, parent, false);
return new ChildViewHolder(view);
}
}
@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
switch (getItemViewType(position)) {
case ItemData.ITEM_TYPE_PARENT:
ParentViewHolder imageViewHolder = (ParentViewHolder) holder;
imageViewHolder.bindView(mDataSet.get(position), position,
imageClickListener);
break;
case ItemData.ITEM_TYPE_CHILD:
ChildViewHolder textViewHolder = (ChildViewHolder) holder;
textViewHolder.bindView(mDataSet.get(position), position);
break;
default:
break;
}
}
private ItemDataClickListener imageClickListener = new ItemDataClickListener() {
@Override
public void onExpandChildren(ItemData itemData) {
int position = getCurrentPosition(itemData.getUuid());
List<ItemData> children = getChildrenByPath(itemData.getPath(),
itemData.getTreeDepth());
if (children == null) {
return;
}
addAll(children, position + 1);// 插入到点击点的下方
itemData.setChildren(children);
if (onScrollToListener != null) {
onScrollToListener.scrollTo(position + 1);
}
}
@Override
public void onHideChildren(ItemData itemData) {
int position = getCurrentPosition(itemData.getUuid());
List<ItemData> children = itemData.getChildren();
if (children == null) {
return;
}
removeAll(position + 1, getChildrenCount(itemData) - 1);
if (onScrollToListener != null) {
onScrollToListener.scrollTo(position);
}
itemData.setChildren(null);
}
};
@Override
public int getItemCount() {
return mDataSet.size();
}
private int getChildrenCount(ItemData item) {
List<ItemData> list = new ArrayList<ItemData>();
printChild(item, list);
return list.size();
}
private void printChild(ItemData item, List<ItemData> list) {
list.add(item);
if (item.getChildren() != null) {
for (int i = 0; i < item.getChildren().size(); i++) {
printChild(item.getChildren().get(i), list);
}
}
}
/**
* 根据路径获取子目录或文件
*
* @param path
* @param treeDepth
* @return
*/
public List<ItemData> getChildrenByPath(String path, int treeDepth) {
treeDepth++;
try {
List<ItemData> list = new ArrayList<ItemData>();
File file = new File(path);
File[] children = file.listFiles();
List<ItemData> fileList = new ArrayList<ItemData>();
for (File child : children) {
if (child.isDirectory()) {
list.add(new ItemData(ItemData.ITEM_TYPE_PARENT, child
.getName(), child.getAbsolutePath(), UUID
.randomUUID().toString(), treeDepth, null));
} else {
fileList.add(new ItemData(ItemData.ITEM_TYPE_CHILD, child
.getName(), child.getAbsolutePath(), UUID
.randomUUID().toString(), treeDepth, null));
}
}
Collections.sort(list);
Collections.sort(fileList);
list.addAll(fileList);
return list;
} catch (Exception e) {
}
return null;
}
/**
* 从position开始删除,删除
*
* @param position
* @param itemCount
* 删除的数目
*/
protected void removeAll(int position, int itemCount) {
for (int i = 0; i < itemCount; i++) {
mDataSet.remove(position);
}
notifyItemRangeRemoved(position, itemCount);
}
protected int getCurrentPosition(String uuid) {
for (int i = 0; i < mDataSet.size(); i++) {
if (uuid.equalsIgnoreCase(mDataSet.get(i).getUuid())) {
return i;
}
}
return -1;
}
@Override
public int getItemViewType(int position) {
return mDataSet.get(position).getType();
}
public void add(ItemData text, int position) {
mDataSet.add(position, text);
notifyItemInserted(position);
}
public void addAll(List<ItemData> list, int position) {
mDataSet.addAll(position, list);
notifyItemRangeInserted(position, list.size());
}
}
- 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
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 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
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 在MainActivity中调用
由于使用的是RecyclerView,在动态添加和删除孩子节点时,会有明显的“展开”和“收起”效果。
/**
* @Author Zheng Haibo
* @PersonalWebsite http://www.mobctrl.net
* @Description
*/
public class MainActivity extends Activity {
private RecyclerView recyclerView;
private RecyclerAdapter myAdapter;
private LinearLayoutManager linearLayoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.getItemAnimator().setAddDuration(100);
recyclerView.getItemAnimator().setRemoveDuration(100);
recyclerView.getItemAnimator().setMoveDuration(200);
recyclerView.getItemAnimator().setChangeDuration(100);
myAdapter = new RecyclerAdapter(this);
recyclerView.setAdapter(myAdapter);
myAdapter.setOnScrollToListener(new OnScrollToListener() {
@Override
public void scrollTo(int position) {
recyclerView.scrollToPosition(position);
}
});
initDatas();
}
private void initDatas() {
List<ItemData> list = myAdapter.getChildrenByPath("/", 0);
myAdapter.addAll(list, 0);
}
}
- 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
- 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
Project
Demo的Github地址:https://github.com/nuptboyzhb/TreeRecyclerView
@Author: Zheng Haibo 莫川

浙公网安备 33010602011771号