├── GroupRefreshLoadListView ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── assets │ └── .gitignore ├── libs │ └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── listview_plus_arrow.png │ ├── drawable-mdpi │ │ └── ic_launcher.png │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_test.xml │ │ ├── line_content_layout.xml │ │ └── line_group_layout.xml │ ├── menu │ │ └── main.xml │ ├── values-v11 │ │ └── styles.xml │ ├── values-v14 │ │ └── styles.xml │ ├── values-w820dp │ │ └── dimens.xml │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── jph │ └── examples │ ├── activity │ ├── MainActivity.java │ ├── PinnedSectionListActivity.java │ └── TestActivity.java │ ├── adapter │ ├── BillAdapter.java │ └── TestAdapter.java │ └── bean │ ├── Bill.java │ ├── BillList.java │ ├── Message.java │ └── MessageGroup.java ├── README.md ├── library-ListViewPlus ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── ic_launcher-web.png ├── proguard-project.txt ├── project.properties ├── res │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── listview_plus_arrow.png │ ├── drawable-mdpi │ │ └── ic_launcher.png │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ ├── drawable-xxhdpi │ │ ├── ic.png │ │ └── ic_launcher.png │ ├── layout │ │ ├── listview_plus_footer.xml │ │ └── listview_plus_header.xml │ ├── values-w820dp │ │ └── dimens.xml │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── jph │ └── view │ └── ListViewPlus.java ├── library-PinnedSectionListActivity ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── assets │ └── .gitignore ├── build.gradle ├── proguard-project.txt ├── project.properties ├── res │ └── .gitignore └── src │ └── com │ └── hb │ └── views │ └── PinnedSectionListView.java └── raw ├── 上拉加载.png ├── 下拉刷新.png └── 运行效果图.gif /GroupRefreshLoadListView/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/.gitignore: -------------------------------------------------------------------------------- 1 | /gen 2 | /bin 3 | /libs 4 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | GroupRefreshLoadListView 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/assets/.gitignore: -------------------------------------------------------------------------------- 1 | # placeholder 2 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/libs/android-support-v4.jar -------------------------------------------------------------------------------- /GroupRefreshLoadListView/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | android.library.reference.1=..\\library-PinnedSectionListActivity 16 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/drawable-hdpi/listview_plus_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/res/drawable-hdpi/listview_plus_arrow.png -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/GroupRefreshLoadListView/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/layout/activity_test.xml: -------------------------------------------------------------------------------- 1 | 5 | 12 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/layout/line_content_layout.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 23 | 24 | 32 | 33 | 41 | 42 | 49 | 50 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/layout/line_group_layout.xml: -------------------------------------------------------------------------------- 1 | 5 | 15 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | 20 | 21 | 26 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 64dp 9 | 10 | 11 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffff4444 4 | #ff99cc00 5 | #ffffbb33 6 | #ff33b5e5 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pinned Section Demo 5 | Fast scroll 6 | Add padding 7 | Show shadow 8 | Show Header & Footer 9 | Update dataset 10 | TestActivity 11 | Hello world! 12 | Settings 13 | 14 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.activity; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.os.Handler; 8 | import android.view.View; 9 | import android.widget.AdapterView; 10 | import android.widget.AdapterView.OnItemClickListener; 11 | import android.widget.Toast; 12 | 13 | import com.hb.views.PinnedSectionListView; 14 | import com.jph.examples.R; 15 | import com.jph.examples.adapter.BillAdapter; 16 | import com.jph.examples.adapter.BillAdapter.Item; 17 | import com.jph.examples.bean.Bill; 18 | import com.jph.view.ListViewPlus; 19 | import com.jph.view.ListViewPlus.ListViewPlusListener; 20 | 21 | /** 22 | * 方支付宝账单页分组+上拉加载下拉刷新效果 23 | * @author JPH 24 | * @Date 2015-3-7下午11:27:07 25 | */ 26 | public class MainActivity extends Activity implements ListViewPlusListener{ 27 | private PinnedSectionListView listView; 28 | private BillAdapter adapter; 29 | private ArrayListbills; 30 | private int page=1; 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_test); 35 | listView=(PinnedSectionListView) findViewById(R.id.list); 36 | bills=new ArrayList(); 37 | listView.setLoadEnable(true); 38 | listView.setListViewPlusListener(this); 39 | adapter=new BillAdapter(this, bills); 40 | listView.setAdapter(adapter); 41 | listView.setOnItemClickListener(new OnItemClickListener() { 42 | @Override 43 | public void onItemClick(AdapterView parent, View view, 44 | int position, long id) { 45 | Item item=(Item) adapter.getItem(position-1); 46 | if(item.getType()==Item.SECTION)return; 47 | Toast.makeText(MainActivity.this,item.getTitle() ,Toast.LENGTH_LONG).show(); 48 | } 49 | }); 50 | loadData(ListViewPlus.REFRESH); 51 | } 52 | private void loadData(final int what) { 53 | new Handler().postDelayed(new Runnable() { 54 | @Override 55 | public void run() { 56 | if (ListViewPlus.REFRESH==what) { 57 | bills.clear(); 58 | listView.stopRefresh(); 59 | page=1; 60 | }else { 61 | listView.stopLoadMore(); 62 | page++; 63 | } 64 | for (int i = (page-1)*20; i implements 46 | PinnedSectionListAdapter { 47 | 48 | private static final int[] COLORS = new int[] { R.color.green_light, 49 | R.color.orange_light, R.color.blue_light, R.color.red_light }; 50 | 51 | public SimpleAdapter(Context context, int resource, 52 | int textViewResourceId) { 53 | super(context, resource, textViewResourceId); 54 | generateDataset('A', 'Z', false); 55 | } 56 | 57 | public void generateDataset(char from, char to, boolean clear) { 58 | 59 | if (clear) 60 | clear(); 61 | 62 | final int sectionsNumber = to - from + 1; 63 | int sectionPosition = 0, listPosition = 0; 64 | for (char i = 0; i < sectionsNumber; i++) { 65 | Item section = new Item(Item.SECTION, 66 | String.valueOf((char) ('A' + i))); 67 | section.sectionPosition = sectionPosition; 68 | section.listPosition = listPosition++; 69 | add(section); 70 | 71 | final int itemsNumber = (int) Math.abs((Math.cos(2f * Math.PI 72 | / 3f * sectionsNumber / (i + 1f)) * 25f)); 73 | for (int j = 0; j < itemsNumber; j++) { 74 | Item item = new Item(Item.ITEM, 75 | section.text.toUpperCase(Locale.ENGLISH) + " - " 76 | + j); 77 | item.sectionPosition = sectionPosition; 78 | item.listPosition = listPosition++; 79 | add(item); 80 | } 81 | 82 | sectionPosition++; 83 | } 84 | } 85 | 86 | @Override 87 | public View getView(int position, View convertView, ViewGroup parent) { 88 | TextView view = (TextView) super.getView(position, convertView, 89 | parent); 90 | view.setTextColor(Color.DKGRAY); 91 | view.setTag("" + position); 92 | Item item = getItem(position); 93 | if (item.type == Item.SECTION) { 94 | view.setBackgroundColor(parent.getResources().getColor( 95 | COLORS[item.sectionPosition % COLORS.length])); 96 | } 97 | return view; 98 | } 99 | 100 | @Override 101 | public int getViewTypeCount() { 102 | return 2; 103 | } 104 | 105 | @Override 106 | public int getItemViewType(int position) { 107 | return getItem(position).type; 108 | } 109 | 110 | @Override 111 | public boolean isItemViewTypePinned(int viewType) { 112 | return viewType == Item.SECTION; 113 | } 114 | 115 | } 116 | 117 | static class Item { 118 | 119 | public static final int ITEM = 0; 120 | public static final int SECTION = 1; 121 | 122 | public final int type; 123 | public final String text; 124 | 125 | public int sectionPosition; 126 | public int listPosition; 127 | 128 | public Item(int type, String text) { 129 | this.type = type; 130 | this.text = text; 131 | } 132 | 133 | @Override 134 | public String toString() { 135 | return text; 136 | } 137 | 138 | } 139 | 140 | private boolean hasHeaderAndFooter; 141 | private boolean isFastScroll; 142 | private boolean addPadding; 143 | private boolean isShadowVisible = true; 144 | private int mDatasetUpdateCount; 145 | 146 | @Override 147 | protected void onCreate(Bundle savedInstanceState) { 148 | super.onCreate(savedInstanceState); 149 | setContentView(R.layout.activity_main); 150 | if (savedInstanceState != null) { 151 | isFastScroll = savedInstanceState.getBoolean("isFastScroll"); 152 | addPadding = savedInstanceState.getBoolean("addPadding"); 153 | isShadowVisible = savedInstanceState.getBoolean("isShadowVisible"); 154 | hasHeaderAndFooter = savedInstanceState 155 | .getBoolean("hasHeaderAndFooter"); 156 | } 157 | ((ListViewPlus) getListView()).setLoadEnable(true); 158 | initializeHeaderAndFooter(); 159 | initializeAdapter(); 160 | initializePadding(); 161 | } 162 | 163 | @Override 164 | protected void onSaveInstanceState(Bundle outState) { 165 | super.onSaveInstanceState(outState); 166 | outState.putBoolean("isFastScroll", isFastScroll); 167 | outState.putBoolean("addPadding", addPadding); 168 | outState.putBoolean("isShadowVisible", isShadowVisible); 169 | outState.putBoolean("hasHeaderAndFooter", hasHeaderAndFooter); 170 | } 171 | 172 | @Override 173 | protected void onListItemClick(ListView l, View v, int position, long id) { 174 | Item item = (Item) getListView().getAdapter().getItem(position); 175 | if (item != null) { 176 | Toast.makeText(this, "Item " + position + ": " + item.text, 177 | Toast.LENGTH_SHORT).show(); 178 | } else { 179 | Toast.makeText(this, "Item " + position, Toast.LENGTH_SHORT).show(); 180 | } 181 | } 182 | 183 | @Override 184 | public boolean onCreateOptionsMenu(Menu menu) { 185 | getMenuInflater().inflate(R.menu.main, menu); 186 | menu.getItem(0).setChecked(isFastScroll); 187 | menu.getItem(1).setChecked(addPadding); 188 | menu.getItem(2).setChecked(isShadowVisible); 189 | return true; 190 | } 191 | 192 | @Override 193 | public boolean onOptionsItemSelected(MenuItem item) { 194 | switch (item.getItemId()) { 195 | case R.id.action_fastscroll: 196 | isFastScroll = !isFastScroll; 197 | item.setChecked(isFastScroll); 198 | initializeAdapter(); 199 | break; 200 | case R.id.action_addpadding: 201 | addPadding = !addPadding; 202 | item.setChecked(addPadding); 203 | initializePadding(); 204 | break; 205 | case R.id.action_showShadow: 206 | isShadowVisible = !isShadowVisible; 207 | item.setChecked(isShadowVisible); 208 | ((PinnedSectionListView) getListView()) 209 | .setShadowVisible(isShadowVisible); 210 | break; 211 | case R.id.action_showHeaderAndFooter: 212 | hasHeaderAndFooter = !hasHeaderAndFooter; 213 | item.setChecked(hasHeaderAndFooter); 214 | initializeHeaderAndFooter(); 215 | break; 216 | case R.id.action_updateDataset: 217 | updateDataset(); 218 | break; 219 | } 220 | return true; 221 | } 222 | 223 | private void updateDataset() { 224 | mDatasetUpdateCount++; 225 | SimpleAdapter adapter = (SimpleAdapter) getListAdapter(); 226 | switch (mDatasetUpdateCount % 4) { 227 | case 0: 228 | adapter.generateDataset('A', 'B', true); 229 | break; 230 | case 1: 231 | adapter.generateDataset('C', 'M', true); 232 | break; 233 | case 2: 234 | adapter.generateDataset('P', 'Z', true); 235 | break; 236 | case 3: 237 | adapter.generateDataset('A', 'Z', true); 238 | break; 239 | } 240 | adapter.notifyDataSetChanged(); 241 | } 242 | 243 | private void initializePadding() { 244 | float density = getResources().getDisplayMetrics().density; 245 | int padding = addPadding ? (int) (16 * density) : 0; 246 | getListView().setPadding(padding, padding, padding, padding); 247 | } 248 | 249 | private void initializeHeaderAndFooter() { 250 | setListAdapter(null); 251 | if (hasHeaderAndFooter) { 252 | ListView list = getListView(); 253 | 254 | LayoutInflater inflater = LayoutInflater.from(this); 255 | TextView header1 = (TextView) inflater.inflate( 256 | android.R.layout.simple_list_item_1, list, false); 257 | header1.setText("First header"); 258 | list.addHeaderView(header1); 259 | 260 | TextView header2 = (TextView) inflater.inflate( 261 | android.R.layout.simple_list_item_1, list, false); 262 | header2.setText("Second header"); 263 | list.addHeaderView(header2); 264 | 265 | TextView footer = (TextView) inflater.inflate( 266 | android.R.layout.simple_list_item_1, list, false); 267 | footer.setText("Single footer"); 268 | list.addFooterView(footer); 269 | } 270 | initializeAdapter(); 271 | } 272 | 273 | @SuppressLint("NewApi") 274 | private void initializeAdapter() { 275 | getListView().setFastScrollEnabled(isFastScroll); 276 | setListAdapter(new SimpleAdapter(this, 277 | android.R.layout.simple_list_item_1, android.R.id.text1)); 278 | } 279 | 280 | @Override 281 | public void onClick(View v) { 282 | Toast.makeText(this, "Item: " + v.getTag(), Toast.LENGTH_SHORT).show(); 283 | } 284 | 285 | } -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/activity/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.activity; 2 | 3 | import java.util.ArrayList; 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.view.View; 8 | import android.widget.AdapterView; 9 | import android.widget.Toast; 10 | import android.widget.AdapterView.OnItemClickListener; 11 | 12 | import com.hb.views.PinnedSectionListView; 13 | import com.jph.examples.R; 14 | import com.jph.examples.adapter.TestAdapter; 15 | import com.jph.examples.adapter.TestAdapter.Item; 16 | import com.jph.examples.bean.BillList; 17 | import com.jph.examples.bean.Message; 18 | import com.jph.examples.bean.MessageGroup; 19 | import com.jph.view.ListViewPlus; 20 | import com.jph.view.ListViewPlus.ListViewPlusListener; 21 | 22 | public class TestActivity extends Activity implements ListViewPlusListener{ 23 | private PinnedSectionListView listView; 24 | private TestAdapter adapter; 25 | private BillList billList; 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_test); 30 | listView=(PinnedSectionListView) findViewById(R.id.list); 31 | listView.setLoadEnable(true); 32 | listView.setListViewPlusListener(this); 33 | adapter=new TestAdapter(this, billList); 34 | listView.setAdapter(adapter); 35 | listView.setOnItemClickListener(new OnItemClickListener() { 36 | @Override 37 | public void onItemClick(AdapterView parent, View view, 38 | int position, long id) { 39 | Item item=(Item) adapter.getItem(position-1); 40 | if(item.getType()==Item.SECTION)return; 41 | Toast.makeText(TestActivity.this,item.getTitle() ,Toast.LENGTH_LONG).show(); 42 | } 43 | }); 44 | loadData(ListViewPlus.REFRESH); 45 | } 46 | private void loadData(final int what) { 47 | new Handler().postDelayed(new Runnable() { 48 | @Override 49 | public void run() { 50 | ArrayList messageGroups = new ArrayList(); 51 | if (ListViewPlus.REFRESH==what) { 52 | listView.stopRefresh(); 53 | }else { 54 | listView.stopLoadMore(); 55 | } 56 | for (int i = 0; i <10; i++) { 57 | ArrayListmessages=new ArrayList(); 58 | for (int j = 1; j < 10; j++) { 59 | messages.add(new Message(i+"-"+j, "", "")); 60 | } 61 | messageGroups.add(new MessageGroup(""+i, messages)); 62 | } 63 | billList=new BillList(0, messageGroups); 64 | adapter.inflaterItems(billList, ListViewPlus.REFRESH==what); 65 | adapter.notifyDataSetChanged(); 66 | } 67 | }, 1000); 68 | } 69 | @Override 70 | public void onRefresh() { 71 | loadData(ListViewPlus.REFRESH); 72 | } 73 | @Override 74 | public void onLoadMore() { 75 | loadData(ListViewPlus.LOAD); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/adapter/BillAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.adapter; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.ArrayList; 5 | import java.util.Date; 6 | import java.util.Iterator; 7 | import java.util.Locale; 8 | import java.util.Map.Entry; 9 | import java.util.TreeMap; 10 | 11 | import android.app.Activity; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.BaseAdapter; 16 | import android.widget.TextView; 17 | 18 | import com.hb.views.PinnedSectionListView.PinnedSectionListAdapter; 19 | import com.jph.examples.R; 20 | import com.jph.examples.bean.Bill; 21 | 22 | /** 23 | * 账单历史记录的适配器 24 | * @author JPH 25 | * @Date 2015-3-7下午10:43:10 26 | */ 27 | public class BillAdapter extends BaseAdapter implements PinnedSectionListAdapter{ 28 | private static final int[] COLORS = new int[] { R.color.green_light, 29 | R.color.orange_light, R.color.blue_light, R.color.red_light }; 30 | private Activity activity; 31 | /**服务器返回的历史账单列表**/ 32 | private ArrayListbills; 33 | /**本地分组后的账单**/ 34 | private TreeMap>groupBills; 35 | /**Adapter的数据源**/ 36 | private ArrayListitems; 37 | private SimpleDateFormat sdf; 38 | public BillAdapter(Activity activity,ArrayList bills) { 39 | this.activity = activity; 40 | this.bills=bills; 41 | items=new ArrayList(); 42 | groupBills=new TreeMap>(); 43 | sdf=new SimpleDateFormat("yyyy-MM", Locale.getDefault()); 44 | } 45 | /** 46 | * 初始化列表项 47 | * @author JPH 48 | */ 49 | public void inflaterItems() { 50 | items.clear(); 51 | groupBills.clear(); 52 | for(Bill bill:bills){//遍历bills将集合中的所有数据以月份进行分类 53 | String groupName=sdf.format(new Date(bill.getDate())); 54 | if (groupBills.containsKey(groupName)) {//如果Map已经存在以该记录的日期为分组名的分组,则将该条记录插入到该分组中 55 | groupBills.get(groupName).add(bill); 56 | }else {//如果不存在,以该记录的日期作为分组名称创建分组,并将该记录插入到创建的分组中 57 | ArrayListtempBills=new ArrayList(); 58 | tempBills.add(bill); 59 | groupBills.put(groupName, tempBills); 60 | } 61 | } 62 | 63 | Iterator>>iterator=groupBills.descendingMap().entrySet().iterator(); 64 | while(iterator.hasNext()){//将分组后的数据添加到数据源的集合中 65 | Entry>entry=iterator.next(); 66 | items.add(new Item(Item.SECTION, entry.getKey()));//将分组添加到集合中 67 | for(Bill bill:entry.getValue()){//将组中的数据添加到集合中 68 | items.add(new Item(Item.ITEM, bill)); 69 | } 70 | } 71 | } 72 | @Override 73 | public int getCount() { 74 | return items.size(); 75 | } 76 | @Override 77 | public Object getItem(int position) { 78 | return items.get(position); 79 | } 80 | @Override 81 | public long getItemId(int position) { 82 | return position; 83 | } 84 | @Override 85 | public View getView(int position, View convertView, ViewGroup parent) { 86 | Item item = (Item) getItem(position); 87 | if (item.type == Item.SECTION) { 88 | convertView=LayoutInflater.from(activity).inflate(R.layout.line_group_layout, null); 89 | convertView.setBackgroundColor(activity.getResources().getColor(COLORS[position%4])); 90 | TextView tvGroupName=(TextView) convertView.findViewById(R.id.tvGroupName); 91 | tvGroupName.setText(item.getGroupName()); 92 | }else { 93 | convertView=LayoutInflater.from(activity).inflate(R.layout.line_content_layout, null); 94 | TextView tvTitle=(TextView) convertView.findViewById(R.id.tvTitle); 95 | TextView tvSum=(TextView) convertView.findViewById(R.id.tvSum); 96 | TextView tvDate=(TextView) convertView.findViewById(R.id.tvDateTime); 97 | TextView tvStatus=(TextView) convertView.findViewById(R.id.tvStatus); 98 | tvTitle.setText(item.getTitle()); 99 | tvSum.setText(item.getSum()); 100 | tvStatus.setText(item.getStatus()); 101 | SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); 102 | tvDate.setText(sdf.format(new Date(item.getDate()))); 103 | } 104 | return convertView; 105 | } 106 | @Override 107 | public int getItemViewType(int position) { 108 | return items.get(position).getType(); 109 | } 110 | @Override 111 | public int getViewTypeCount() { 112 | return 2; 113 | } 114 | @Override 115 | public boolean isItemViewTypePinned(int viewType) { 116 | return viewType==Item.SECTION; 117 | } 118 | @Override 119 | public void notifyDataSetChanged() { 120 | inflaterItems();//重新初始化数据 121 | super.notifyDataSetChanged(); 122 | } 123 | public class Item extends Bill{ 124 | public static final int ITEM = 0; 125 | public static final int SECTION = 1; 126 | private String groupName; 127 | public int type; 128 | public Item(int type,Bill bill) { 129 | super(bill); 130 | this.type=type; 131 | } 132 | public Item(int type,String groupName) { 133 | super(null); 134 | this.type=type; 135 | this.groupName=groupName; 136 | } 137 | public String getGroupName() { 138 | return groupName; 139 | } 140 | public void setGroupName(String groupName) { 141 | this.groupName = groupName; 142 | } 143 | public int getType() { 144 | return type; 145 | } 146 | public void setType(int type) { 147 | this.type = type; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/adapter/TestAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.adapter; 2 | 3 | import java.util.ArrayList; 4 | import android.app.Activity; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.TextView; 10 | import com.hb.views.PinnedSectionListView.PinnedSectionListAdapter; 11 | import com.jph.examples.R; 12 | import com.jph.examples.bean.BillList; 13 | import com.jph.examples.bean.Message; 14 | import com.jph.examples.bean.MessageGroup; 15 | 16 | public class TestAdapter extends BaseAdapter implements PinnedSectionListAdapter{ 17 | private static final int[] COLORS = new int[] { R.color.green_light, 18 | R.color.orange_light, R.color.blue_light, R.color.red_light }; 19 | private Activity activity; 20 | private ArrayListitems; 21 | public TestAdapter(Activity activity, BillList billList) { 22 | this.activity = activity; 23 | items=new ArrayList(); 24 | inflaterItems(billList, true); 25 | } 26 | /** 27 | * 初始化列表项 28 | * @param billList 29 | * @param isClear 是否清除原有原有数据 30 | * @return 31 | */ 32 | public void inflaterItems(BillList billList,boolean isClear) { 33 | if (isClear) items.clear(); 34 | if(billList==null)return; 35 | ArrayListmessageGroups=billList.getMessageGroups(); 36 | for(MessageGroup messageGroup:messageGroups){ 37 | ArrayListmessages=messageGroup.getMessages(); 38 | items.add(new Item(Item.SECTION, messageGroup.getGroupName())); 39 | for(Message message:messages){ 40 | items.add(new Item(Item.ITEM, message)); 41 | } 42 | } 43 | } 44 | @Override 45 | public int getCount() { 46 | return items.size(); 47 | } 48 | @Override 49 | public Object getItem(int position) { 50 | return items.get(position); 51 | } 52 | @Override 53 | public long getItemId(int position) { 54 | return position; 55 | } 56 | @Override 57 | public View getView(int position, View convertView, ViewGroup parent) { 58 | Item item = (Item) getItem(position); 59 | if (item.type == Item.SECTION) { 60 | convertView=LayoutInflater.from(activity).inflate(R.layout.line_group_layout, null); 61 | convertView.setBackgroundColor(activity.getResources().getColor(COLORS[position%4])); 62 | TextView tvGroupName=(TextView) convertView.findViewById(R.id.tvGroupName); 63 | tvGroupName.setText(item.getGroupName()); 64 | }else { 65 | convertView=LayoutInflater.from(activity).inflate(R.layout.line_content_layout, null); 66 | TextView tvTitle=(TextView) convertView.findViewById(R.id.tvTitle); 67 | TextView tvContent=(TextView) convertView.findViewById(R.id.tvDateTime); 68 | TextView tvDate=(TextView) convertView.findViewById(R.id.tvSum); 69 | tvTitle.setText(item.getTitle()); 70 | tvContent.setText(item.getContent()); 71 | tvDate.setText(item.getDate()); 72 | } 73 | return convertView; 74 | } 75 | @Override 76 | public int getItemViewType(int position) { 77 | return items.get(position).getType(); 78 | } 79 | @Override 80 | public int getViewTypeCount() { 81 | return 2; 82 | } 83 | @Override 84 | public boolean isItemViewTypePinned(int viewType) { 85 | return viewType==Item.SECTION; 86 | } 87 | public class Item extends Message{ 88 | public static final int ITEM = 0; 89 | public static final int SECTION = 1; 90 | private String groupName; 91 | public int type; 92 | public Item(int type,Message message) { 93 | super(message); 94 | this.type=type; 95 | } 96 | public Item(int type,String groupName) { 97 | super(null); 98 | this.type=type; 99 | this.groupName=groupName; 100 | } 101 | public String getGroupName() { 102 | return groupName; 103 | } 104 | public void setGroupName(String groupName) { 105 | this.groupName = groupName; 106 | } 107 | public int getType() { 108 | return type; 109 | } 110 | public void setType(int type) { 111 | this.type = type; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/bean/Bill.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.bean; 2 | 3 | 4 | /** 5 | * 账单概括实体类 6 | * @author JPH 7 | * @Date 2015-3-7下午9:49:39 8 | */ 9 | public class Bill { 10 | private int id; 11 | private String title; 12 | private String sum; 13 | private long date; 14 | private String status; 15 | public Bill(Bill bill){ 16 | if(bill==null)return; 17 | this.id=bill.id; 18 | this.title=bill.title; 19 | this.sum=bill.sum; 20 | this.date=bill.date; 21 | this.status=bill.status; 22 | } 23 | public Bill(int id, String title, String money, long date, String status) { 24 | super(); 25 | this.id = id; 26 | this.title = title; 27 | this.sum = money; 28 | this.date = date; 29 | this.status = status; 30 | } 31 | public int getId() { 32 | return id; 33 | } 34 | public void setId(int id) { 35 | this.id = id; 36 | } 37 | public String getTitle() { 38 | return title; 39 | } 40 | public void setTitle(String title) { 41 | this.title = title; 42 | } 43 | public String getSum() { 44 | return sum; 45 | } 46 | public void setSum(String sum) { 47 | this.sum = sum; 48 | } 49 | public long getDate() { 50 | return date; 51 | } 52 | public void setDate(long date) { 53 | this.date = date; 54 | } 55 | public String getStatus() { 56 | return status; 57 | } 58 | public void setStatus(String status) { 59 | this.status = status; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/bean/BillList.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.bean; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class BillList { 6 | private int status; 7 | private ArrayListmessageGroups; 8 | public BillList(int status, ArrayList messageGroups) { 9 | this.status = status; 10 | this.messageGroups = messageGroups; 11 | } 12 | public int getStatus() { 13 | return status; 14 | } 15 | public void setStatus(int status) { 16 | this.status = status; 17 | } 18 | public ArrayList getMessageGroups() { 19 | return messageGroups; 20 | } 21 | public void setMessageGroups(ArrayList messageGroups) { 22 | this.messageGroups = messageGroups; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/bean/Message.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.bean; 2 | 3 | public class Message{ 4 | private String title; 5 | private String content; 6 | private String date; 7 | public Message(Message message){ 8 | if(message!=null){ 9 | this.title=message.getTitle(); 10 | this.content=message.getContent(); 11 | this.date=message.getDate(); 12 | } 13 | } 14 | public Message(String title, String content, String date) { 15 | this.title = title; 16 | this.content = content; 17 | this.date = date; 18 | } 19 | public String getTitle() { 20 | return title; 21 | } 22 | public void setTitle(String title) { 23 | this.title = title; 24 | } 25 | public String getContent() { 26 | return content; 27 | } 28 | public void setContent(String content) { 29 | this.content = content; 30 | } 31 | public String getDate() { 32 | return date; 33 | } 34 | public void setDate(String date) { 35 | this.date = date; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GroupRefreshLoadListView/src/com/jph/examples/bean/MessageGroup.java: -------------------------------------------------------------------------------- 1 | package com.jph.examples.bean; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class MessageGroup { 6 | /**分组名称**/ 7 | private String groupName; 8 | private ArrayListmessages; 9 | public MessageGroup() { 10 | } 11 | public MessageGroup(String groupName, ArrayList messages) { 12 | this.groupName = groupName; 13 | this.messages = messages; 14 | } 15 | public String getGroupName() { 16 | return groupName; 17 | } 18 | public void setGroupName(String groupName) { 19 | this.groupName = groupName; 20 | } 21 | public ArrayList getMessages() { 22 | return messages; 23 | } 24 | public void setMessages(ArrayList messages) { 25 | this.messages = messages; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GroupListView简介 # 2 | - GroupListView支持上拉加载下拉刷新,并且可以根据需要选择是否启用下拉刷新、上拉加载 3 | - GroupListView支持列表项悬停效果 4 | 5 | ## 运行效果图 ## 6 | ![运行效果图](https://github.com/crazycodeboy/GroupListView/blob/dev/raw/%E8%BF%90%E8%A1%8C%E6%95%88%E6%9E%9C%E5%9B%BE.gif?raw=true) 7 | 8 | 9 | ## PS. ## 10 | 11 | >为了实现支付宝账单列表的悬停、上拉加载、下拉刷新的效果,我从Git上Fetch了pulltorefresh和PinnedSectionList两个开源项目,并对pulltorefresh进行修改实现上拉加载的效果,将修改后的pulltorefresh更名成了ListViewPlus,最后将PinnedSectionList的library依赖于ListViewPlus,就实现了带有悬停效果并且支持上拉加载、下拉刷新的GroupListView,现在将其开源出来供大家使用。 12 | 13 | ## 使用方法 ## 14 | 1. GroupListView是基于ListView开发的一个控件,所以大家可以使用ListView的方式来使用它。 15 | 2. ListViewPlus部分:关于ListViewPlus方面的功能大家可以参考[https://github.com/crazycodeboy/ListViewPlus](https://github.com/crazycodeboy/ListViewPlus "ListViewPlus") 16 | 3. PinnedSectionList部分: 17 | 18 | PinnedSectionList的使用需要一个实现如下方法的适配器: 19 | 20 | ```java 21 | @Override 22 | public int getItemViewType(int position) { 23 | return items.get(position).getType(); 24 | } 25 | @Override 26 | public int getViewTypeCount() { 27 | return 2; 28 | } 29 | @Override 30 | public boolean isItemViewTypePinned(int viewType) { 31 | return viewType==Item.SECTION; 32 | } 33 | ``` 34 | 35 | 4 .其它使用细节可以参照实例。 36 | 37 | 38 | -------------------------------------------------------------------------------- /library-ListViewPlus/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library-ListViewPlus/.gitignore: -------------------------------------------------------------------------------- 1 | /gen 2 | /bin 3 | -------------------------------------------------------------------------------- /library-ListViewPlus/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | library-ListViewPlus 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /library-ListViewPlus/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /library-ListViewPlus/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /library-ListViewPlus/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/ic_launcher-web.png -------------------------------------------------------------------------------- /library-ListViewPlus/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /library-ListViewPlus/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | android.library=true 16 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-hdpi/listview_plus_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-hdpi/listview_plus_arrow.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-xxhdpi/ic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-xxhdpi/ic.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/library-ListViewPlus/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /library-ListViewPlus/res/layout/listview_plus_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/layout/listview_plus_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 18 | 19 | 24 | 25 | 29 | 30 | 35 | 36 | 41 | 42 | 43 | 44 | 52 | 53 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 64dp 9 | 10 | 11 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 上拉加载下拉刷新ListView 5 | Hello world! 6 | Settings 7 | 下拉刷新 8 | 松开刷新数据 9 | 正在加载... 10 | 上次更新时间: 11 | 查看更多 12 | 松开载入更多 13 | 14 | -------------------------------------------------------------------------------- /library-ListViewPlus/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /library-ListViewPlus/src/com/jph/view/ListViewPlus.java: -------------------------------------------------------------------------------- 1 | package com.jph.view; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | import java.util.Locale; 6 | 7 | import android.content.Context; 8 | import android.util.AttributeSet; 9 | import android.view.Gravity; 10 | import android.view.LayoutInflater; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewTreeObserver.OnGlobalLayoutListener; 14 | import android.view.animation.Animation; 15 | import android.view.animation.DecelerateInterpolator; 16 | import android.view.animation.RotateAnimation; 17 | import android.widget.AbsListView; 18 | import android.widget.AbsListView.OnScrollListener; 19 | import android.widget.ImageView; 20 | import android.widget.LinearLayout; 21 | import android.widget.ListAdapter; 22 | import android.widget.ListView; 23 | import android.widget.ProgressBar; 24 | import android.widget.RelativeLayout; 25 | import android.widget.Scroller; 26 | import android.widget.TextView; 27 | 28 | import com.jph.lp.R; 29 | 30 | /** 31 | * 基于ListView的自定义控件实现上拉加载下拉刷新 32 | * @author JPH 33 | * @date 2015-3-10 下午1:01:56 34 | */ 35 | public class ListViewPlus extends ListView implements OnScrollListener { 36 | /**区分当前操作是刷新还是加载**/ 37 | public static final int REFRESH = 0; 38 | public static final int LOAD = 1; 39 | private float mLastY = -1; // save event y 40 | private Scroller mScroller; // used for scroll back 41 | private OnScrollListener mScrollListener; // user's scroll listener 42 | 43 | // the interface to trigger refresh and load more. 44 | private ListViewPlusListener mListViewListener; 45 | 46 | // -- header view 47 | private ListViewPlusHeader mHeaderView; 48 | // header view content, use it to calculate the Header's height. And hide it 49 | // when disable pull refresh. 50 | private RelativeLayout mHeaderViewContent; 51 | private TextView mHeaderTimeView; 52 | private int mHeaderViewHeight; // header view's height 53 | private boolean mEnablePullRefresh = true; 54 | private boolean mPullRefreshing = false; // is refreashing. 55 | 56 | // -- footer view 57 | private ListViewPlusFooter mFooterView; 58 | private boolean mEnablePullLoad; 59 | private boolean mPullLoading; 60 | private boolean mIsFooterReady = false; 61 | 62 | // total list items, used to detect is at the bottom of listview. 63 | private int mTotalItemCount; 64 | 65 | // for mScroller, scroll back from header or footer. 66 | private int mScrollBack; 67 | private int minItemCount=3; 68 | private final static int SCROLLBACK_HEADER = 0; 69 | private final static int SCROLLBACK_FOOTER = 1; 70 | 71 | private final static int SCROLL_DURATION = 400; // scroll back duration 72 | private final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >= 50px 73 | // at bottom, trigger 74 | // load more. 75 | private final static float OFFSET_RADIO = 1.8f; // support iOS like pull 76 | // feature. 77 | 78 | /** 79 | * @param context 80 | */ 81 | public ListViewPlus(Context context) { 82 | super(context); 83 | initWithContext(context); 84 | } 85 | 86 | public ListViewPlus(Context context, AttributeSet attrs) { 87 | super(context, attrs); 88 | initWithContext(context); 89 | } 90 | 91 | public ListViewPlus(Context context, AttributeSet attrs, int defStyle) { 92 | super(context, attrs, defStyle); 93 | initWithContext(context); 94 | } 95 | 96 | private void initWithContext(Context context) { 97 | mScroller = new Scroller(context, new DecelerateInterpolator()); 98 | // listview_plus need the scroll event, and it will dispatch the event to 99 | // user's listener (as a proxy). 100 | super.setOnScrollListener(this); 101 | 102 | // init header view 103 | mHeaderView = new ListViewPlusHeader(context); 104 | mHeaderViewContent = (RelativeLayout) mHeaderView 105 | .findViewById(R.id.listview_plus_header_content); 106 | mHeaderTimeView = (TextView) mHeaderView 107 | .findViewById(R.id.listview_plus_header_time); 108 | addHeaderView(mHeaderView); 109 | 110 | // init footer view 111 | mFooterView = new ListViewPlusFooter(context); 112 | 113 | // init header height 114 | mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener( 115 | new OnGlobalLayoutListener() { 116 | @Override 117 | public void onGlobalLayout() { 118 | mHeaderViewHeight = mHeaderViewContent.getHeight(); 119 | getViewTreeObserver() 120 | .removeGlobalOnLayoutListener(this); 121 | } 122 | }); 123 | } 124 | 125 | @Override 126 | public void setAdapter(ListAdapter adapter) { 127 | // make sure ListViewPlusFooter is the last footer view, and only add once. 128 | if (mIsFooterReady == false) { 129 | mIsFooterReady = true; 130 | addFooterView(mFooterView); 131 | } 132 | super.setAdapter(adapter); 133 | } 134 | 135 | /** 136 | * enable or disable pull down refresh feature. 137 | * 138 | * @param enable 139 | */ 140 | public void setRefreshEnable(boolean enable) { 141 | mEnablePullRefresh = enable; 142 | if (!mEnablePullRefresh) { // disable, hide the content 143 | mHeaderViewContent.setVisibility(View.INVISIBLE); 144 | } else { 145 | mHeaderViewContent.setVisibility(View.VISIBLE); 146 | } 147 | } 148 | 149 | /** 150 | * enable or disable pull up load more feature. 151 | * 152 | * @param enable 153 | */ 154 | public void setLoadEnable(boolean enable) { 155 | mEnablePullLoad = enable; 156 | if (!mEnablePullLoad) { 157 | mFooterView.hide(); 158 | mFooterView.setOnClickListener(null); 159 | //make sure "pull up" don't show a line in bottom when listview with one page 160 | setFooterDividersEnabled(false); 161 | } else { 162 | mPullLoading = false; 163 | mFooterView.show(); 164 | mFooterView.setState(ListViewPlusFooter.STATE_NORMAL); 165 | //make sure "pull up" don't show a line in bottom when listview with one page 166 | setFooterDividersEnabled(true); 167 | // both "pull up" and "click" will invoke load more. 168 | mFooterView.setOnClickListener(new OnClickListener() { 169 | @Override 170 | public void onClick(View v) { 171 | startLoadMore(); 172 | } 173 | }); 174 | } 175 | } 176 | 177 | /** 178 | * stop refresh, reset header view. 179 | */ 180 | public void stopRefresh() { 181 | if (mPullRefreshing == true) { 182 | mPullRefreshing = false; 183 | resetHeaderHeight(); 184 | setRefreshTime(getCurrentTime()); 185 | } 186 | } 187 | 188 | /** 189 | * stop load more, reset footer view. 190 | */ 191 | public void stopLoadMore() { 192 | if (mPullLoading == true) { 193 | mPullLoading = false; 194 | mFooterView.setState(ListViewPlusFooter.STATE_NORMAL); 195 | } 196 | } 197 | 198 | /** 199 | * set last refresh time 200 | * 201 | * @param time 202 | */ 203 | public void setRefreshTime(String time) { 204 | mHeaderTimeView.setText(time); 205 | } 206 | 207 | private void invokeOnScrolling() { 208 | if (mScrollListener instanceof OnXScrollListener) { 209 | OnXScrollListener l = (OnXScrollListener) mScrollListener; 210 | l.onXScrolling(this); 211 | } 212 | } 213 | 214 | private void updateHeaderHeight(float delta) { 215 | mHeaderView.setVisiableHeight((int) delta 216 | + mHeaderView.getVisiableHeight()); 217 | if (mEnablePullRefresh && !mPullRefreshing) { // 未处于刷新状态,更新箭头 218 | if (mHeaderView.getVisiableHeight() > mHeaderViewHeight) { 219 | mHeaderView.setState(ListViewPlusHeader.STATE_READY); 220 | } else { 221 | mHeaderView.setState(ListViewPlusHeader.STATE_NORMAL); 222 | } 223 | } 224 | setSelection(0); // scroll to top each time 225 | } 226 | 227 | /** 228 | * reset header view's height. 229 | */ 230 | private void resetHeaderHeight() { 231 | int height = mHeaderView.getVisiableHeight(); 232 | if (height == 0) // not visible. 233 | return; 234 | // refreshing and header isn't shown fully. do nothing. 235 | if (mPullRefreshing && height <= mHeaderViewHeight) { 236 | return; 237 | } 238 | int finalHeight = 0; // default: scroll back to dismiss header. 239 | // is refreshing, just scroll back to show all the header. 240 | if (mPullRefreshing && height > mHeaderViewHeight) { 241 | finalHeight = mHeaderViewHeight; 242 | } 243 | mScrollBack = SCROLLBACK_HEADER; 244 | mScroller.startScroll(0, height, 0, finalHeight - height, 245 | SCROLL_DURATION); 246 | // trigger computeScroll 247 | invalidate(); 248 | } 249 | 250 | private void updateFooterHeight(float delta) { 251 | int height = mFooterView.getBottomMargin() + (int) delta; 252 | if (mEnablePullLoad && !mPullLoading) { 253 | if (height > PULL_LOAD_MORE_DELTA) { // height enough to invoke load 254 | // more. 255 | mFooterView.setState(ListViewPlusFooter.STATE_READY); 256 | } else { 257 | mFooterView.setState(ListViewPlusFooter.STATE_NORMAL); 258 | } 259 | } 260 | mFooterView.setBottomMargin(height); 261 | 262 | // setSelection(mTotalItemCount - 1); // scroll to bottom 263 | } 264 | 265 | private void resetFooterHeight() { 266 | int bottomMargin = mFooterView.getBottomMargin(); 267 | if (bottomMargin > 0) { 268 | mScrollBack = SCROLLBACK_FOOTER; 269 | mScroller.startScroll(0, bottomMargin, 0, -bottomMargin, 270 | SCROLL_DURATION); 271 | invalidate(); 272 | } 273 | } 274 | 275 | private void startLoadMore() { 276 | mPullLoading = true; 277 | mFooterView.setState(ListViewPlusFooter.STATE_LOADING); 278 | if (mListViewListener != null) { 279 | mListViewListener.onLoadMore(); 280 | } 281 | } 282 | 283 | @Override 284 | public boolean onTouchEvent(MotionEvent ev) { 285 | if (mLastY == -1) { 286 | mLastY = ev.getRawY(); 287 | } 288 | 289 | switch (ev.getAction()) { 290 | case MotionEvent.ACTION_DOWN: 291 | mLastY = ev.getRawY(); 292 | break; 293 | case MotionEvent.ACTION_MOVE: 294 | final float deltaY = ev.getRawY() - mLastY; 295 | mLastY = ev.getRawY(); 296 | if (getFirstVisiblePosition() == 0 297 | && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) { 298 | // the first item is showing, header has shown or pull down. 299 | updateHeaderHeight(deltaY / OFFSET_RADIO); 300 | invokeOnScrolling(); 301 | } else if (getLastVisiblePosition() == mTotalItemCount - 1 302 | && (mFooterView.getBottomMargin() > 0 || deltaY < 0)) { 303 | // last item, already pulled up or want to pull up. 304 | updateFooterHeight(-deltaY / OFFSET_RADIO); 305 | } 306 | break; 307 | default: 308 | mLastY = -1; // reset 309 | if (getFirstVisiblePosition() == 0) { 310 | // invoke refresh 311 | if (mEnablePullRefresh 312 | && mHeaderView.getVisiableHeight() > mHeaderViewHeight) { 313 | mPullRefreshing = true; 314 | mHeaderView.setState(ListViewPlusHeader.STATE_REFRESHING); 315 | if (mListViewListener != null) { 316 | mListViewListener.onRefresh(); 317 | } 318 | } 319 | resetHeaderHeight(); 320 | } else if (getLastVisiblePosition() == mTotalItemCount - 1) { 321 | // invoke load more. 322 | if (mEnablePullLoad 323 | && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA 324 | && !mPullLoading) { 325 | startLoadMore(); 326 | } 327 | resetFooterHeight(); 328 | } 329 | break; 330 | } 331 | return super.onTouchEvent(ev); 332 | } 333 | 334 | @Override 335 | public void computeScroll() { 336 | if (mScroller.computeScrollOffset()) { 337 | if (mScrollBack == SCROLLBACK_HEADER) { 338 | mHeaderView.setVisiableHeight(mScroller.getCurrY()); 339 | } else { 340 | mFooterView.setBottomMargin(mScroller.getCurrY()); 341 | } 342 | postInvalidate(); 343 | invokeOnScrolling(); 344 | } 345 | super.computeScroll(); 346 | } 347 | 348 | @Override 349 | public void setOnScrollListener(OnScrollListener l) { 350 | mScrollListener = l; 351 | } 352 | @Override 353 | public void onScrollStateChanged(AbsListView view, int scrollState) { 354 | if (mScrollListener != null) { 355 | mScrollListener.onScrollStateChanged(view, scrollState); 356 | } 357 | } 358 | @Override 359 | public void onScroll(AbsListView view, int firstVisibleItem, 360 | int visibleItemCount, int totalItemCount) { 361 | // send to user's listener 362 | if(mFooterView!=null){ 363 | if (totalItemCount 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | /gen 3 | /bin 4 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | library-PinnedSectionListActivity 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/assets/.gitignore: -------------------------------------------------------------------------------- 1 | # placeholder 2 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:0.11.1' 7 | } 8 | } 9 | 10 | apply plugin: 'android-library' 11 | 12 | android { 13 | sourceSets { 14 | main { 15 | manifest.srcFile 'AndroidManifest.xml' 16 | java.srcDirs = ['src'] 17 | } 18 | } 19 | compileSdkVersion 17 20 | buildToolsVersion '19.1' 21 | } 22 | 23 | android.libraryVariants 24 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | android.library=true 16 | android.library.reference.1=../library-ListViewPlus 17 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/res/.gitignore: -------------------------------------------------------------------------------- 1 | # placeholder 2 | -------------------------------------------------------------------------------- /library-PinnedSectionListActivity/src/com/hb/views/PinnedSectionListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Sergej Shafarenka, halfbit.de 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file kt in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hb.views; 18 | 19 | import android.content.Context; 20 | import android.database.DataSetObserver; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.PointF; 24 | import android.graphics.Rect; 25 | import android.graphics.drawable.GradientDrawable; 26 | import android.graphics.drawable.GradientDrawable.Orientation; 27 | import android.os.Parcelable; 28 | import android.util.AttributeSet; 29 | import android.view.MotionEvent; 30 | import android.view.SoundEffectConstants; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | import android.view.accessibility.AccessibilityEvent; 34 | import android.widget.AbsListView; 35 | import android.widget.HeaderViewListAdapter; 36 | import android.widget.ListAdapter; 37 | import android.widget.ListView; 38 | import android.widget.SectionIndexer; 39 | 40 | import com.hb.views.pinnedsection.BuildConfig; 41 | import com.jph.view.ListViewPlus; 42 | 43 | /** 44 | * ListView, which is capable to pin section views at its top while the rest is still scrolled. 45 | */ 46 | public class PinnedSectionListView extends ListViewPlus { 47 | 48 | //-- inner classes 49 | 50 | /** List adapter to be implemented for being used with PinnedSectionListView adapter. */ 51 | public static interface PinnedSectionListAdapter extends ListAdapter { 52 | /** This method shall return 'true' if views of given type has to be pinned. */ 53 | boolean isItemViewTypePinned(int viewType); 54 | } 55 | 56 | /** Wrapper class for pinned section view and its position in the list. */ 57 | static class PinnedSection { 58 | public View view; 59 | public int position; 60 | public long id; 61 | } 62 | 63 | //-- class fields 64 | 65 | // fields used for handling touch events 66 | private final Rect mTouchRect = new Rect(); 67 | private final PointF mTouchPoint = new PointF(); 68 | private int mTouchSlop; 69 | private View mTouchTarget; 70 | private MotionEvent mDownEvent; 71 | 72 | // fields used for drawing shadow under a pinned section 73 | private GradientDrawable mShadowDrawable; 74 | private int mSectionsDistanceY; 75 | private int mShadowHeight; 76 | 77 | /** Delegating listener, can be null. */ 78 | OnScrollListener mDelegateOnScrollListener; 79 | 80 | /** Shadow for being recycled, can be null. */ 81 | PinnedSection mRecycleSection; 82 | 83 | /** shadow instance with a pinned view, can be null. */ 84 | PinnedSection mPinnedSection; 85 | 86 | /** Pinned view Y-translation. We use it to stick pinned view to the next section. */ 87 | int mTranslateY; 88 | 89 | /** Scroll listener which does the magic */ 90 | private final OnScrollListener mOnScrollListener = new OnScrollListener() { 91 | 92 | @Override public void onScrollStateChanged(AbsListView view, int scrollState) { 93 | if (mDelegateOnScrollListener != null) { // delegate 94 | mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); 95 | } 96 | } 97 | 98 | @Override 99 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 100 | 101 | if (mDelegateOnScrollListener != null) { // delegate 102 | mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); 103 | } 104 | 105 | // get expected adapter or fail fast 106 | ListAdapter adapter = getAdapter(); 107 | if (adapter == null || visibleItemCount == 0) return; // nothing to do 108 | 109 | final boolean isFirstVisibleItemSection = 110 | isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem)); 111 | 112 | if (isFirstVisibleItemSection) { 113 | View sectionView = getChildAt(0); 114 | if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow 115 | destroyPinnedShadow(); 116 | } else { // section doesn't stick to the top, make sure we have a pinned shadow 117 | ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount); 118 | } 119 | 120 | } else { // section is not at the first visible position 121 | int sectionPosition = findCurrentSectionPosition(firstVisibleItem); 122 | if (sectionPosition > -1) { // we have section position 123 | ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount); 124 | } else { // there is no section for the first visible item, destroy shadow 125 | destroyPinnedShadow(); 126 | } 127 | } 128 | }; 129 | 130 | }; 131 | 132 | /** Default change observer. */ 133 | private final DataSetObserver mDataSetObserver = new DataSetObserver() { 134 | @Override public void onChanged() { 135 | recreatePinnedShadow(); 136 | }; 137 | @Override public void onInvalidated() { 138 | recreatePinnedShadow(); 139 | } 140 | }; 141 | 142 | //-- constructors 143 | 144 | public PinnedSectionListView(Context context, AttributeSet attrs) { 145 | super(context, attrs); 146 | initView(); 147 | } 148 | 149 | public PinnedSectionListView(Context context, AttributeSet attrs, int defStyle) { 150 | super(context, attrs, defStyle); 151 | initView(); 152 | } 153 | 154 | private void initView() { 155 | setOnScrollListener(mOnScrollListener); 156 | mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 157 | initShadow(true); 158 | } 159 | 160 | //-- public API methods 161 | 162 | public void setShadowVisible(boolean visible) { 163 | initShadow(visible); 164 | if (mPinnedSection != null) { 165 | View v = mPinnedSection.view; 166 | invalidate(v.getLeft(), v.getTop(), v.getRight(), v.getBottom() + mShadowHeight); 167 | } 168 | } 169 | 170 | //-- pinned section drawing methods 171 | 172 | public void initShadow(boolean visible) { 173 | if (visible) { 174 | if (mShadowDrawable == null) { 175 | mShadowDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, 176 | new int[] { Color.parseColor("#ffa0a0a0"), Color.parseColor("#50a0a0a0"), Color.parseColor("#00a0a0a0")}); 177 | mShadowHeight = (int) (8 * getResources().getDisplayMetrics().density); 178 | } 179 | } else { 180 | if (mShadowDrawable != null) { 181 | mShadowDrawable = null; 182 | mShadowHeight = 0; 183 | } 184 | } 185 | } 186 | 187 | /** Create shadow wrapper with a pinned view for a view at given position */ 188 | void createPinnedShadow(int position) { 189 | 190 | // try to recycle shadow 191 | PinnedSection pinnedShadow = mRecycleSection; 192 | mRecycleSection = null; 193 | 194 | // create new shadow, if needed 195 | if (pinnedShadow == null) pinnedShadow = new PinnedSection(); 196 | // request new view using recycled view, if such 197 | View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this); 198 | 199 | // read layout parameters 200 | LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); 201 | if (layoutParams == null) { 202 | layoutParams = (LayoutParams) generateDefaultLayoutParams(); 203 | pinnedView.setLayoutParams(layoutParams); 204 | } 205 | 206 | int heightMode = MeasureSpec.getMode(layoutParams.height); 207 | int heightSize = MeasureSpec.getSize(layoutParams.height); 208 | 209 | if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY; 210 | 211 | int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); 212 | if (heightSize > maxHeight) heightSize = maxHeight; 213 | 214 | // measure & layout 215 | int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY); 216 | int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 217 | pinnedView.measure(ws, hs); 218 | pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); 219 | mTranslateY = 0; 220 | 221 | // initialize pinned shadow 222 | pinnedShadow.view = pinnedView; 223 | pinnedShadow.position = position; 224 | pinnedShadow.id = getAdapter().getItemId(position); 225 | 226 | // store pinned shadow 227 | mPinnedSection = pinnedShadow; 228 | } 229 | 230 | /** Destroy shadow wrapper for currently pinned view */ 231 | void destroyPinnedShadow() { 232 | if (mPinnedSection != null) { 233 | // keep shadow for being recycled later 234 | mRecycleSection = mPinnedSection; 235 | mPinnedSection = null; 236 | } 237 | } 238 | 239 | /** Makes sure we have an actual pinned shadow for given position. */ 240 | void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) { 241 | if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item 242 | destroyPinnedShadow(); 243 | return; 244 | } 245 | 246 | if (mPinnedSection != null 247 | && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required 248 | destroyPinnedShadow(); 249 | } 250 | 251 | if (mPinnedSection == null) { // create shadow, if empty 252 | createPinnedShadow(sectionPosition); 253 | } 254 | 255 | // align shadow according to next section position, if needed 256 | int nextPosition = sectionPosition + 1; 257 | if (nextPosition < getCount()) { 258 | int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition, 259 | visibleItemCount - (nextPosition - firstVisibleItem)); 260 | if (nextSectionPosition > -1) { 261 | View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem); 262 | final int bottom = mPinnedSection.view.getBottom() + getPaddingTop(); 263 | mSectionsDistanceY = nextSectionView.getTop() - bottom; 264 | if (mSectionsDistanceY < 0) { 265 | // next section overlaps pinned shadow, move it up 266 | mTranslateY = mSectionsDistanceY; 267 | } else { 268 | // next section does not overlap with pinned, stick to top 269 | mTranslateY = 0; 270 | } 271 | } else { 272 | // no other sections are visible, stick to top 273 | mTranslateY = 0; 274 | mSectionsDistanceY = Integer.MAX_VALUE; 275 | } 276 | } 277 | 278 | } 279 | 280 | int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) { 281 | ListAdapter adapter = getAdapter(); 282 | 283 | int adapterDataCount = adapter.getCount(); 284 | if (getLastVisiblePosition() >= adapterDataCount) return -1; // dataset has changed, no candidate 285 | 286 | if (firstVisibleItem+visibleItemCount >= adapterDataCount){//added to prevent index Outofbound (in case) 287 | visibleItemCount = adapterDataCount-firstVisibleItem; 288 | } 289 | 290 | for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) { 291 | int position = firstVisibleItem + childIndex; 292 | int viewType = adapter.getItemViewType(position); 293 | if (isItemViewTypePinned(adapter, viewType)) return position; 294 | } 295 | return -1; 296 | } 297 | 298 | int findCurrentSectionPosition(int fromPosition) { 299 | ListAdapter adapter = getAdapter(); 300 | 301 | if (fromPosition >= adapter.getCount()) return -1; // dataset has changed, no candidate 302 | 303 | if (adapter instanceof SectionIndexer) { 304 | // try fast way by asking section indexer 305 | SectionIndexer indexer = (SectionIndexer) adapter; 306 | int sectionPosition = indexer.getSectionForPosition(fromPosition); 307 | int itemPosition = indexer.getPositionForSection(sectionPosition); 308 | int typeView = adapter.getItemViewType(itemPosition); 309 | if (isItemViewTypePinned(adapter, typeView)) { 310 | return itemPosition; 311 | } // else, no luck 312 | } 313 | 314 | // try slow way by looking through to the next section item above 315 | for (int position=fromPosition; position>=0; position--) { 316 | int viewType = adapter.getItemViewType(position); 317 | if (isItemViewTypePinned(adapter, viewType)) return position; 318 | } 319 | return -1; // no candidate found 320 | } 321 | 322 | void recreatePinnedShadow() { 323 | destroyPinnedShadow(); 324 | ListAdapter adapter = getAdapter(); 325 | if (adapter != null && adapter.getCount() > 0) { 326 | int firstVisiblePosition = getFirstVisiblePosition(); 327 | int sectionPosition = findCurrentSectionPosition(firstVisiblePosition); 328 | if (sectionPosition == -1) return; // no views to pin, exit 329 | ensureShadowForPosition(sectionPosition, 330 | firstVisiblePosition, getLastVisiblePosition() - firstVisiblePosition); 331 | } 332 | } 333 | 334 | @Override 335 | public void setOnScrollListener(OnScrollListener listener) { 336 | if (listener == mOnScrollListener) { 337 | super.setOnScrollListener(listener); 338 | } else { 339 | mDelegateOnScrollListener = listener; 340 | } 341 | } 342 | 343 | @Override 344 | public void onRestoreInstanceState(Parcelable state) { 345 | super.onRestoreInstanceState(state); 346 | post(new Runnable() { 347 | @Override public void run() { // restore pinned view after configuration change 348 | recreatePinnedShadow(); 349 | } 350 | }); 351 | } 352 | 353 | @Override 354 | public void setAdapter(ListAdapter adapter) { 355 | 356 | // assert adapter in debug mode 357 | if (BuildConfig.DEBUG && adapter != null) { 358 | if (!(adapter instanceof PinnedSectionListAdapter)) 359 | throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?"); 360 | if (adapter.getViewTypeCount() < 2) 361 | throw new IllegalArgumentException("Does your adapter handle at least two types" + 362 | " of views in getViewTypeCount() method: items and sections?"); 363 | } 364 | 365 | // unregister observer at old adapter and register on new one 366 | ListAdapter oldAdapter = getAdapter(); 367 | if (oldAdapter != null) oldAdapter.unregisterDataSetObserver(mDataSetObserver); 368 | if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver); 369 | 370 | // destroy pinned shadow, if new adapter is not same as old one 371 | if (oldAdapter != adapter) destroyPinnedShadow(); 372 | 373 | super.setAdapter(adapter); 374 | } 375 | 376 | @Override 377 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 378 | super.onLayout(changed, l, t, r, b); 379 | if (mPinnedSection != null) { 380 | int parentWidth = r - l - getPaddingLeft() - getPaddingRight(); 381 | int shadowWidth = mPinnedSection.view.getWidth(); 382 | if (parentWidth != shadowWidth) { 383 | recreatePinnedShadow(); 384 | } 385 | } 386 | } 387 | 388 | @Override 389 | protected void dispatchDraw(Canvas canvas) { 390 | super.dispatchDraw(canvas); 391 | 392 | if (mPinnedSection != null) { 393 | 394 | // prepare variables 395 | int pLeft = getListPaddingLeft(); 396 | int pTop = getListPaddingTop(); 397 | View view = mPinnedSection.view; 398 | 399 | // draw child 400 | canvas.save(); 401 | 402 | int clipHeight = view.getHeight() + 403 | (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY)); 404 | canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight); 405 | 406 | canvas.translate(pLeft, pTop + mTranslateY); 407 | drawChild(canvas, mPinnedSection.view, getDrawingTime()); 408 | 409 | if (mShadowDrawable != null && mSectionsDistanceY > 0) { 410 | mShadowDrawable.setBounds(mPinnedSection.view.getLeft(), 411 | mPinnedSection.view.getBottom(), 412 | mPinnedSection.view.getRight(), 413 | mPinnedSection.view.getBottom() + mShadowHeight); 414 | mShadowDrawable.draw(canvas); 415 | } 416 | 417 | canvas.restore(); 418 | } 419 | } 420 | 421 | //-- touch handling methods 422 | 423 | @Override 424 | public boolean dispatchTouchEvent(MotionEvent ev) { 425 | 426 | final float x = ev.getX(); 427 | final float y = ev.getY(); 428 | final int action = ev.getAction(); 429 | 430 | if (action == MotionEvent.ACTION_DOWN 431 | && mTouchTarget == null 432 | && mPinnedSection != null 433 | && isPinnedViewTouched(mPinnedSection.view, x, y)) { // create touch target 434 | 435 | // user touched pinned view 436 | mTouchTarget = mPinnedSection.view; 437 | mTouchPoint.x = x; 438 | mTouchPoint.y = y; 439 | 440 | // copy down event for eventually be used later 441 | mDownEvent = MotionEvent.obtain(ev); 442 | } 443 | 444 | if (mTouchTarget != null) { 445 | if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view 446 | mTouchTarget.dispatchTouchEvent(ev); 447 | } 448 | 449 | if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view 450 | super.dispatchTouchEvent(ev); 451 | performPinnedItemClick(); 452 | clearTouchTarget(); 453 | 454 | } else if (action == MotionEvent.ACTION_CANCEL) { // cancel 455 | clearTouchTarget(); 456 | 457 | } else if (action == MotionEvent.ACTION_MOVE) { 458 | if (Math.abs(y - mTouchPoint.y) > mTouchSlop) { 459 | 460 | // cancel sequence on touch target 461 | MotionEvent event = MotionEvent.obtain(ev); 462 | event.setAction(MotionEvent.ACTION_CANCEL); 463 | mTouchTarget.dispatchTouchEvent(event); 464 | event.recycle(); 465 | 466 | // provide correct sequence to super class for further handling 467 | super.dispatchTouchEvent(mDownEvent); 468 | super.dispatchTouchEvent(ev); 469 | clearTouchTarget(); 470 | 471 | } 472 | } 473 | 474 | return true; 475 | } 476 | 477 | // call super if this was not our pinned view 478 | return super.dispatchTouchEvent(ev); 479 | } 480 | 481 | private boolean isPinnedViewTouched(View view, float x, float y) { 482 | view.getHitRect(mTouchRect); 483 | 484 | // by taping top or bottom padding, the list performs on click on a border item. 485 | // we don't add top padding here to keep behavior consistent. 486 | mTouchRect.top += mTranslateY; 487 | 488 | mTouchRect.bottom += mTranslateY + getPaddingTop(); 489 | mTouchRect.left += getPaddingLeft(); 490 | mTouchRect.right -= getPaddingRight(); 491 | return mTouchRect.contains((int)x, (int)y); 492 | } 493 | 494 | private void clearTouchTarget() { 495 | mTouchTarget = null; 496 | if (mDownEvent != null) { 497 | mDownEvent.recycle(); 498 | mDownEvent = null; 499 | } 500 | } 501 | 502 | private boolean performPinnedItemClick() { 503 | if (mPinnedSection == null) return false; 504 | 505 | OnItemClickListener listener = getOnItemClickListener(); 506 | if (listener != null && getAdapter().isEnabled(mPinnedSection.position)) { 507 | View view = mPinnedSection.view; 508 | playSoundEffect(SoundEffectConstants.CLICK); 509 | if (view != null) { 510 | view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 511 | } 512 | listener.onItemClick(this, view, mPinnedSection.position, mPinnedSection.id); 513 | return true; 514 | } 515 | return false; 516 | } 517 | 518 | public static boolean isItemViewTypePinned(ListAdapter adapter, int viewType) { 519 | if (adapter instanceof HeaderViewListAdapter) { 520 | adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); 521 | } 522 | return ((PinnedSectionListAdapter) adapter).isItemViewTypePinned(viewType); 523 | } 524 | 525 | } 526 | -------------------------------------------------------------------------------- /raw/上拉加载.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/raw/上拉加载.png -------------------------------------------------------------------------------- /raw/下拉刷新.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/raw/下拉刷新.png -------------------------------------------------------------------------------- /raw/运行效果图.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazycodeboy/GroupListView/8c4f696a7000b02fded831544ffb66442ef4e501/raw/运行效果图.gif --------------------------------------------------------------------------------