在上一篇博客《Android 使用SwipeActionAdapter开源库实现简单列表的左右滑动操作》里,已经介绍了利用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; } }Adapter继承了SectionedBaseAdapter,并且实现它里面的抽象方法,尤其是getItemView和getSectionHeaderView两个方法,前者用于创建普通列表的布局,后者用于创建悬浮标题的布局。(具体讲解可参看《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
3.1 修改SwipeActionAdapter,让其实现PinnedHeaderListView.PinnedSectionedHeaderAdapter,并实现其中的方法
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); } }
注1: 在SwipeActionAdapter中,我们把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. 为列表添加事件处理
4.1 在onSwipe(int[] positionList, SwipeDirection[] directionList)方法中处理滑动事件
@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啦!欢迎指正!
如有疑问,请留言,共同探讨。
源码地址: https://github.com/zuiwuyuan/PinnedHeader_SwipeAction_ListView