在上一篇博客《Android 使用SwipeActionAdapter开源库实现简单列表的左右滑动操作》里,已经介绍了利用SwipeActionAdapter来左右滑动操作列表; 然,有时候,会要求一些特殊的列表也能够实现左右滑动: 列表滑动过程中,分组标题可以固定在顶部,同时列表支持左右滑动!效果图如下:
StickyListView 来做,尝试一番后,发现左右滑动ListView Item时,它的背景(上图滑动时出现的颜色)不显示不显示了,怎么也解决不了,后来在github上也搜索了一番,也发现了这么一个项目https://github.com/he667/StickyListSwipe,同样是想使用SwipeActionAdapter+StickyListView来实现上图的效果,然,该项目也遇到了背景不显示的情况,且没有解决,无奈之下舍弃了该项目,决定使用SwipeActionAdapter+Pinnedheaderlistview,实现了上图所示效果。
SwipeActionAdapter该库,请看我上一篇博客《Android 使用SwipeActionAdapter开源库实现简单列表的左右滑动操作》;至于Pinnedheaderlistview的介绍,可以看下《Android PinnedHeaderListView 详解》这篇博客。
先熟悉下这两个库的使用,然后在跟着继续学习如何把这两个库融合到一起实现上面的效果!
1. 创建项目,分别导入SwipeActionAdapter和Pinnedheaderlistview两个库,这样方便直接对源码进行修改。
SwipeActionAdapter和Pinnedheaderlistview之间也会有些依赖关系,所以我精简了一些,直接把SwipeActionAdapter拷贝到sample模块下,方便修改引用。(到最后下载源码即可看到最重的项目)
2. 使用Pinnedheaderlistview,创建头部可悬浮显示的列表
2.1 列表适配器
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.List;
import bean.HeaderItem;
import za.co.immedia.pinnedheaderlistview.SectionedBaseAdapter;
public class TestSectionedAdapter extends SectionedBaseAdapter {
List<HeaderItem> headers;
public TestSectionedAdapter(List<HeaderItem> headers) {
this.headers = headers;
}
@Override
public Object getItem(int section, int position) {
return null;
}
@Override
public long getItemId(int section, int position) {
return 0;
}
@Override
public int getSectionCount() {
return headers.size();
}
@Override
public int getCountForSection(int section) {
return headers.get(section).dataItem.size();
}
@Override
public View getItemView(int section, int position, View convertView, ViewGroup parent) {
LinearLayout layout = null;
if (convertView == null) {
LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layout = (LinearLayout) inflator.inflate(R.layout.list_item, null);
} else {
layout = (LinearLayout) convertView;
}
// ((TextView) layout.findViewById(R.id.textItem)).setText("Section " + section + " Item " + position);
((TextView) layout.findViewById(R.id.textItem)).setText(headers.get(section).dataItem.get(position).name);
return layout;
}
@Override
public View getSectionHeaderView(int section, View convertView, ViewGroup parent) {
LinearLayout layout = null;
if (convertView == null) {
LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layout = (LinearLayout) inflator.inflate(R.layout.header_item, null);
} else {
layout = (LinearLayout) convertView;
}
// ((TextView) layout.findViewById(R.id.textItem)).setText("Header for section " + section);
((TextView) layout.findViewById(R.id.textItem)).setText(headers.get(section).name);
return layout;
}
}
《Android PinnedHeaderListView 详解》)
2.2 填充数据,实现基本的悬浮头部列表
public class MainActivity extends Activity {
List<HeaderItem> headers = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
private void initView() {
PinnedHeaderListView listView = (PinnedHeaderListView) findViewById(R.id.pinnedListView);
SectionedBaseAdapter sectionedAdapter = new TestSectionedAdapter(headers);
listView.setAdapter(sectionedAdapter);
}
private void initData() {
for (int i = 0; i < 5; i++) {
HeaderItem headerItem = new HeaderItem();
List<DataItem> datas = new ArrayList<>();
for (int j = 0; j < 5; j++) {
DataItem dataItem = new DataItem();
dataItem.name = " 列表数据 " + j;
datas.add(dataItem);
}
headerItem.name = "标题 " + i;
headerItem.dataItem = datas;
headers.add(headerItem);
}
}
}
3. 在Pinnedheaderlistview基础上,扩展SwipeActionAdapter
public class SwipeActionAdapter extends DecoratorAdapter implements
SwipeActionTouchListener.ActionCallbacks, PinnedHeaderListView.PinnedSectionedHeaderAdapter {
private ListView mListView;
private SwipeActionTouchListener mTouchListener;
protected SwipeActionListener mSwipeActionListener;
private boolean mFadeOut = false;
private boolean mFixedBackgrounds = false;
private boolean mDimBackgrounds = false;
private float mFarSwipeFraction = 0.5f;
private float mNormalSwipeFraction = 0.25f;
protected HashMap<SwipeDirection, Integer> mBackgroundResIds = new HashMap<>();
private SectionedBaseAdapter listAdapter;
public SwipeActionAdapter(BaseAdapter baseAdapter, SectionedBaseAdapter listAdapter) {
super(baseAdapter);
this.listAdapter = listAdapter;
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
SwipeViewGroup output = (SwipeViewGroup) convertView;
if (output == null) {
output = new SwipeViewGroup(parent.getContext());
for (Map.Entry<SwipeDirection, Integer> entry : mBackgroundResIds.entrySet()) {
output.addBackground(View.inflate(parent.getContext(), entry.getValue(), null), entry.getKey());
}
output.setSwipeTouchListener(mTouchListener);
}
output.setContentView(super.getView(position, output.getContentView(), output));
// LogUtils.e(output);
return output;
}
/**
* SwipeActionTouchListener.ActionCallbacks callback
* We just link it through to our own interface
*
* @param position the position of the item that was swiped
* @param direction the direction in which the swipe has happened
* @return boolean indicating whether the item has actions
*/
@Override
public boolean hasActions(int position, SwipeDirection direction) {
// 这样设置,标题栏就可以不用滑动了
if (listAdapter != null) {
if (listAdapter.isSectionHeader(position)) {
return false;
}
}
return mSwipeActionListener != null && mSwipeActionListener.hasActions(position, direction);
}
/**
* SwipeActionTouchListener.ActionCallbacks callback
* We just link it through to our own interface
*
* @param listView The originating {@link ListView}.
* @param position The position to perform the action on, sorted in descending order
* for convenience.
* @param direction The type of swipe that triggered the action
* @return boolean that indicates whether the list item should be dismissed or shown again.
*/
@Override
public boolean onPreAction(ListView listView, int position, SwipeDirection direction) {
return mSwipeActionListener != null && mSwipeActionListener.shouldDismiss(position, direction);
}
/**
* SwipeActionTouchListener.ActionCallbacks callback
* We just link it through to our own interface
*
* @param listView The originating {@link ListView}.
* @param position The positions to perform the action on, sorted in descending order
* for convenience.
* @param direction The type of swipe that triggered the action.
*/
@Override
public void onAction(ListView listView, int[] position, SwipeDirection[] direction) {
if (mSwipeActionListener != null) mSwipeActionListener.onSwipe(position, direction);
}
/**
* Set whether items should have a fadeOut animation
*
* @param mFadeOut true makes items fade out with a swipe (opacity -> 0)
* @return A reference to the current instance so that commands can be chained
*/
@SuppressWarnings("unused")
public SwipeActionAdapter setFadeOut(boolean mFadeOut) {
this.mFadeOut = mFadeOut;
if (mListView != null) mTouchListener.setFadeOut(mFadeOut);
return this;
}
/**
* Set whether the backgrounds should be fixed or swipe in from the side
* The default value for this property is false: backgrounds will swipe in
*
* @param fixedBackgrounds true for fixed backgrounds, false for swipe in
*/
@SuppressWarnings("unused")
public SwipeActionAdapter setFixedBackgrounds(boolean fixedBackgrounds) {
this.mFixedBackgrounds = fixedBackgrounds;
if (mListView != null) mTouchListener.setFixedBackgrounds(fixedBackgrounds);
return this;
}
/**
* Set whether the backgrounds should be dimmed when in no-trigger zone
* The default value for this property is false: backgrounds will not dim
*
* @param dimBackgrounds true for dimmed backgrounds, false for no opacity change
*/
@SuppressWarnings("unused")
public SwipeActionAdapter setDimBackgrounds(boolean dimBackgrounds) {
this.mDimBackgrounds = dimBackgrounds;
if (mListView != null) mTouchListener.setDimBackgrounds(dimBackgrounds);
return this;
}
/**
* Set the fraction of the View Width that needs to be swiped before it is counted as a far swipe
*
* @param farSwipeFraction float between 0 and 1
*/
@SuppressWarnings("unused")
public SwipeActionAdapter setFarSwipeFraction(float farSwipeFraction) {
if (farSwipeFraction < 0 || farSwipeFraction > 1) {
throw new IllegalArgumentException("Must be a float between 0 and 1");
}
this.mFarSwipeFraction = farSwipeFraction;
if (mListView != null) mTouchListener.setFarSwipeFraction(farSwipeFraction);
return this;
}
/**
* Set the fraction of the View Width that needs to be swiped before it is counted as a normal swipe
*
* @param normalSwipeFraction float between 0 and 1
*/
@SuppressWarnings("unused")
public SwipeActionAdapter setNormalSwipeFraction(float normalSwipeFraction) {
if (normalSwipeFraction < 0 || normalSwipeFraction > 1) {
throw new IllegalArgumentException("Must be a float between 0 and 1");
}
this.mNormalSwipeFraction = normalSwipeFraction;
if (mListView != null) mTouchListener.setNormalSwipeFraction(normalSwipeFraction);
return this;
}
/**
* We need the ListView to be able to modify it's OnTouchListener
*
* @param listView the ListView to which the adapter will be attached
* @return A reference to the current instance so that commands can be chained
*/
public SwipeActionAdapter setListView(ListView listView) {
this.mListView = listView;
mTouchListener = new SwipeActionTouchListener(listView, this);
this.mListView.setOnTouchListener(mTouchListener);
this.mListView.setOnScrollListener(mTouchListener.makeScrollListener());
this.mListView.setClipChildren(false);
mTouchListener.setFadeOut(mFadeOut);
mTouchListener.setDimBackgrounds(mDimBackgrounds);
mTouchListener.setFixedBackgrounds(mFixedBackgrounds);
mTouchListener.setNormalSwipeFraction(mNormalSwipeFraction);
mTouchListener.setFarSwipeFraction(mFarSwipeFraction);
return this;
}
/**
* Getter that is just here for completeness
*
* @return the current ListView
*/
@SuppressWarnings("unused")
public AbsListView getListView() {
return mListView;
}
/**
* Add a background image for a certain callback. The key for the background must be one of the
* directions from the SwipeDirections class.
*
* @param key the identifier of the callback for which this resource should be shown
* @param resId the resource Id of the background to add
* @return A reference to the current instance so that commands can be chained
*/
public SwipeActionAdapter addBackground(SwipeDirection key, int resId) {
if (SwipeDirection.getAllDirections().contains(key)) mBackgroundResIds.put(key, resId);
return this;
}
/**
* Set the listener for swipe events
*
* @param mSwipeActionListener class listening to swipe events
* @return A reference to the current instance so that commands can be chained
*/
public SwipeActionAdapter setSwipeActionListener(SwipeActionListener mSwipeActionListener) {
this.mSwipeActionListener = mSwipeActionListener;
return this;
}
@Override
public boolean isSectionHeader(int position) {
return listAdapter.isSectionHeader(position);
}
@Override
public int getSectionForPosition(int position) {
return listAdapter.getSectionForPosition(position);
}
@Override
public int getPositionInSectionForPosition(int position) {
return listAdapter.getPositionInSectionForPosition(position);
}
@Override
public View getSectionHeaderView(int section, View convertView, ViewGroup parent) {
return listAdapter.getSectionHeaderView(section, convertView, parent);
}
@Override
public int getSectionHeaderViewType(int section) {
return listAdapter.getSectionHeaderViewType(section);
}
/**
* Interface that listeners of swipe events should implement
*/
public interface SwipeActionListener {
boolean hasActions(int position, SwipeDirection direction);
boolean shouldDismiss(int position, SwipeDirection direction);
void onSwipe(int[] position, SwipeDirection[] direction);
}
}
PinnedHeaderListView.PinnedSectionedHeaderAdapter接口中的方法,完全交给了通过构造方法传递过来的listAdapter进行处理,SwipeActionAdapter只做了一层封装的作用。
注2:修改了public boolean hasActions(int position, SwipeDirection direction) 方法,因为,如果不修改的话,左右滑动时,连标题栏也可以滑动了,所以在该方法中,我们判断如果滑动的事标题栏,则禁止其滑动。
3.2 实现分组且可左右滑动的列表
public class MainActivity extends Activity implements SwipeActionAdapter.SwipeActionListener {
protected SwipeActionAdapter mAdapter;
List<HeaderItem> headers = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
private void initView() {
PinnedHeaderListView listView = (PinnedHeaderListView) findViewById(R.id.pinnedListView);
SectionedBaseAdapter sectionedAdapter = new TestSectionedAdapter(headers);
mAdapter = new SwipeActionAdapter(sectionedAdapter, sectionedAdapter);
mAdapter.setSwipeActionListener(this)
.setDimBackgrounds(true)
.setListView(listView);
listView.setAdapter(mAdapter);
mAdapter.addBackground(SwipeDirection.DIRECTION_FAR_LEFT, R.layout.row_bg_left_far)
.addBackground(SwipeDirection.DIRECTION_NORMAL_LEFT, R.layout.row_bg_left)
.addBackground(SwipeDirection.DIRECTION_FAR_RIGHT, R.layout.row_bg_right_far)
.addBackground(SwipeDirection.DIRECTION_NORMAL_RIGHT, R.layout.row_bg_right);
}
@Override
public boolean hasActions(int position, SwipeDirection direction) {
return true;
}
@Override
public boolean shouldDismiss(int position, SwipeDirection direction) {
boolean isDismiss = false;
switch (direction) {
case DIRECTION_FAR_RIGHT:
case DIRECTION_NORMAL_RIGHT:
isDismiss = true;
break;
}
return isDismiss;
}
@Override
public void onSwipe(int[] positionList, SwipeDirection[] directionList) {
for (int i = 0; i < positionList.length; i++) {
SwipeDirection direction = directionList[i];
int position = positionList[i];
String dir = "";
switch (direction) {
case DIRECTION_FAR_LEFT:
dir = "删除";
break;
case DIRECTION_NORMAL_LEFT:
dir = "编辑";
break;
case DIRECTION_FAR_RIGHT:
dir = "标为完成";
break;
case DIRECTION_NORMAL_RIGHT:
dir = "标为未完成";
break;
}
Toast.makeText(this, dir, Toast.LENGTH_SHORT).show();
}
}
private void initData() {
for (int i = 0; i < 5; i++) {
HeaderItem headerItem = new HeaderItem();
List<DataItem> datas = new ArrayList<>();
for (int j = 0; j < 5; j++) {
DataItem dataItem = new DataItem();
dataItem.name = " 列表数据 " + j;
datas.add(dataItem);
}
headerItem.name = "标题 " + i;
headerItem.dataItem = datas;
headers.add(headerItem);
}
LogUtils.e(FastJsonUtil.t2Json2(headers));
for (int i = 0; i < headers.size(); i++) {
HeaderItem headerItem = headers.get(i);
List<DataItem> dataItems = headerItem.dataItem;
for (int j = 0; j < dataItems.size(); j++) {
// LogUtils.e(headerItem.name + " " + dataItems.get(j).name);
}
}
}
}
SwipeActionAdapter如何使用,可参考《Android 使用SwipeActionAdapter开源库实现简单列表的左右滑动操作》, 然后,在initView方法中,需要添加mAdapter = new SwipeActionAdapter(sectionedAdapter, sectionedAdapter);其中sectionedAdapter是SectionedBaseAdapter的实例,前面扩展SwipeActionAdapter时提过,要把SwipeActionAdapter只是封装了一层SectionedBaseAdapter中的方法,具体的实现,由传递过来的实例对象sectionedAdapter来实现。
4. 为列表添加事件处理
滑动事件
@Override
public void onSwipe(int[] positionList, SwipeDirection[] directionList) {
for (int i = 0; i < positionList.length; i++) {
SwipeDirection direction = directionList[i];
int position = positionList[i];
String dir = "";
switch (direction) {
case DIRECTION_FAR_LEFT:
dir = "删除";
break;
case DIRECTION_NORMAL_LEFT:
dir = "编辑";
break;
case DIRECTION_FAR_RIGHT:
dir = "标为完成";
break;
case DIRECTION_NORMAL_RIGHT:
dir = "标为未完成";
break;
}
Toast.makeText(this, dir, Toast.LENGTH_SHORT).show();
if (mAdapter != null) {
try {
int section = mAdapter.getSectionForPosition(position);
int itemPos = mAdapter.getPositionInSectionForPosition(position);
if (position != -1) {
// String sectionName = headers.get(section).name;
// DataItem dataItem = headers.get(section).dataItem.get(itemPos);
headers.get(section).dataItem.remove(itemPos);
mAdapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
其中,mAdapter是上面扩展的SwipeActionAdapter的实例。通过mAdapter,可以获取滑动的标题的位置、数据项在标题中位置。 之后,就可以操作数据了。
4.2 列表的点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int rawPosition, long id) {
PinnedHeaderListView.PinnedSectionedHeaderAdapter adapter;
if (adapterView.getAdapter().getClass().equals(HeaderViewListAdapter.class)) {
HeaderViewListAdapter wrapperAdapter = (HeaderViewListAdapter) adapterView.getAdapter();
adapter = (PinnedHeaderListView.PinnedSectionedHeaderAdapter) wrapperAdapter.getWrappedAdapter();
} else {
adapter = (PinnedHeaderListView.PinnedSectionedHeaderAdapter) adapterView.getAdapter();
}
int section = adapter.getSectionForPosition(rawPosition);
int position = adapter.getPositionInSectionForPosition(rawPosition);
// LogUtils.e("section : " + section + " position : " + position);
if (position != -1) {
String sectionName = headers.get(section).name;
DataItem dataItem = headers.get(section).dataItem.get(position);
LogUtils.e(sectionName + " --> " + dataItem.name);
}
}
});
如此这般,就OK啦!欢迎指正!
如有疑问,请留言,共同探讨。