@@ -128,6 +128,12 @@ |
||
| 128 | 128 |
android:screenOrientation="portrait"/> |
| 129 | 129 |
|
| 130 | 130 |
<activity |
| 131 |
+ android:name=".upload.UploadActivity" |
|
| 132 |
+ android:configChanges="keyboardHidden|orientation|screenSize" |
|
| 133 |
+ android:label="@string/app_name" |
|
| 134 |
+ android:screenOrientation="portrait"/> |
|
| 135 |
+ |
|
| 136 |
+ <activity |
|
| 131 | 137 |
android:name=".wxapi.WXEntryActivity" |
| 132 | 138 |
android:exported="true" |
| 133 | 139 |
android:launchMode="singleTop" |
@@ -22,6 +22,7 @@ import ai.pai.lensman.base.BaseActivity; |
||
| 22 | 22 |
import ai.pai.lensman.bean.SessionBean; |
| 23 | 23 |
import ai.pai.lensman.briefs.BriefsActivity; |
| 24 | 24 |
import ai.pai.lensman.session.SessionActivity; |
| 25 |
+import ai.pai.lensman.upload.UploadActivity; |
|
| 25 | 26 |
import ai.pai.lensman.utils.UmengEvent; |
| 26 | 27 |
import butterknife.BindView; |
| 27 | 28 |
import butterknife.ButterKnife; |
@@ -82,6 +83,11 @@ public class MainActivity extends BaseActivity implements MainContract.View {
|
||
| 82 | 83 |
startActivity(new Intent(this, BriefsActivity.class)); |
| 83 | 84 |
} |
| 84 | 85 |
|
| 86 |
+ @OnClick(R.id.iv_upload_manage) |
|
| 87 |
+ void jumpToUploadManage(){
|
|
| 88 |
+ startActivity(new Intent(this, UploadActivity.class)); |
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 85 | 91 |
@OnClick(R.id.iv_add_session) |
| 86 | 92 |
void jumpToNewSession(){
|
| 87 | 93 |
LogHelper.d(TAG,"jumpToNewSession"); |
@@ -81,7 +81,7 @@ public class SessionRecyclerAdapter extends RecyclerView.Adapter<SessionRecycler |
||
| 81 | 81 |
lp.width = width; |
| 82 | 82 |
lp.height = height; |
| 83 | 83 |
holder.photo.setLayoutParams(lp); |
| 84 |
- holder.sesseionSeq.setText(String.valueOf(item.sessionSeq)); |
|
| 84 |
+ holder.sessionSeq.setText(String.valueOf(item.sessionSeq)); |
|
| 85 | 85 |
ArrayList<PhotoBean> photoList = item.sessionPhotos; |
| 86 | 86 |
|
| 87 | 87 |
if(photoList!=null && photoList.size()>0){
|
@@ -134,7 +134,7 @@ public class SessionRecyclerAdapter extends RecyclerView.Adapter<SessionRecycler |
||
| 134 | 134 |
|
| 135 | 135 |
@BindView(R.id.iv_session_item) ImageView photo; |
| 136 | 136 |
|
| 137 |
- @BindView(R.id.tv_session_seq) TextView sesseionSeq; |
|
| 137 |
+ @BindView(R.id.tv_session_seq) TextView sessionSeq; |
|
| 138 | 138 |
|
| 139 | 139 |
@BindView(R.id.tv_session_upload_status) TextView uploadStatus; |
| 140 | 140 |
|
@@ -0,0 +1,30 @@ |
||
| 1 |
+package ai.pai.lensman.upload; |
|
| 2 |
+ |
|
| 3 |
+import android.os.Bundle; |
|
| 4 |
+ |
|
| 5 |
+import ai.pai.lensman.R; |
|
| 6 |
+import ai.pai.lensman.base.BaseActivity; |
|
| 7 |
+import butterknife.ButterKnife; |
|
| 8 |
+import butterknife.OnClick; |
|
| 9 |
+ |
|
| 10 |
+/** |
|
| 11 |
+ * Created by chengzhenyu on 2017/4/4. |
|
| 12 |
+ */ |
|
| 13 |
+ |
|
| 14 |
+public class UploadActivity extends BaseActivity{
|
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+ @Override |
|
| 18 |
+ protected void onCreate(Bundle savedInstanceState) {
|
|
| 19 |
+ super.onCreate(savedInstanceState); |
|
| 20 |
+ setContentView(R.layout.activity_upload); |
|
| 21 |
+ unbinder = ButterKnife.bind(this); |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ @OnClick(R.id.title_bar_back_layout) |
|
| 25 |
+ void back(){
|
|
| 26 |
+ finish(); |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+} |
@@ -25,6 +25,9 @@ public class BoxUrlContainer {
|
||
| 25 | 25 |
|
| 26 | 26 |
public static String BOX_INFO_URL = BASE_URL+"box_info"; |
| 27 | 27 |
|
| 28 |
+ public static String SET_TIME_URL = BASE_URL+"set_time"; |
|
| 29 |
+ |
|
| 30 |
+ public static String BOX_TIME_URL = BASE_URL+"box_time"; |
|
| 28 | 31 |
|
| 29 | 32 |
public static void resetIPHost(String ip) {
|
| 30 | 33 |
BOX_IP = ip; |
@@ -44,6 +47,10 @@ public class BoxUrlContainer {
|
||
| 44 | 47 |
FETCH_ORIGIN_URL = BASE_URL +"fetch_origin"; |
| 45 | 48 |
|
| 46 | 49 |
BOX_INFO_URL = BASE_URL+"box_info"; |
| 50 |
+ |
|
| 51 |
+ SET_TIME_URL = BASE_URL+"set_time"; |
|
| 52 |
+ |
|
| 53 |
+ BOX_TIME_URL = BASE_URL+"box_time"; |
|
| 47 | 54 |
} |
| 48 | 55 |
|
| 49 | 56 |
|
@@ -68,4 +68,7 @@ public class UrlContainer {
|
||
| 68 | 68 |
public static final String PLATFORM_PRICE_RULES_PAGE_URL = "http://pai.ai/page/price"; |
| 69 | 69 |
|
| 70 | 70 |
public static final String PATCH_CONFIG_URL = HOST_URL+"op/patch"; |
| 71 |
+ |
|
| 72 |
+ public static final String SERVER_TIME_URL=HOST_URL+"s/server_time"; |
|
| 73 |
+ |
|
| 71 | 74 |
} |
@@ -36,6 +36,7 @@ |
||
| 36 | 36 |
android:textSize="@dimen/action_bar_title_medium_text_size" /> |
| 37 | 37 |
|
| 38 | 38 |
<ImageView |
| 39 |
+ android:id="@+id/iv_upload_manage" |
|
| 39 | 40 |
android:layout_width="32dp" |
| 40 | 41 |
android:layout_height="32dp" |
| 41 | 42 |
android:padding="4dp" |
@@ -0,0 +1,68 @@ |
||
| 1 |
+<?xml version="1.0" encoding="utf-8"?> |
|
| 2 |
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
| 3 |
+ android:layout_width="match_parent" |
|
| 4 |
+ android:layout_height="match_parent" |
|
| 5 |
+ android:background="@color/background_white"> |
|
| 6 |
+ |
|
| 7 |
+ <LinearLayout |
|
| 8 |
+ android:id="@+id/title_bar_with_back_btn" |
|
| 9 |
+ android:layout_width="match_parent" |
|
| 10 |
+ android:layout_height="@dimen/action_bar_height" |
|
| 11 |
+ android:background="@color/colorPrimary" |
|
| 12 |
+ android:orientation="horizontal"> |
|
| 13 |
+ |
|
| 14 |
+ <LinearLayout |
|
| 15 |
+ android:id="@+id/title_bar_back_layout" |
|
| 16 |
+ android:layout_width="70dp" |
|
| 17 |
+ android:layout_height="match_parent" |
|
| 18 |
+ android:gravity="center_vertical" |
|
| 19 |
+ android:orientation="horizontal" |
|
| 20 |
+ android:paddingLeft="12dp"> |
|
| 21 |
+ |
|
| 22 |
+ <ImageView |
|
| 23 |
+ android:layout_width="32dp" |
|
| 24 |
+ android:layout_height="32dp" |
|
| 25 |
+ android:src="@drawable/back_selector" /> |
|
| 26 |
+ |
|
| 27 |
+ </LinearLayout> |
|
| 28 |
+ |
|
| 29 |
+ <TextView |
|
| 30 |
+ android:id="@+id/title_bar_middle_txt" |
|
| 31 |
+ android:layout_width="0dp" |
|
| 32 |
+ android:layout_height="match_parent" |
|
| 33 |
+ android:layout_weight="1" |
|
| 34 |
+ android:gravity="center" |
|
| 35 |
+ android:paddingLeft="10dp" |
|
| 36 |
+ android:paddingRight="10dp" |
|
| 37 |
+ android:text="@string/upload_settings" |
|
| 38 |
+ android:textColor="@color/text_white" |
|
| 39 |
+ android:textSize="@dimen/action_bar_title_medium_text_size" /> |
|
| 40 |
+ |
|
| 41 |
+ <LinearLayout |
|
| 42 |
+ android:id="@+id/title_bar_option_layout" |
|
| 43 |
+ android:layout_width="70dp" |
|
| 44 |
+ android:layout_height="match_parent" |
|
| 45 |
+ android:gravity="center_vertical|right" |
|
| 46 |
+ android:visibility="invisible" |
|
| 47 |
+ android:orientation="horizontal" |
|
| 48 |
+ android:paddingRight="9dp"> |
|
| 49 |
+ |
|
| 50 |
+ |
|
| 51 |
+ <ImageView |
|
| 52 |
+ android:id="@+id/iv_options" |
|
| 53 |
+ android:layout_width="32dp" |
|
| 54 |
+ android:layout_height="32dp" |
|
| 55 |
+ android:layout_marginLeft="6dp" |
|
| 56 |
+ android:src="@drawable/option" /> |
|
| 57 |
+ |
|
| 58 |
+ </LinearLayout> |
|
| 59 |
+ |
|
| 60 |
+ </LinearLayout> |
|
| 61 |
+ |
|
| 62 |
+ <android.support.v7.widget.RecyclerView |
|
| 63 |
+ android:id="@+id/recycler_view_sessions" |
|
| 64 |
+ android:layout_width="match_parent" |
|
| 65 |
+ android:layout_height="match_parent" |
|
| 66 |
+ android:layout_below="@id/title_bar_with_back_btn"/> |
|
| 67 |
+ |
|
| 68 |
+</RelativeLayout> |
@@ -171,4 +171,6 @@ |
||
| 171 | 171 |
<string name="upload_slow">长</string> |
| 172 | 172 |
<string name="upload_normal">中</string> |
| 173 | 173 |
<string name="upload_fast">短</string> |
| 174 |
+ |
|
| 175 |
+ <string name="upload_settings">上传管理</string> |
|
| 174 | 176 |
</resources> |
@@ -0,0 +1,225 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.database.DataSetObserver; |
|
| 5 |
+import android.graphics.drawable.Drawable; |
|
| 6 |
+import android.view.View; |
|
| 7 |
+import android.view.View.OnClickListener; |
|
| 8 |
+import android.view.ViewGroup; |
|
| 9 |
+import android.widget.BaseAdapter; |
|
| 10 |
+import android.widget.Checkable; |
|
| 11 |
+import android.widget.ListAdapter; |
|
| 12 |
+ |
|
| 13 |
+import java.util.LinkedList; |
|
| 14 |
+import java.util.List; |
|
| 15 |
+ |
|
| 16 |
+/** |
|
| 17 |
+ * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and
|
|
| 18 |
+ * automatically handles wrapping the result of |
|
| 19 |
+ * {@link StickyListHeadersAdapter#getView(int, View, ViewGroup)}
|
|
| 20 |
+ * and |
|
| 21 |
+ * {@link StickyListHeadersAdapter#getHeaderView(int, View, ViewGroup)}
|
|
| 22 |
+ * appropriately. |
|
| 23 |
+ * |
|
| 24 |
+ * @author Jake Wharton (jakewharton@gmail.com) |
|
| 25 |
+ */ |
|
| 26 |
+class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter {
|
|
| 27 |
+ |
|
| 28 |
+ interface OnHeaderClickListener {
|
|
| 29 |
+ void onHeaderClick(View header, int itemPosition, long headerId); |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ StickyListHeadersAdapter mDelegate; |
|
| 33 |
+ private final List<View> mHeaderCache = new LinkedList<View>(); |
|
| 34 |
+ private final Context mContext; |
|
| 35 |
+ private Drawable mDivider; |
|
| 36 |
+ private int mDividerHeight; |
|
| 37 |
+ private OnHeaderClickListener mOnHeaderClickListener; |
|
| 38 |
+ private DataSetObserver mDataSetObserver = new DataSetObserver() {
|
|
| 39 |
+ |
|
| 40 |
+ @Override |
|
| 41 |
+ public void onInvalidated() {
|
|
| 42 |
+ mHeaderCache.clear(); |
|
| 43 |
+ AdapterWrapper.super.notifyDataSetInvalidated(); |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ @Override |
|
| 47 |
+ public void onChanged() {
|
|
| 48 |
+ AdapterWrapper.super.notifyDataSetChanged(); |
|
| 49 |
+ } |
|
| 50 |
+ }; |
|
| 51 |
+ |
|
| 52 |
+ AdapterWrapper(Context context, |
|
| 53 |
+ StickyListHeadersAdapter delegate) {
|
|
| 54 |
+ this.mContext = context; |
|
| 55 |
+ this.mDelegate = delegate; |
|
| 56 |
+ delegate.registerDataSetObserver(mDataSetObserver); |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ void setDivider(Drawable divider, int dividerHeight) {
|
|
| 60 |
+ this.mDivider = divider; |
|
| 61 |
+ this.mDividerHeight = dividerHeight; |
|
| 62 |
+ notifyDataSetChanged(); |
|
| 63 |
+ } |
|
| 64 |
+ |
|
| 65 |
+ @Override |
|
| 66 |
+ public boolean areAllItemsEnabled() {
|
|
| 67 |
+ return mDelegate.areAllItemsEnabled(); |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ @Override |
|
| 71 |
+ public boolean isEnabled(int position) {
|
|
| 72 |
+ return mDelegate.isEnabled(position); |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ @Override |
|
| 76 |
+ public int getCount() {
|
|
| 77 |
+ return mDelegate.getCount(); |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ @Override |
|
| 81 |
+ public Object getItem(int position) {
|
|
| 82 |
+ return mDelegate.getItem(position); |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ @Override |
|
| 86 |
+ public long getItemId(int position) {
|
|
| 87 |
+ return mDelegate.getItemId(position); |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ @Override |
|
| 91 |
+ public boolean hasStableIds() {
|
|
| 92 |
+ return mDelegate.hasStableIds(); |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ @Override |
|
| 96 |
+ public int getItemViewType(int position) {
|
|
| 97 |
+ return mDelegate.getItemViewType(position); |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ @Override |
|
| 101 |
+ public int getViewTypeCount() {
|
|
| 102 |
+ return mDelegate.getViewTypeCount(); |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ @Override |
|
| 106 |
+ public boolean isEmpty() {
|
|
| 107 |
+ return mDelegate.isEmpty(); |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ /** |
|
| 111 |
+ * Will recycle header from {@link WrapperView} if it exists
|
|
| 112 |
+ */ |
|
| 113 |
+ private void recycleHeaderIfExists(WrapperView wv) {
|
|
| 114 |
+ View header = wv.mHeader; |
|
| 115 |
+ if (header != null) {
|
|
| 116 |
+ // reset the headers visibility when adding it to the cache |
|
| 117 |
+ header.setVisibility(View.VISIBLE); |
|
| 118 |
+ mHeaderCache.add(header); |
|
| 119 |
+ } |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ /** |
|
| 123 |
+ * Get a header view. This optionally pulls a header from the supplied |
|
| 124 |
+ * {@link WrapperView} and will also recycle the divider if it exists.
|
|
| 125 |
+ */ |
|
| 126 |
+ private View configureHeader(WrapperView wv, final int position) {
|
|
| 127 |
+ View header = wv.mHeader == null ? popHeader() : wv.mHeader; |
|
| 128 |
+ header = mDelegate.getHeaderView(position, header, wv); |
|
| 129 |
+ if (header == null) {
|
|
| 130 |
+ throw new NullPointerException("Header view must not be null.");
|
|
| 131 |
+ } |
|
| 132 |
+ //if the header isn't clickable, the listselector will be drawn on top of the header |
|
| 133 |
+ header.setClickable(true); |
|
| 134 |
+ header.setOnClickListener(new OnClickListener() {
|
|
| 135 |
+ |
|
| 136 |
+ @Override |
|
| 137 |
+ public void onClick(View v) {
|
|
| 138 |
+ if(mOnHeaderClickListener != null){
|
|
| 139 |
+ long headerId = mDelegate.getHeaderId(position); |
|
| 140 |
+ mOnHeaderClickListener.onHeaderClick(v, position, headerId); |
|
| 141 |
+ } |
|
| 142 |
+ } |
|
| 143 |
+ }); |
|
| 144 |
+ return header; |
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ private View popHeader() {
|
|
| 148 |
+ if(mHeaderCache.size() > 0) {
|
|
| 149 |
+ return mHeaderCache.remove(0); |
|
| 150 |
+ } |
|
| 151 |
+ return null; |
|
| 152 |
+ } |
|
| 153 |
+ |
|
| 154 |
+ /** Returns {@code true} if the previous position has the same header ID. */
|
|
| 155 |
+ private boolean previousPositionHasSameHeader(int position) {
|
|
| 156 |
+ return position != 0 |
|
| 157 |
+ && mDelegate.getHeaderId(position) == mDelegate |
|
| 158 |
+ .getHeaderId(position - 1); |
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ @Override |
|
| 162 |
+ public WrapperView getView(int position, View convertView, ViewGroup parent) {
|
|
| 163 |
+ WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView; |
|
| 164 |
+ View item = mDelegate.getView(position, wv.mItem, parent); |
|
| 165 |
+ View header = null; |
|
| 166 |
+ if (previousPositionHasSameHeader(position)) {
|
|
| 167 |
+ recycleHeaderIfExists(wv); |
|
| 168 |
+ } else {
|
|
| 169 |
+ header = configureHeader(wv, position); |
|
| 170 |
+ } |
|
| 171 |
+ if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) {
|
|
| 172 |
+ // Need to create Checkable subclass of WrapperView for ListView to work correctly |
|
| 173 |
+ wv = new CheckableWrapperView(mContext); |
|
| 174 |
+ } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) {
|
|
| 175 |
+ wv = new WrapperView(mContext); |
|
| 176 |
+ } |
|
| 177 |
+ wv.update(item, header, mDivider, mDividerHeight); |
|
| 178 |
+ return wv; |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener){
|
|
| 182 |
+ this.mOnHeaderClickListener = onHeaderClickListener; |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ @Override |
|
| 186 |
+ public boolean equals(Object o) {
|
|
| 187 |
+ return mDelegate.equals(o); |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ @Override |
|
| 191 |
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
|
| 192 |
+ return ((BaseAdapter) mDelegate).getDropDownView(position, convertView, parent); |
|
| 193 |
+ } |
|
| 194 |
+ |
|
| 195 |
+ @Override |
|
| 196 |
+ public int hashCode() {
|
|
| 197 |
+ return mDelegate.hashCode(); |
|
| 198 |
+ } |
|
| 199 |
+ |
|
| 200 |
+ @Override |
|
| 201 |
+ public void notifyDataSetChanged() {
|
|
| 202 |
+ ((BaseAdapter) mDelegate).notifyDataSetChanged(); |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ @Override |
|
| 206 |
+ public void notifyDataSetInvalidated() {
|
|
| 207 |
+ ((BaseAdapter) mDelegate).notifyDataSetInvalidated(); |
|
| 208 |
+ } |
|
| 209 |
+ |
|
| 210 |
+ @Override |
|
| 211 |
+ public String toString() {
|
|
| 212 |
+ return mDelegate.toString(); |
|
| 213 |
+ } |
|
| 214 |
+ |
|
| 215 |
+ @Override |
|
| 216 |
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
|
|
| 217 |
+ return mDelegate.getHeaderView(position, convertView, parent); |
|
| 218 |
+ } |
|
| 219 |
+ |
|
| 220 |
+ @Override |
|
| 221 |
+ public long getHeaderId(int position) {
|
|
| 222 |
+ return mDelegate.getHeaderId(position); |
|
| 223 |
+ } |
|
| 224 |
+ |
|
| 225 |
+} |
@@ -0,0 +1,31 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.widget.Checkable; |
|
| 5 |
+ |
|
| 6 |
+/** |
|
| 7 |
+ * A WrapperView that implements the checkable interface |
|
| 8 |
+ * |
|
| 9 |
+ * @author Emil Sjölander |
|
| 10 |
+ */ |
|
| 11 |
+class CheckableWrapperView extends WrapperView implements Checkable {
|
|
| 12 |
+ |
|
| 13 |
+ public CheckableWrapperView(final Context context) {
|
|
| 14 |
+ super(context); |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ @Override |
|
| 18 |
+ public boolean isChecked() {
|
|
| 19 |
+ return ((Checkable) mItem).isChecked(); |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ @Override |
|
| 23 |
+ public void setChecked(final boolean checked) {
|
|
| 24 |
+ ((Checkable) mItem).setChecked(checked); |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ @Override |
|
| 28 |
+ public void toggle() {
|
|
| 29 |
+ setChecked(!isChecked()); |
|
| 30 |
+ } |
|
| 31 |
+} |
@@ -0,0 +1,147 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import java.util.ArrayList; |
|
| 4 |
+import java.util.LinkedHashMap; |
|
| 5 |
+import java.util.List; |
|
| 6 |
+import java.util.Map; |
|
| 7 |
+import java.util.Set; |
|
| 8 |
+ |
|
| 9 |
+/** |
|
| 10 |
+ * a hash map can maintain an one-to-many relationship which the value only belongs to one “one” part |
|
| 11 |
+ * and the map also support getKey by value quickly |
|
| 12 |
+ * |
|
| 13 |
+ * @author lsjwzh |
|
| 14 |
+ */ |
|
| 15 |
+class DistinctMultiHashMap<TKey,TItemValue> {
|
|
| 16 |
+ private IDMapper<TKey, TItemValue> mIDMapper; |
|
| 17 |
+ |
|
| 18 |
+ interface IDMapper<TKey,TItemValue>{
|
|
| 19 |
+ public Object keyToKeyId(TKey key); |
|
| 20 |
+ public TKey keyIdToKey(Object keyId); |
|
| 21 |
+ public Object valueToValueId(TItemValue value); |
|
| 22 |
+ public TItemValue valueIdToValue(Object valueId); |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ LinkedHashMap<Object,List<TItemValue>> mKeyToValuesMap = new LinkedHashMap<Object, List<TItemValue>>(); |
|
| 26 |
+ LinkedHashMap<Object,TKey> mValueToKeyIndexer = new LinkedHashMap<Object, TKey>(); |
|
| 27 |
+ |
|
| 28 |
+ DistinctMultiHashMap(){
|
|
| 29 |
+ this(new IDMapper<TKey, TItemValue>() {
|
|
| 30 |
+ @Override |
|
| 31 |
+ public Object keyToKeyId(TKey key) {
|
|
| 32 |
+ return key; |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ @Override |
|
| 36 |
+ public TKey keyIdToKey(Object keyId) {
|
|
| 37 |
+ return (TKey) keyId; |
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ @Override |
|
| 41 |
+ public Object valueToValueId(TItemValue value) {
|
|
| 42 |
+ return value; |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ @Override |
|
| 46 |
+ public TItemValue valueIdToValue(Object valueId) {
|
|
| 47 |
+ return (TItemValue) valueId; |
|
| 48 |
+ } |
|
| 49 |
+ }); |
|
| 50 |
+ } |
|
| 51 |
+ DistinctMultiHashMap(IDMapper<TKey, TItemValue> idMapper){
|
|
| 52 |
+ mIDMapper = idMapper; |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ public List<TItemValue> get(TKey key){
|
|
| 56 |
+ //todo immutable |
|
| 57 |
+ return mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)); |
|
| 58 |
+ } |
|
| 59 |
+ public TKey getKey(TItemValue value){
|
|
| 60 |
+ return mValueToKeyIndexer.get(mIDMapper.valueToValueId(value)); |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ public void add(TKey key,TItemValue value){
|
|
| 64 |
+ Object keyId = mIDMapper.keyToKeyId(key); |
|
| 65 |
+ if(mKeyToValuesMap.get(keyId)==null){
|
|
| 66 |
+ mKeyToValuesMap.put(keyId,new ArrayList<TItemValue>()); |
|
| 67 |
+ } |
|
| 68 |
+ //remove old relationship |
|
| 69 |
+ TKey keyForValue = getKey(value); |
|
| 70 |
+ if(keyForValue !=null){
|
|
| 71 |
+ mKeyToValuesMap.get(mIDMapper.keyToKeyId(keyForValue)).remove(value); |
|
| 72 |
+ } |
|
| 73 |
+ mValueToKeyIndexer.put(mIDMapper.valueToValueId(value), key); |
|
| 74 |
+ if(!containsValue(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)),value)) {
|
|
| 75 |
+ mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)).add(value); |
|
| 76 |
+ } |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ public void removeKey(TKey key){
|
|
| 80 |
+ if(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))!=null){
|
|
| 81 |
+ for (TItemValue value : mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))){
|
|
| 82 |
+ mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value)); |
|
| 83 |
+ } |
|
| 84 |
+ mKeyToValuesMap.remove(mIDMapper.keyToKeyId(key)); |
|
| 85 |
+ } |
|
| 86 |
+ } |
|
| 87 |
+ public void removeValue(TItemValue value){
|
|
| 88 |
+ if(getKey(value)!=null){
|
|
| 89 |
+ List<TItemValue> itemValues = mKeyToValuesMap.get(mIDMapper.keyToKeyId(getKey(value))); |
|
| 90 |
+ if(itemValues!=null){
|
|
| 91 |
+ itemValues.remove(value); |
|
| 92 |
+ } |
|
| 93 |
+ } |
|
| 94 |
+ mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value)); |
|
| 95 |
+ } |
|
| 96 |
+ |
|
| 97 |
+ public void clear(){
|
|
| 98 |
+ mValueToKeyIndexer.clear(); |
|
| 99 |
+ mKeyToValuesMap.clear(); |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ public void clearValues(){
|
|
| 103 |
+ for (Map.Entry<Object,List<TItemValue>> entry:entrySet()){
|
|
| 104 |
+ if(entry.getValue()!=null){
|
|
| 105 |
+ entry.getValue().clear(); |
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ mValueToKeyIndexer.clear(); |
|
| 109 |
+ } |
|
| 110 |
+ |
|
| 111 |
+ public Set<Map.Entry<Object,List<TItemValue>>> entrySet(){
|
|
| 112 |
+ return mKeyToValuesMap.entrySet(); |
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ public Set<Map.Entry<Object,TKey>> reverseEntrySet(){
|
|
| 116 |
+ return mValueToKeyIndexer.entrySet(); |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ public int size(){
|
|
| 120 |
+ return mKeyToValuesMap.size(); |
|
| 121 |
+ } |
|
| 122 |
+ public int valuesSize(){
|
|
| 123 |
+ return mValueToKeyIndexer.size(); |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ protected boolean containsValue(List<TItemValue> list,TItemValue value){
|
|
| 127 |
+ for (TItemValue itemValue :list){
|
|
| 128 |
+ if(mIDMapper.valueToValueId(itemValue).equals(mIDMapper.valueToValueId(value))){
|
|
| 129 |
+ return true; |
|
| 130 |
+ } |
|
| 131 |
+ } |
|
| 132 |
+ return false; |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ /** |
|
| 136 |
+ * @param position |
|
| 137 |
+ * @return |
|
| 138 |
+ */ |
|
| 139 |
+ public TItemValue getValueByPosition(int position){
|
|
| 140 |
+ Object[] vauleIdArray = mValueToKeyIndexer.keySet().toArray(); |
|
| 141 |
+ if(position>vauleIdArray.length){
|
|
| 142 |
+ throw new IndexOutOfBoundsException(); |
|
| 143 |
+ } |
|
| 144 |
+ Object valueId = vauleIdArray[position]; |
|
| 145 |
+ return mIDMapper.valueIdToValue(valueId); |
|
| 146 |
+ } |
|
| 147 |
+} |
@@ -0,0 +1,39 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import java.util.HashMap; |
|
| 4 |
+ |
|
| 5 |
+/** |
|
| 6 |
+ * simple two way hashmap |
|
| 7 |
+ * @author lsjwzh |
|
| 8 |
+ */ |
|
| 9 |
+class DualHashMap<TKey, TValue> {
|
|
| 10 |
+ HashMap<TKey, TValue> mKeyToValue = new HashMap<TKey, TValue>(); |
|
| 11 |
+ HashMap<TValue, TKey> mValueToKey = new HashMap<TValue, TKey>(); |
|
| 12 |
+ |
|
| 13 |
+ public void put(TKey t1, TValue t2){
|
|
| 14 |
+ remove(t1); |
|
| 15 |
+ removeByValue(t2); |
|
| 16 |
+ mKeyToValue.put(t1, t2); |
|
| 17 |
+ mValueToKey.put(t2, t1); |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ public TKey getKey(TValue value){
|
|
| 21 |
+ return mValueToKey.get(value); |
|
| 22 |
+ } |
|
| 23 |
+ public TValue get(TKey key){
|
|
| 24 |
+ return mKeyToValue.get(key); |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ public void remove(TKey key){
|
|
| 28 |
+ if(get(key)!=null){
|
|
| 29 |
+ mValueToKey.remove(get(key)); |
|
| 30 |
+ } |
|
| 31 |
+ mKeyToValue.remove(key); |
|
| 32 |
+ } |
|
| 33 |
+ public void removeByValue(TValue value){
|
|
| 34 |
+ if(getKey(value)!=null){
|
|
| 35 |
+ mKeyToValue.remove(getKey(value)); |
|
| 36 |
+ } |
|
| 37 |
+ mValueToKey.remove(value); |
|
| 38 |
+ } |
|
| 39 |
+} |
@@ -0,0 +1,131 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.database.DataSetObserver; |
|
| 4 |
+import android.view.View; |
|
| 5 |
+import android.view.ViewGroup; |
|
| 6 |
+import android.widget.BaseAdapter; |
|
| 7 |
+ |
|
| 8 |
+import java.util.ArrayList; |
|
| 9 |
+import java.util.List; |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+/** |
|
| 13 |
+ * @author lsjwzh |
|
| 14 |
+ */ |
|
| 15 |
+ class ExpandableStickyListHeadersAdapter extends BaseAdapter implements StickyListHeadersAdapter {
|
|
| 16 |
+ |
|
| 17 |
+ private final StickyListHeadersAdapter mInnerAdapter; |
|
| 18 |
+ DualHashMap<View,Long> mViewToItemIdMap = new DualHashMap<View, Long>(); |
|
| 19 |
+ DistinctMultiHashMap<Integer,View> mHeaderIdToViewMap = new DistinctMultiHashMap<Integer, View>(); |
|
| 20 |
+ List<Long> mCollapseHeaderIds = new ArrayList<Long>(); |
|
| 21 |
+ |
|
| 22 |
+ ExpandableStickyListHeadersAdapter(StickyListHeadersAdapter innerAdapter){
|
|
| 23 |
+ this.mInnerAdapter = innerAdapter; |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ @Override |
|
| 27 |
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
|
|
| 28 |
+ return mInnerAdapter.getHeaderView(position,convertView,parent); |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ @Override |
|
| 32 |
+ public long getHeaderId(int position) {
|
|
| 33 |
+ return mInnerAdapter.getHeaderId(position); |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ @Override |
|
| 37 |
+ public boolean areAllItemsEnabled() {
|
|
| 38 |
+ return mInnerAdapter.areAllItemsEnabled(); |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ @Override |
|
| 42 |
+ public boolean isEnabled(int i) {
|
|
| 43 |
+ return mInnerAdapter.isEnabled(i); |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ @Override |
|
| 47 |
+ public void registerDataSetObserver(DataSetObserver dataSetObserver) {
|
|
| 48 |
+ mInnerAdapter.registerDataSetObserver(dataSetObserver); |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ @Override |
|
| 52 |
+ public void unregisterDataSetObserver(DataSetObserver dataSetObserver) {
|
|
| 53 |
+ mInnerAdapter.unregisterDataSetObserver(dataSetObserver); |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ @Override |
|
| 57 |
+ public int getCount() {
|
|
| 58 |
+ return mInnerAdapter.getCount(); |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ @Override |
|
| 62 |
+ public Object getItem(int i) {
|
|
| 63 |
+ return mInnerAdapter.getItem(i); |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ @Override |
|
| 67 |
+ public long getItemId(int i) {
|
|
| 68 |
+ return mInnerAdapter.getItemId(i); |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ @Override |
|
| 72 |
+ public boolean hasStableIds() {
|
|
| 73 |
+ return mInnerAdapter.hasStableIds(); |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ @Override |
|
| 77 |
+ public View getView(int i, View view, ViewGroup viewGroup) {
|
|
| 78 |
+ View convertView = mInnerAdapter.getView(i,view,viewGroup); |
|
| 79 |
+ mViewToItemIdMap.put(convertView, getItemId(i)); |
|
| 80 |
+ mHeaderIdToViewMap.add((int) getHeaderId(i), convertView); |
|
| 81 |
+ if(mCollapseHeaderIds.contains(getHeaderId(i))){
|
|
| 82 |
+ convertView.setVisibility(View.GONE); |
|
| 83 |
+ }else {
|
|
| 84 |
+ convertView.setVisibility(View.VISIBLE); |
|
| 85 |
+ } |
|
| 86 |
+ return convertView; |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ @Override |
|
| 90 |
+ public int getItemViewType(int i) {
|
|
| 91 |
+ return mInnerAdapter.getItemViewType(i); |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ @Override |
|
| 95 |
+ public int getViewTypeCount() {
|
|
| 96 |
+ return mInnerAdapter.getViewTypeCount(); |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ @Override |
|
| 100 |
+ public boolean isEmpty() {
|
|
| 101 |
+ return mInnerAdapter.isEmpty(); |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ public List<View> getItemViewsByHeaderId(long headerId){
|
|
| 105 |
+ return mHeaderIdToViewMap.get((int) headerId); |
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 108 |
+ public boolean isHeaderCollapsed(long headerId){
|
|
| 109 |
+ return mCollapseHeaderIds.contains(headerId); |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ public void expand(long headerId) {
|
|
| 113 |
+ if(isHeaderCollapsed(headerId)){
|
|
| 114 |
+ mCollapseHeaderIds.remove((Object) headerId); |
|
| 115 |
+ } |
|
| 116 |
+ } |
|
| 117 |
+ |
|
| 118 |
+ public void collapse(long headerId) {
|
|
| 119 |
+ if(!isHeaderCollapsed(headerId)){
|
|
| 120 |
+ mCollapseHeaderIds.add(headerId); |
|
| 121 |
+ } |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ public View findViewByItemId(long itemId){
|
|
| 125 |
+ return mViewToItemIdMap.getKey(itemId); |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ public long findItemIdByView(View view){
|
|
| 129 |
+ return mViewToItemIdMap.get(view); |
|
| 130 |
+ } |
|
| 131 |
+} |
@@ -0,0 +1,126 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.util.AttributeSet; |
|
| 5 |
+import android.view.View; |
|
| 6 |
+ |
|
| 7 |
+import java.util.List; |
|
| 8 |
+ |
|
| 9 |
+/** |
|
| 10 |
+ * add expand/collapse functions like ExpandableListView |
|
| 11 |
+ * @author lsjwzh |
|
| 12 |
+ */ |
|
| 13 |
+public class ExpandableStickyListHeadersListView extends StickyListHeadersListView {
|
|
| 14 |
+ public interface IAnimationExecutor{
|
|
| 15 |
+ public void executeAnim(View target, int animType); |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ public final static int ANIMATION_COLLAPSE = 1; |
|
| 19 |
+ public final static int ANIMATION_EXPAND = 0; |
|
| 20 |
+ |
|
| 21 |
+ ExpandableStickyListHeadersAdapter mExpandableStickyListHeadersAdapter; |
|
| 22 |
+ |
|
| 23 |
+ |
|
| 24 |
+ |
|
| 25 |
+ IAnimationExecutor mDefaultAnimExecutor = new IAnimationExecutor() {
|
|
| 26 |
+ @Override |
|
| 27 |
+ public void executeAnim(View target, int animType) {
|
|
| 28 |
+ if(animType==ANIMATION_EXPAND){
|
|
| 29 |
+ target.setVisibility(VISIBLE); |
|
| 30 |
+ }else if(animType==ANIMATION_COLLAPSE){
|
|
| 31 |
+ target.setVisibility(GONE); |
|
| 32 |
+ } |
|
| 33 |
+ } |
|
| 34 |
+ }; |
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+ public ExpandableStickyListHeadersListView(Context context) {
|
|
| 38 |
+ super(context); |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs) {
|
|
| 42 |
+ super(context, attrs); |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) {
|
|
| 46 |
+ super(context, attrs, defStyle); |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ @Override |
|
| 50 |
+ public ExpandableStickyListHeadersAdapter getAdapter() {
|
|
| 51 |
+ return mExpandableStickyListHeadersAdapter; |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ @Override |
|
| 55 |
+ public void setAdapter(StickyListHeadersAdapter adapter) {
|
|
| 56 |
+ mExpandableStickyListHeadersAdapter = new ExpandableStickyListHeadersAdapter(adapter); |
|
| 57 |
+ super.setAdapter(mExpandableStickyListHeadersAdapter); |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ public View findViewByItemId(long itemId){
|
|
| 61 |
+ return mExpandableStickyListHeadersAdapter.findViewByItemId(itemId); |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ public long findItemIdByView(View view){
|
|
| 65 |
+ return mExpandableStickyListHeadersAdapter.findItemIdByView(view); |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ public void expand(long headerId) {
|
|
| 69 |
+ if(!mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId)){
|
|
| 70 |
+ return; |
|
| 71 |
+ } |
|
| 72 |
+ mExpandableStickyListHeadersAdapter.expand(headerId); |
|
| 73 |
+ //find and expand views in group |
|
| 74 |
+ List<View> itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByHeaderId(headerId); |
|
| 75 |
+ if(itemViews==null){
|
|
| 76 |
+ return; |
|
| 77 |
+ } |
|
| 78 |
+ for (View view : itemViews) {
|
|
| 79 |
+ animateView(view, ANIMATION_EXPAND); |
|
| 80 |
+ } |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ public void collapse(long headerId) {
|
|
| 84 |
+ if(mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId)){
|
|
| 85 |
+ return; |
|
| 86 |
+ } |
|
| 87 |
+ mExpandableStickyListHeadersAdapter.collapse(headerId); |
|
| 88 |
+ //find and hide views with the same header |
|
| 89 |
+ List<View> itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByHeaderId(headerId); |
|
| 90 |
+ if(itemViews==null){
|
|
| 91 |
+ return; |
|
| 92 |
+ } |
|
| 93 |
+ for (View view : itemViews) {
|
|
| 94 |
+ animateView(view, ANIMATION_COLLAPSE); |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ public boolean isHeaderCollapsed(long headerId){
|
|
| 99 |
+ return mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId); |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ public void setAnimExecutor(IAnimationExecutor animExecutor) {
|
|
| 103 |
+ this.mDefaultAnimExecutor = animExecutor; |
|
| 104 |
+ } |
|
| 105 |
+ |
|
| 106 |
+ /** |
|
| 107 |
+ * Performs either COLLAPSE or EXPAND animation on the target view |
|
| 108 |
+ * |
|
| 109 |
+ * @param target the view to animate |
|
| 110 |
+ * @param type the animation type, either ExpandCollapseAnimation.COLLAPSE |
|
| 111 |
+ * or ExpandCollapseAnimation.EXPAND |
|
| 112 |
+ */ |
|
| 113 |
+ private void animateView(final View target, final int type) {
|
|
| 114 |
+ if(ANIMATION_EXPAND==type&&target.getVisibility()==VISIBLE){
|
|
| 115 |
+ return; |
|
| 116 |
+ } |
|
| 117 |
+ if(ANIMATION_COLLAPSE==type&&target.getVisibility()!=VISIBLE){
|
|
| 118 |
+ return; |
|
| 119 |
+ } |
|
| 120 |
+ if(mDefaultAnimExecutor !=null){
|
|
| 121 |
+ mDefaultAnimExecutor.executeAnim(target,type); |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+} |
@@ -0,0 +1,32 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.widget.SectionIndexer; |
|
| 5 |
+ |
|
| 6 |
+class SectionIndexerAdapterWrapper extends |
|
| 7 |
+ AdapterWrapper implements SectionIndexer {
|
|
| 8 |
+ |
|
| 9 |
+ SectionIndexer mSectionIndexerDelegate; |
|
| 10 |
+ |
|
| 11 |
+ SectionIndexerAdapterWrapper(Context context, |
|
| 12 |
+ StickyListHeadersAdapter delegate) {
|
|
| 13 |
+ super(context, delegate); |
|
| 14 |
+ mSectionIndexerDelegate = (SectionIndexer) delegate; |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ @Override |
|
| 18 |
+ public int getPositionForSection(int section) {
|
|
| 19 |
+ return mSectionIndexerDelegate.getPositionForSection(section); |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ @Override |
|
| 23 |
+ public int getSectionForPosition(int position) {
|
|
| 24 |
+ return mSectionIndexerDelegate.getSectionForPosition(position); |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ @Override |
|
| 28 |
+ public Object[] getSections() {
|
|
| 29 |
+ return mSectionIndexerDelegate.getSections(); |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+} |
@@ -0,0 +1,38 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.view.View; |
|
| 4 |
+import android.view.ViewGroup; |
|
| 5 |
+import android.widget.ListAdapter; |
|
| 6 |
+ |
|
| 7 |
+public interface StickyListHeadersAdapter extends ListAdapter {
|
|
| 8 |
+ /** |
|
| 9 |
+ * Get a View that displays the header data at the specified position in the |
|
| 10 |
+ * set. You can either create a View manually or inflate it from an XML layout |
|
| 11 |
+ * file. |
|
| 12 |
+ * |
|
| 13 |
+ * @param position |
|
| 14 |
+ * The position of the item within the adapter's data set of the item whose |
|
| 15 |
+ * header view we want. |
|
| 16 |
+ * @param convertView |
|
| 17 |
+ * The old view to reuse, if possible. Note: You should check that this view is |
|
| 18 |
+ * non-null and of an appropriate type before using. If it is not possible to |
|
| 19 |
+ * convert this view to display the correct data, this method can create a new |
|
| 20 |
+ * view. |
|
| 21 |
+ * @param parent |
|
| 22 |
+ * The parent that this view will eventually be attached to. |
|
| 23 |
+ * @return |
|
| 24 |
+ * A View corresponding to the data at the specified position. |
|
| 25 |
+ */ |
|
| 26 |
+ View getHeaderView(int position, View convertView, ViewGroup parent); |
|
| 27 |
+ |
|
| 28 |
+ /** |
|
| 29 |
+ * Get the header id associated with the specified position in the list. |
|
| 30 |
+ * |
|
| 31 |
+ * @param position |
|
| 32 |
+ * The position of the item within the adapter's data set whose header id we |
|
| 33 |
+ * want. |
|
| 34 |
+ * @return |
|
| 35 |
+ * The id of the header at the specified position. |
|
| 36 |
+ */ |
|
| 37 |
+ long getHeaderId(int position); |
|
| 38 |
+} |
@@ -0,0 +1,1131 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.annotation.SuppressLint; |
|
| 4 |
+import android.annotation.TargetApi; |
|
| 5 |
+import android.content.Context; |
|
| 6 |
+import android.content.res.TypedArray; |
|
| 7 |
+import android.database.DataSetObserver; |
|
| 8 |
+import android.graphics.Canvas; |
|
| 9 |
+import android.graphics.drawable.Drawable; |
|
| 10 |
+import android.os.Build; |
|
| 11 |
+import android.os.Parcelable; |
|
| 12 |
+import android.util.AttributeSet; |
|
| 13 |
+import android.util.Log; |
|
| 14 |
+import android.util.SparseBooleanArray; |
|
| 15 |
+import android.view.MotionEvent; |
|
| 16 |
+import android.view.View; |
|
| 17 |
+import android.view.ViewConfiguration; |
|
| 18 |
+import android.view.ViewGroup; |
|
| 19 |
+import android.widget.AbsListView; |
|
| 20 |
+import android.widget.AbsListView.MultiChoiceModeListener; |
|
| 21 |
+import android.widget.AbsListView.OnScrollListener; |
|
| 22 |
+import android.widget.AdapterView.OnItemClickListener; |
|
| 23 |
+import android.widget.AdapterView.OnItemLongClickListener; |
|
| 24 |
+import android.widget.FrameLayout; |
|
| 25 |
+import android.widget.ListView; |
|
| 26 |
+import android.widget.SectionIndexer; |
|
| 27 |
+ |
|
| 28 |
+import com.android.views.R; |
|
| 29 |
+import com.android.views.stickylistheaders.WrapperViewList.LifeCycleListener; |
|
| 30 |
+ |
|
| 31 |
+/** |
|
| 32 |
+ * Even though this is a FrameLayout subclass we still consider it a ListView. |
|
| 33 |
+ * This is because of 2 reasons: |
|
| 34 |
+ * 1. It acts like as ListView. |
|
| 35 |
+ * 2. It used to be a ListView subclass and refactoring the name would cause compatibility errors. |
|
| 36 |
+ * |
|
| 37 |
+ * @author Emil Sjölander |
|
| 38 |
+ */ |
|
| 39 |
+public class StickyListHeadersListView extends FrameLayout {
|
|
| 40 |
+ |
|
| 41 |
+ public interface OnHeaderClickListener {
|
|
| 42 |
+ void onHeaderClick(StickyListHeadersListView l, View header, |
|
| 43 |
+ int itemPosition, long headerId, boolean currentlySticky); |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ /** |
|
| 47 |
+ * Notifies the listener when the sticky headers top offset has changed. |
|
| 48 |
+ */ |
|
| 49 |
+ public interface OnStickyHeaderOffsetChangedListener {
|
|
| 50 |
+ /** |
|
| 51 |
+ * @param l The view parent |
|
| 52 |
+ * @param header The currently sticky header being offset. |
|
| 53 |
+ * This header is not guaranteed to have it's measurements set. |
|
| 54 |
+ * It is however guaranteed that this view has been measured, |
|
| 55 |
+ * therefor you should user getMeasured* methods instead of |
|
| 56 |
+ * get* methods for determining the view's size. |
|
| 57 |
+ * @param offset The amount the sticky header is offset by towards to top of the screen. |
|
| 58 |
+ */ |
|
| 59 |
+ void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset); |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ /** |
|
| 63 |
+ * Notifies the listener when the sticky header has been updated |
|
| 64 |
+ */ |
|
| 65 |
+ public interface OnStickyHeaderChangedListener {
|
|
| 66 |
+ /** |
|
| 67 |
+ * @param l The view parent |
|
| 68 |
+ * @param header The new sticky header view. |
|
| 69 |
+ * @param itemPosition The position of the item within the adapter's data set of |
|
| 70 |
+ * the item whose header is now sticky. |
|
| 71 |
+ * @param headerId The id of the new sticky header. |
|
| 72 |
+ */ |
|
| 73 |
+ void onStickyHeaderChanged(StickyListHeadersListView l, View header, |
|
| 74 |
+ int itemPosition, long headerId); |
|
| 75 |
+ |
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ /* --- Children --- */ |
|
| 79 |
+ private WrapperViewList mList; |
|
| 80 |
+ private View mHeader; |
|
| 81 |
+ |
|
| 82 |
+ /* --- Header state --- */ |
|
| 83 |
+ private Long mHeaderId; |
|
| 84 |
+ // used to not have to call getHeaderId() all the time |
|
| 85 |
+ private Integer mHeaderPosition; |
|
| 86 |
+ private Integer mHeaderOffset; |
|
| 87 |
+ |
|
| 88 |
+ /* --- Delegates --- */ |
|
| 89 |
+ private OnScrollListener mOnScrollListenerDelegate; |
|
| 90 |
+ private AdapterWrapper mAdapter; |
|
| 91 |
+ |
|
| 92 |
+ /* --- Settings --- */ |
|
| 93 |
+ private boolean mAreHeadersSticky = true; |
|
| 94 |
+ private boolean mClippingToPadding = true; |
|
| 95 |
+ private boolean mIsDrawingListUnderStickyHeader = true; |
|
| 96 |
+ private int mStickyHeaderTopOffset = 0; |
|
| 97 |
+ private int mPaddingLeft = 0; |
|
| 98 |
+ private int mPaddingTop = 0; |
|
| 99 |
+ private int mPaddingRight = 0; |
|
| 100 |
+ private int mPaddingBottom = 0; |
|
| 101 |
+ |
|
| 102 |
+ /* --- Touch handling --- */ |
|
| 103 |
+ private float mDownY; |
|
| 104 |
+ private boolean mHeaderOwnsTouch; |
|
| 105 |
+ private float mTouchSlop; |
|
| 106 |
+ |
|
| 107 |
+ /* --- Other --- */ |
|
| 108 |
+ private OnHeaderClickListener mOnHeaderClickListener; |
|
| 109 |
+ private OnStickyHeaderOffsetChangedListener mOnStickyHeaderOffsetChangedListener; |
|
| 110 |
+ private OnStickyHeaderChangedListener mOnStickyHeaderChangedListener; |
|
| 111 |
+ private AdapterWrapperDataSetObserver mDataSetObserver; |
|
| 112 |
+ private Drawable mDivider; |
|
| 113 |
+ private int mDividerHeight; |
|
| 114 |
+ |
|
| 115 |
+ public StickyListHeadersListView(Context context) {
|
|
| 116 |
+ this(context, null); |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ public StickyListHeadersListView(Context context, AttributeSet attrs) {
|
|
| 120 |
+ this(context, attrs, R.attr.stickyListHeadersListViewStyle); |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 124 |
+ public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) {
|
|
| 125 |
+ super(context, attrs, defStyle); |
|
| 126 |
+ |
|
| 127 |
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
|
| 128 |
+ |
|
| 129 |
+ // Initialize the wrapped list |
|
| 130 |
+ mList = new WrapperViewList(context); |
|
| 131 |
+ |
|
| 132 |
+ // null out divider, dividers are handled by adapter so they look good with headers |
|
| 133 |
+ mDivider = mList.getDivider(); |
|
| 134 |
+ mDividerHeight = mList.getDividerHeight(); |
|
| 135 |
+ mList.setDivider(null); |
|
| 136 |
+ mList.setDividerHeight(0); |
|
| 137 |
+ |
|
| 138 |
+ if (attrs != null) {
|
|
| 139 |
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.StickyListHeadersListView, defStyle, 0); |
|
| 140 |
+ |
|
| 141 |
+ try {
|
|
| 142 |
+ // -- View attributes -- |
|
| 143 |
+ int padding = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_padding, 0); |
|
| 144 |
+ mPaddingLeft = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingLeft, padding); |
|
| 145 |
+ mPaddingTop = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingTop, padding); |
|
| 146 |
+ mPaddingRight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingRight, padding); |
|
| 147 |
+ mPaddingBottom = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingBottom, padding); |
|
| 148 |
+ |
|
| 149 |
+ setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); |
|
| 150 |
+ |
|
| 151 |
+ // Set clip to padding on the list and reset value to default on |
|
| 152 |
+ // wrapper |
|
| 153 |
+ mClippingToPadding = a.getBoolean(R.styleable.StickyListHeadersListView_android_clipToPadding, true); |
|
| 154 |
+ super.setClipToPadding(true); |
|
| 155 |
+ mList.setClipToPadding(mClippingToPadding); |
|
| 156 |
+ |
|
| 157 |
+ // scrollbars |
|
| 158 |
+ final int scrollBars = a.getInt(R.styleable.StickyListHeadersListView_android_scrollbars, 0x00000200); |
|
| 159 |
+ mList.setVerticalScrollBarEnabled((scrollBars & 0x00000200) != 0); |
|
| 160 |
+ mList.setHorizontalScrollBarEnabled((scrollBars & 0x00000100) != 0); |
|
| 161 |
+ |
|
| 162 |
+ // overscroll |
|
| 163 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
|
|
| 164 |
+ mList.setOverScrollMode(a.getInt(R.styleable.StickyListHeadersListView_android_overScrollMode, 0)); |
|
| 165 |
+ } |
|
| 166 |
+ |
|
| 167 |
+ // -- ListView attributes -- |
|
| 168 |
+ mList.setFadingEdgeLength(a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_fadingEdgeLength, |
|
| 169 |
+ mList.getVerticalFadingEdgeLength())); |
|
| 170 |
+ final int fadingEdge = a.getInt(R.styleable.StickyListHeadersListView_android_requiresFadingEdge, 0); |
|
| 171 |
+ if (fadingEdge == 0x00001000) {
|
|
| 172 |
+ mList.setVerticalFadingEdgeEnabled(false); |
|
| 173 |
+ mList.setHorizontalFadingEdgeEnabled(true); |
|
| 174 |
+ } else if (fadingEdge == 0x00002000) {
|
|
| 175 |
+ mList.setVerticalFadingEdgeEnabled(true); |
|
| 176 |
+ mList.setHorizontalFadingEdgeEnabled(false); |
|
| 177 |
+ } else {
|
|
| 178 |
+ mList.setVerticalFadingEdgeEnabled(false); |
|
| 179 |
+ mList.setHorizontalFadingEdgeEnabled(false); |
|
| 180 |
+ } |
|
| 181 |
+ mList.setCacheColorHint(a |
|
| 182 |
+ .getColor(R.styleable.StickyListHeadersListView_android_cacheColorHint, mList.getCacheColorHint())); |
|
| 183 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
| 184 |
+ mList.setChoiceMode(a.getInt(R.styleable.StickyListHeadersListView_android_choiceMode, |
|
| 185 |
+ mList.getChoiceMode())); |
|
| 186 |
+ } |
|
| 187 |
+ mList.setDrawSelectorOnTop(a.getBoolean(R.styleable.StickyListHeadersListView_android_drawSelectorOnTop, false)); |
|
| 188 |
+ mList.setFastScrollEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_fastScrollEnabled, |
|
| 189 |
+ mList.isFastScrollEnabled())); |
|
| 190 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
| 191 |
+ mList.setFastScrollAlwaysVisible(a.getBoolean( |
|
| 192 |
+ R.styleable.StickyListHeadersListView_android_fastScrollAlwaysVisible, |
|
| 193 |
+ mList.isFastScrollAlwaysVisible())); |
|
| 194 |
+ } |
|
| 195 |
+ |
|
| 196 |
+ mList.setScrollBarStyle(a.getInt(R.styleable.StickyListHeadersListView_android_scrollbarStyle, 0)); |
|
| 197 |
+ |
|
| 198 |
+ if (a.hasValue(R.styleable.StickyListHeadersListView_android_listSelector)) {
|
|
| 199 |
+ mList.setSelector(a.getDrawable(R.styleable.StickyListHeadersListView_android_listSelector)); |
|
| 200 |
+ } |
|
| 201 |
+ |
|
| 202 |
+ mList.setScrollingCacheEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_scrollingCache, |
|
| 203 |
+ mList.isScrollingCacheEnabled())); |
|
| 204 |
+ |
|
| 205 |
+ if (a.hasValue(R.styleable.StickyListHeadersListView_android_divider)) {
|
|
| 206 |
+ mDivider = a.getDrawable(R.styleable.StickyListHeadersListView_android_divider); |
|
| 207 |
+ } |
|
| 208 |
+ |
|
| 209 |
+ mList.setStackFromBottom(a.getBoolean(R.styleable.StickyListHeadersListView_android_stackFromBottom, false)); |
|
| 210 |
+ |
|
| 211 |
+ mDividerHeight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_dividerHeight, |
|
| 212 |
+ mDividerHeight); |
|
| 213 |
+ |
|
| 214 |
+ mList.setTranscriptMode(a.getInt(R.styleable.StickyListHeadersListView_android_transcriptMode, |
|
| 215 |
+ ListView.TRANSCRIPT_MODE_DISABLED)); |
|
| 216 |
+ |
|
| 217 |
+ // -- StickyListHeaders attributes -- |
|
| 218 |
+ mAreHeadersSticky = a.getBoolean(R.styleable.StickyListHeadersListView_hasStickyHeaders, true); |
|
| 219 |
+ mIsDrawingListUnderStickyHeader = a.getBoolean( |
|
| 220 |
+ R.styleable.StickyListHeadersListView_isDrawingListUnderStickyHeader, |
|
| 221 |
+ true); |
|
| 222 |
+ } finally {
|
|
| 223 |
+ a.recycle(); |
|
| 224 |
+ } |
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ // attach some listeners to the wrapped list |
|
| 228 |
+ mList.setLifeCycleListener(new WrapperViewListLifeCycleListener()); |
|
| 229 |
+ mList.setOnScrollListener(new WrapperListScrollListener()); |
|
| 230 |
+ |
|
| 231 |
+ addView(mList); |
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ @Override |
|
| 235 |
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
| 236 |
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
|
| 237 |
+ measureHeader(mHeader); |
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ private void ensureHeaderHasCorrectLayoutParams(View header) {
|
|
| 241 |
+ ViewGroup.LayoutParams lp = header.getLayoutParams(); |
|
| 242 |
+ if (lp == null) {
|
|
| 243 |
+ lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); |
|
| 244 |
+ header.setLayoutParams(lp); |
|
| 245 |
+ } else if (lp.height == LayoutParams.MATCH_PARENT || lp.width == LayoutParams.WRAP_CONTENT) {
|
|
| 246 |
+ lp.height = LayoutParams.WRAP_CONTENT; |
|
| 247 |
+ lp.width = LayoutParams.MATCH_PARENT; |
|
| 248 |
+ header.setLayoutParams(lp); |
|
| 249 |
+ } |
|
| 250 |
+ } |
|
| 251 |
+ |
|
| 252 |
+ private void measureHeader(View header) {
|
|
| 253 |
+ if (header != null) {
|
|
| 254 |
+ final int width = getMeasuredWidth() - mPaddingLeft - mPaddingRight; |
|
| 255 |
+ final int parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec( |
|
| 256 |
+ width, MeasureSpec.EXACTLY); |
|
| 257 |
+ final int parentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, |
|
| 258 |
+ MeasureSpec.UNSPECIFIED); |
|
| 259 |
+ measureChild(header, parentWidthMeasureSpec, |
|
| 260 |
+ parentHeightMeasureSpec); |
|
| 261 |
+ } |
|
| 262 |
+ } |
|
| 263 |
+ |
|
| 264 |
+ @Override |
|
| 265 |
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
| 266 |
+ mList.layout(0, 0, mList.getMeasuredWidth(), getHeight()); |
|
| 267 |
+ if (mHeader != null) {
|
|
| 268 |
+ MarginLayoutParams lp = (MarginLayoutParams) mHeader.getLayoutParams(); |
|
| 269 |
+ int headerTop = lp.topMargin; |
|
| 270 |
+ mHeader.layout(mPaddingLeft, headerTop, mHeader.getMeasuredWidth() |
|
| 271 |
+ + mPaddingLeft, headerTop + mHeader.getMeasuredHeight()); |
|
| 272 |
+ } |
|
| 273 |
+ } |
|
| 274 |
+ |
|
| 275 |
+ @Override |
|
| 276 |
+ protected void dispatchDraw(Canvas canvas) {
|
|
| 277 |
+ // Only draw the list here. |
|
| 278 |
+ // The header should be drawn right after the lists children are drawn. |
|
| 279 |
+ // This is done so that the header is above the list items |
|
| 280 |
+ // but below the list decorators (scroll bars etc). |
|
| 281 |
+ if (mList.getVisibility() == VISIBLE || mList.getAnimation() != null) {
|
|
| 282 |
+ drawChild(canvas, mList, 0); |
|
| 283 |
+ } |
|
| 284 |
+ } |
|
| 285 |
+ |
|
| 286 |
+ // Reset values tied the header. also remove header form layout |
|
| 287 |
+ // This is called in response to the data set or the adapter changing |
|
| 288 |
+ private void clearHeader() {
|
|
| 289 |
+ if (mHeader != null) {
|
|
| 290 |
+ removeView(mHeader); |
|
| 291 |
+ mHeader = null; |
|
| 292 |
+ mHeaderId = null; |
|
| 293 |
+ mHeaderPosition = null; |
|
| 294 |
+ mHeaderOffset = null; |
|
| 295 |
+ |
|
| 296 |
+ // reset the top clipping length |
|
| 297 |
+ mList.setTopClippingLength(0); |
|
| 298 |
+ updateHeaderVisibilities(); |
|
| 299 |
+ } |
|
| 300 |
+ } |
|
| 301 |
+ |
|
| 302 |
+ private void updateOrClearHeader(int firstVisiblePosition) {
|
|
| 303 |
+ final int adapterCount = mAdapter == null ? 0 : mAdapter.getCount(); |
|
| 304 |
+ if (adapterCount == 0 || !mAreHeadersSticky) {
|
|
| 305 |
+ return; |
|
| 306 |
+ } |
|
| 307 |
+ |
|
| 308 |
+ final int headerViewCount = mList.getHeaderViewsCount(); |
|
| 309 |
+ int headerPosition = firstVisiblePosition - headerViewCount; |
|
| 310 |
+ if (mList.getChildCount() > 0) {
|
|
| 311 |
+ View firstItem = mList.getChildAt(0); |
|
| 312 |
+ if (firstItem.getBottom() < stickyHeaderTop()) {
|
|
| 313 |
+ headerPosition++; |
|
| 314 |
+ } |
|
| 315 |
+ } |
|
| 316 |
+ |
|
| 317 |
+ // It is not a mistake to call getFirstVisiblePosition() here. |
|
| 318 |
+ // Most of the time getFixedFirstVisibleItem() should be called |
|
| 319 |
+ // but that does not work great together with getChildAt() |
|
| 320 |
+ final boolean doesListHaveChildren = mList.getChildCount() != 0; |
|
| 321 |
+ final boolean isFirstViewBelowTop = doesListHaveChildren |
|
| 322 |
+ && mList.getFirstVisiblePosition() == 0 |
|
| 323 |
+ && mList.getChildAt(0).getTop() >= stickyHeaderTop(); |
|
| 324 |
+ final boolean isHeaderPositionOutsideAdapterRange = headerPosition > adapterCount - 1 |
|
| 325 |
+ || headerPosition < 0; |
|
| 326 |
+ if (!doesListHaveChildren || isHeaderPositionOutsideAdapterRange || isFirstViewBelowTop) {
|
|
| 327 |
+ clearHeader(); |
|
| 328 |
+ return; |
|
| 329 |
+ } |
|
| 330 |
+ |
|
| 331 |
+ updateHeader(headerPosition); |
|
| 332 |
+ } |
|
| 333 |
+ |
|
| 334 |
+ private void updateHeader(int headerPosition) {
|
|
| 335 |
+ |
|
| 336 |
+ // check if there is a new header should be sticky |
|
| 337 |
+ if (mHeaderPosition == null || mHeaderPosition != headerPosition) {
|
|
| 338 |
+ mHeaderPosition = headerPosition; |
|
| 339 |
+ final long headerId = mAdapter.getHeaderId(headerPosition); |
|
| 340 |
+ if (mHeaderId == null || mHeaderId != headerId) {
|
|
| 341 |
+ mHeaderId = headerId; |
|
| 342 |
+ final View header = mAdapter.getHeaderView(mHeaderPosition, mHeader, this); |
|
| 343 |
+ if (mHeader != header) {
|
|
| 344 |
+ if (header == null) {
|
|
| 345 |
+ throw new NullPointerException("header may not be null");
|
|
| 346 |
+ } |
|
| 347 |
+ swapHeader(header); |
|
| 348 |
+ } |
|
| 349 |
+ ensureHeaderHasCorrectLayoutParams(mHeader); |
|
| 350 |
+ measureHeader(mHeader); |
|
| 351 |
+ if(mOnStickyHeaderChangedListener != null) {
|
|
| 352 |
+ mOnStickyHeaderChangedListener.onStickyHeaderChanged(this, mHeader, headerPosition, mHeaderId); |
|
| 353 |
+ } |
|
| 354 |
+ // Reset mHeaderOffset to null ensuring |
|
| 355 |
+ // that it will be set on the header and |
|
| 356 |
+ // not skipped for performance reasons. |
|
| 357 |
+ mHeaderOffset = null; |
|
| 358 |
+ } |
|
| 359 |
+ } |
|
| 360 |
+ |
|
| 361 |
+ int headerOffset = stickyHeaderTop(); |
|
| 362 |
+ |
|
| 363 |
+ // Calculate new header offset |
|
| 364 |
+ // Skip looking at the first view. it never matters because it always |
|
| 365 |
+ // results in a headerOffset = 0 |
|
| 366 |
+ for (int i = 0; i < mList.getChildCount(); i++) {
|
|
| 367 |
+ final View child = mList.getChildAt(i); |
|
| 368 |
+ final boolean doesChildHaveHeader = child instanceof WrapperView && ((WrapperView) child).hasHeader(); |
|
| 369 |
+ final boolean isChildFooter = mList.containsFooterView(child); |
|
| 370 |
+ if (child.getTop() >= stickyHeaderTop() && (doesChildHaveHeader || isChildFooter)) {
|
|
| 371 |
+ headerOffset = Math.min(child.getTop() - mHeader.getMeasuredHeight(), headerOffset); |
|
| 372 |
+ break; |
|
| 373 |
+ } |
|
| 374 |
+ } |
|
| 375 |
+ |
|
| 376 |
+ setHeaderOffet(headerOffset); |
|
| 377 |
+ |
|
| 378 |
+ if (!mIsDrawingListUnderStickyHeader) {
|
|
| 379 |
+ mList.setTopClippingLength(mHeader.getMeasuredHeight() |
|
| 380 |
+ + mHeaderOffset); |
|
| 381 |
+ } |
|
| 382 |
+ |
|
| 383 |
+ updateHeaderVisibilities(); |
|
| 384 |
+ } |
|
| 385 |
+ |
|
| 386 |
+ private void swapHeader(View newHeader) {
|
|
| 387 |
+ if (mHeader != null) {
|
|
| 388 |
+ removeView(mHeader); |
|
| 389 |
+ } |
|
| 390 |
+ mHeader = newHeader; |
|
| 391 |
+ addView(mHeader); |
|
| 392 |
+ if (mOnHeaderClickListener != null) {
|
|
| 393 |
+ mHeader.setOnClickListener(new OnClickListener() {
|
|
| 394 |
+ @Override |
|
| 395 |
+ public void onClick(View v) {
|
|
| 396 |
+ mOnHeaderClickListener.onHeaderClick( |
|
| 397 |
+ StickyListHeadersListView.this, mHeader, |
|
| 398 |
+ mHeaderPosition, mHeaderId, true); |
|
| 399 |
+ } |
|
| 400 |
+ }); |
|
| 401 |
+ } |
|
| 402 |
+ mHeader.setClickable(true); |
|
| 403 |
+ } |
|
| 404 |
+ |
|
| 405 |
+ // hides the headers in the list under the sticky header. |
|
| 406 |
+ // Makes sure the other ones are showing |
|
| 407 |
+ private void updateHeaderVisibilities() {
|
|
| 408 |
+ int top = stickyHeaderTop(); |
|
| 409 |
+ int childCount = mList.getChildCount(); |
|
| 410 |
+ for (int i = 0; i < childCount; i++) {
|
|
| 411 |
+ |
|
| 412 |
+ // ensure child is a wrapper view |
|
| 413 |
+ View child = mList.getChildAt(i); |
|
| 414 |
+ if (!(child instanceof WrapperView)) {
|
|
| 415 |
+ continue; |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ // ensure wrapper view child has a header |
|
| 419 |
+ WrapperView wrapperViewChild = (WrapperView) child; |
|
| 420 |
+ if (!wrapperViewChild.hasHeader()) {
|
|
| 421 |
+ continue; |
|
| 422 |
+ } |
|
| 423 |
+ |
|
| 424 |
+ // update header views visibility |
|
| 425 |
+ View childHeader = wrapperViewChild.mHeader; |
|
| 426 |
+ if (wrapperViewChild.getTop() < top) {
|
|
| 427 |
+ if (childHeader.getVisibility() != View.INVISIBLE) {
|
|
| 428 |
+ childHeader.setVisibility(View.INVISIBLE); |
|
| 429 |
+ } |
|
| 430 |
+ } else {
|
|
| 431 |
+ if (childHeader.getVisibility() != View.VISIBLE) {
|
|
| 432 |
+ childHeader.setVisibility(View.VISIBLE); |
|
| 433 |
+ } |
|
| 434 |
+ } |
|
| 435 |
+ } |
|
| 436 |
+ } |
|
| 437 |
+ |
|
| 438 |
+ // Wrapper around setting the header offset in different ways depending on |
|
| 439 |
+ // the API version |
|
| 440 |
+ @SuppressLint("NewApi")
|
|
| 441 |
+ private void setHeaderOffet(int offset) {
|
|
| 442 |
+ if (mHeaderOffset == null || mHeaderOffset != offset) {
|
|
| 443 |
+ mHeaderOffset = offset; |
|
| 444 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
| 445 |
+ mHeader.setTranslationY(mHeaderOffset); |
|
| 446 |
+ } else {
|
|
| 447 |
+ MarginLayoutParams params = (MarginLayoutParams) mHeader.getLayoutParams(); |
|
| 448 |
+ params.topMargin = mHeaderOffset; |
|
| 449 |
+ mHeader.setLayoutParams(params); |
|
| 450 |
+ } |
|
| 451 |
+ if (mOnStickyHeaderOffsetChangedListener != null) {
|
|
| 452 |
+ mOnStickyHeaderOffsetChangedListener.onStickyHeaderOffsetChanged(this, mHeader, -mHeaderOffset); |
|
| 453 |
+ } |
|
| 454 |
+ } |
|
| 455 |
+ } |
|
| 456 |
+ |
|
| 457 |
+ @Override |
|
| 458 |
+ public boolean dispatchTouchEvent(MotionEvent ev) {
|
|
| 459 |
+ int action = ev.getAction() & MotionEvent.ACTION_MASK; |
|
| 460 |
+ if (action == MotionEvent.ACTION_DOWN) {
|
|
| 461 |
+ mDownY = ev.getY(); |
|
| 462 |
+ mHeaderOwnsTouch = mHeader != null && mDownY <= mHeader.getHeight() + mHeaderOffset; |
|
| 463 |
+ } |
|
| 464 |
+ |
|
| 465 |
+ boolean handled; |
|
| 466 |
+ if (mHeaderOwnsTouch) {
|
|
| 467 |
+ if (mHeader != null && Math.abs(mDownY - ev.getY()) <= mTouchSlop) {
|
|
| 468 |
+ handled = mHeader.dispatchTouchEvent(ev); |
|
| 469 |
+ } else {
|
|
| 470 |
+ if (mHeader != null) {
|
|
| 471 |
+ MotionEvent cancelEvent = MotionEvent.obtain(ev); |
|
| 472 |
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL); |
|
| 473 |
+ mHeader.dispatchTouchEvent(cancelEvent); |
|
| 474 |
+ cancelEvent.recycle(); |
|
| 475 |
+ } |
|
| 476 |
+ |
|
| 477 |
+ MotionEvent downEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime(), ev.getAction(), ev.getX(), mDownY, ev.getMetaState()); |
|
| 478 |
+ downEvent.setAction(MotionEvent.ACTION_DOWN); |
|
| 479 |
+ handled = mList.dispatchTouchEvent(downEvent); |
|
| 480 |
+ downEvent.recycle(); |
|
| 481 |
+ mHeaderOwnsTouch = false; |
|
| 482 |
+ } |
|
| 483 |
+ } else {
|
|
| 484 |
+ handled = mList.dispatchTouchEvent(ev); |
|
| 485 |
+ } |
|
| 486 |
+ |
|
| 487 |
+ return handled; |
|
| 488 |
+ } |
|
| 489 |
+ |
|
| 490 |
+ private class AdapterWrapperDataSetObserver extends DataSetObserver {
|
|
| 491 |
+ |
|
| 492 |
+ @Override |
|
| 493 |
+ public void onChanged() {
|
|
| 494 |
+ clearHeader(); |
|
| 495 |
+ } |
|
| 496 |
+ |
|
| 497 |
+ @Override |
|
| 498 |
+ public void onInvalidated() {
|
|
| 499 |
+ clearHeader(); |
|
| 500 |
+ } |
|
| 501 |
+ |
|
| 502 |
+ } |
|
| 503 |
+ |
|
| 504 |
+ private class WrapperListScrollListener implements OnScrollListener {
|
|
| 505 |
+ |
|
| 506 |
+ @Override |
|
| 507 |
+ public void onScroll(AbsListView view, int firstVisibleItem, |
|
| 508 |
+ int visibleItemCount, int totalItemCount) {
|
|
| 509 |
+ if (mOnScrollListenerDelegate != null) {
|
|
| 510 |
+ mOnScrollListenerDelegate.onScroll(view, firstVisibleItem, |
|
| 511 |
+ visibleItemCount, totalItemCount); |
|
| 512 |
+ } |
|
| 513 |
+ updateOrClearHeader(mList.getFixedFirstVisibleItem()); |
|
| 514 |
+ } |
|
| 515 |
+ |
|
| 516 |
+ @Override |
|
| 517 |
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
|
|
| 518 |
+ if (mOnScrollListenerDelegate != null) {
|
|
| 519 |
+ mOnScrollListenerDelegate.onScrollStateChanged(view, |
|
| 520 |
+ scrollState); |
|
| 521 |
+ } |
|
| 522 |
+ } |
|
| 523 |
+ |
|
| 524 |
+ } |
|
| 525 |
+ |
|
| 526 |
+ private class WrapperViewListLifeCycleListener implements LifeCycleListener {
|
|
| 527 |
+ |
|
| 528 |
+ @Override |
|
| 529 |
+ public void onDispatchDrawOccurred(Canvas canvas) {
|
|
| 530 |
+ // onScroll is not called often at all before froyo |
|
| 531 |
+ // therefor we need to update the header here as well. |
|
| 532 |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
|
|
| 533 |
+ updateOrClearHeader(mList.getFixedFirstVisibleItem()); |
|
| 534 |
+ } |
|
| 535 |
+ if (mHeader != null) {
|
|
| 536 |
+ if (mClippingToPadding) {
|
|
| 537 |
+ canvas.save(); |
|
| 538 |
+ canvas.clipRect(0, mPaddingTop, getRight(), getBottom()); |
|
| 539 |
+ drawChild(canvas, mHeader, 0); |
|
| 540 |
+ canvas.restore(); |
|
| 541 |
+ } else {
|
|
| 542 |
+ drawChild(canvas, mHeader, 0); |
|
| 543 |
+ } |
|
| 544 |
+ } |
|
| 545 |
+ } |
|
| 546 |
+ |
|
| 547 |
+ } |
|
| 548 |
+ |
|
| 549 |
+ private class AdapterWrapperHeaderClickHandler implements |
|
| 550 |
+ AdapterWrapper.OnHeaderClickListener {
|
|
| 551 |
+ |
|
| 552 |
+ @Override |
|
| 553 |
+ public void onHeaderClick(View header, int itemPosition, long headerId) {
|
|
| 554 |
+ mOnHeaderClickListener.onHeaderClick( |
|
| 555 |
+ StickyListHeadersListView.this, header, itemPosition, |
|
| 556 |
+ headerId, false); |
|
| 557 |
+ } |
|
| 558 |
+ |
|
| 559 |
+ } |
|
| 560 |
+ |
|
| 561 |
+ private boolean isStartOfSection(int position) {
|
|
| 562 |
+ return position == 0 || mAdapter.getHeaderId(position) != mAdapter.getHeaderId(position - 1); |
|
| 563 |
+ } |
|
| 564 |
+ |
|
| 565 |
+ public int getHeaderOverlap(int position) {
|
|
| 566 |
+ boolean isStartOfSection = isStartOfSection(Math.max(0, position - getHeaderViewsCount())); |
|
| 567 |
+ if (!isStartOfSection) {
|
|
| 568 |
+ View header = mAdapter.getHeaderView(position, null, mList); |
|
| 569 |
+ if (header == null) {
|
|
| 570 |
+ throw new NullPointerException("header may not be null");
|
|
| 571 |
+ } |
|
| 572 |
+ ensureHeaderHasCorrectLayoutParams(header); |
|
| 573 |
+ measureHeader(header); |
|
| 574 |
+ return header.getMeasuredHeight(); |
|
| 575 |
+ } |
|
| 576 |
+ return 0; |
|
| 577 |
+ } |
|
| 578 |
+ |
|
| 579 |
+ private int stickyHeaderTop() {
|
|
| 580 |
+ return mStickyHeaderTopOffset + (mClippingToPadding ? mPaddingTop : 0); |
|
| 581 |
+ } |
|
| 582 |
+ |
|
| 583 |
+ /* ---------- StickyListHeaders specific API ---------- */ |
|
| 584 |
+ |
|
| 585 |
+ public void setAreHeadersSticky(boolean areHeadersSticky) {
|
|
| 586 |
+ mAreHeadersSticky = areHeadersSticky; |
|
| 587 |
+ if (!areHeadersSticky) {
|
|
| 588 |
+ clearHeader(); |
|
| 589 |
+ } else {
|
|
| 590 |
+ updateOrClearHeader(mList.getFixedFirstVisibleItem()); |
|
| 591 |
+ } |
|
| 592 |
+ // invalidating the list will trigger dispatchDraw() |
|
| 593 |
+ mList.invalidate(); |
|
| 594 |
+ } |
|
| 595 |
+ |
|
| 596 |
+ public boolean areHeadersSticky() {
|
|
| 597 |
+ return mAreHeadersSticky; |
|
| 598 |
+ } |
|
| 599 |
+ |
|
| 600 |
+ /** |
|
| 601 |
+ * Use areHeadersSticky() method instead |
|
| 602 |
+ */ |
|
| 603 |
+ @Deprecated |
|
| 604 |
+ public boolean getAreHeadersSticky() {
|
|
| 605 |
+ return areHeadersSticky(); |
|
| 606 |
+ } |
|
| 607 |
+ |
|
| 608 |
+ /** |
|
| 609 |
+ * |
|
| 610 |
+ * @param stickyHeaderTopOffset |
|
| 611 |
+ * The offset of the sticky header fom the top of the view |
|
| 612 |
+ */ |
|
| 613 |
+ public void setStickyHeaderTopOffset(int stickyHeaderTopOffset) {
|
|
| 614 |
+ mStickyHeaderTopOffset = stickyHeaderTopOffset; |
|
| 615 |
+ updateOrClearHeader(mList.getFixedFirstVisibleItem()); |
|
| 616 |
+ } |
|
| 617 |
+ |
|
| 618 |
+ public int getStickyHeaderTopOffset() {
|
|
| 619 |
+ return mStickyHeaderTopOffset; |
|
| 620 |
+ } |
|
| 621 |
+ |
|
| 622 |
+ public void setDrawingListUnderStickyHeader( |
|
| 623 |
+ boolean drawingListUnderStickyHeader) {
|
|
| 624 |
+ mIsDrawingListUnderStickyHeader = drawingListUnderStickyHeader; |
|
| 625 |
+ // reset the top clipping length |
|
| 626 |
+ mList.setTopClippingLength(0); |
|
| 627 |
+ } |
|
| 628 |
+ |
|
| 629 |
+ public boolean isDrawingListUnderStickyHeader() {
|
|
| 630 |
+ return mIsDrawingListUnderStickyHeader; |
|
| 631 |
+ } |
|
| 632 |
+ |
|
| 633 |
+ public void setOnHeaderClickListener(OnHeaderClickListener listener) {
|
|
| 634 |
+ mOnHeaderClickListener = listener; |
|
| 635 |
+ if (mAdapter != null) {
|
|
| 636 |
+ if (mOnHeaderClickListener != null) {
|
|
| 637 |
+ mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); |
|
| 638 |
+ |
|
| 639 |
+ if (mHeader != null) {
|
|
| 640 |
+ mHeader.setOnClickListener(new OnClickListener() {
|
|
| 641 |
+ @Override |
|
| 642 |
+ public void onClick(View v) {
|
|
| 643 |
+ mOnHeaderClickListener.onHeaderClick( |
|
| 644 |
+ StickyListHeadersListView.this, mHeader, |
|
| 645 |
+ mHeaderPosition, mHeaderId, true); |
|
| 646 |
+ } |
|
| 647 |
+ }); |
|
| 648 |
+ } |
|
| 649 |
+ } else {
|
|
| 650 |
+ mAdapter.setOnHeaderClickListener(null); |
|
| 651 |
+ } |
|
| 652 |
+ } |
|
| 653 |
+ } |
|
| 654 |
+ |
|
| 655 |
+ public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener) {
|
|
| 656 |
+ mOnStickyHeaderOffsetChangedListener = listener; |
|
| 657 |
+ } |
|
| 658 |
+ |
|
| 659 |
+ public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener) {
|
|
| 660 |
+ mOnStickyHeaderChangedListener = listener; |
|
| 661 |
+ } |
|
| 662 |
+ |
|
| 663 |
+ public View getListChildAt(int index) {
|
|
| 664 |
+ return mList.getChildAt(index); |
|
| 665 |
+ } |
|
| 666 |
+ |
|
| 667 |
+ public int getListChildCount() {
|
|
| 668 |
+ return mList.getChildCount(); |
|
| 669 |
+ } |
|
| 670 |
+ |
|
| 671 |
+ /** |
|
| 672 |
+ * Use the method with extreme caution!! Changing any values on the |
|
| 673 |
+ * underlying ListView might break everything. |
|
| 674 |
+ * |
|
| 675 |
+ * @return the ListView backing this view. |
|
| 676 |
+ */ |
|
| 677 |
+ public ListView getWrappedList() {
|
|
| 678 |
+ return mList; |
|
| 679 |
+ } |
|
| 680 |
+ |
|
| 681 |
+ private boolean requireSdkVersion(int versionCode) {
|
|
| 682 |
+ if (Build.VERSION.SDK_INT < versionCode) {
|
|
| 683 |
+ Log.e("StickyListHeaders", "Api lvl must be at least "+versionCode+" to call this method");
|
|
| 684 |
+ return false; |
|
| 685 |
+ } |
|
| 686 |
+ return true; |
|
| 687 |
+ } |
|
| 688 |
+ |
|
| 689 |
+ /* ---------- ListView delegate methods ---------- */ |
|
| 690 |
+ |
|
| 691 |
+ public void setAdapter(StickyListHeadersAdapter adapter) {
|
|
| 692 |
+ if (adapter == null) {
|
|
| 693 |
+ if (mAdapter instanceof SectionIndexerAdapterWrapper) {
|
|
| 694 |
+ ((SectionIndexerAdapterWrapper) mAdapter).mSectionIndexerDelegate = null; |
|
| 695 |
+ } |
|
| 696 |
+ if (mAdapter != null) {
|
|
| 697 |
+ mAdapter.mDelegate = null; |
|
| 698 |
+ } |
|
| 699 |
+ mList.setAdapter(null); |
|
| 700 |
+ clearHeader(); |
|
| 701 |
+ return; |
|
| 702 |
+ } |
|
| 703 |
+ if (mAdapter != null) {
|
|
| 704 |
+ mAdapter.unregisterDataSetObserver(mDataSetObserver); |
|
| 705 |
+ } |
|
| 706 |
+ |
|
| 707 |
+ if (adapter instanceof SectionIndexer) {
|
|
| 708 |
+ mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter); |
|
| 709 |
+ } else {
|
|
| 710 |
+ mAdapter = new AdapterWrapper(getContext(), adapter); |
|
| 711 |
+ } |
|
| 712 |
+ mDataSetObserver = new AdapterWrapperDataSetObserver(); |
|
| 713 |
+ mAdapter.registerDataSetObserver(mDataSetObserver); |
|
| 714 |
+ |
|
| 715 |
+ if (mOnHeaderClickListener != null) {
|
|
| 716 |
+ mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); |
|
| 717 |
+ } else {
|
|
| 718 |
+ mAdapter.setOnHeaderClickListener(null); |
|
| 719 |
+ } |
|
| 720 |
+ |
|
| 721 |
+ mAdapter.setDivider(mDivider, mDividerHeight); |
|
| 722 |
+ |
|
| 723 |
+ mList.setAdapter(mAdapter); |
|
| 724 |
+ clearHeader(); |
|
| 725 |
+ } |
|
| 726 |
+ |
|
| 727 |
+ public StickyListHeadersAdapter getAdapter() {
|
|
| 728 |
+ return mAdapter == null ? null : mAdapter.mDelegate; |
|
| 729 |
+ } |
|
| 730 |
+ |
|
| 731 |
+ public void setDivider(Drawable divider) {
|
|
| 732 |
+ mDivider = divider; |
|
| 733 |
+ if (mAdapter != null) {
|
|
| 734 |
+ mAdapter.setDivider(mDivider, mDividerHeight); |
|
| 735 |
+ } |
|
| 736 |
+ } |
|
| 737 |
+ |
|
| 738 |
+ public void setDividerHeight(int dividerHeight) {
|
|
| 739 |
+ mDividerHeight = dividerHeight; |
|
| 740 |
+ if (mAdapter != null) {
|
|
| 741 |
+ mAdapter.setDivider(mDivider, mDividerHeight); |
|
| 742 |
+ } |
|
| 743 |
+ } |
|
| 744 |
+ |
|
| 745 |
+ public Drawable getDivider() {
|
|
| 746 |
+ return mDivider; |
|
| 747 |
+ } |
|
| 748 |
+ |
|
| 749 |
+ public int getDividerHeight() {
|
|
| 750 |
+ return mDividerHeight; |
|
| 751 |
+ } |
|
| 752 |
+ |
|
| 753 |
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
|
|
| 754 |
+ mOnScrollListenerDelegate = onScrollListener; |
|
| 755 |
+ } |
|
| 756 |
+ |
|
| 757 |
+ @Override |
|
| 758 |
+ public void setOnTouchListener(final OnTouchListener l) {
|
|
| 759 |
+ if (l != null) {
|
|
| 760 |
+ mList.setOnTouchListener(new OnTouchListener() {
|
|
| 761 |
+ @Override |
|
| 762 |
+ public boolean onTouch(View v, MotionEvent event) {
|
|
| 763 |
+ return l.onTouch(StickyListHeadersListView.this, event); |
|
| 764 |
+ } |
|
| 765 |
+ }); |
|
| 766 |
+ } else {
|
|
| 767 |
+ mList.setOnTouchListener(null); |
|
| 768 |
+ } |
|
| 769 |
+ } |
|
| 770 |
+ |
|
| 771 |
+ public void setOnItemClickListener(OnItemClickListener listener) {
|
|
| 772 |
+ mList.setOnItemClickListener(listener); |
|
| 773 |
+ } |
|
| 774 |
+ |
|
| 775 |
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
|
|
| 776 |
+ mList.setOnItemLongClickListener(listener); |
|
| 777 |
+ } |
|
| 778 |
+ |
|
| 779 |
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
|
|
| 780 |
+ mList.addHeaderView(v, data, isSelectable); |
|
| 781 |
+ } |
|
| 782 |
+ |
|
| 783 |
+ public void addHeaderView(View v) {
|
|
| 784 |
+ mList.addHeaderView(v); |
|
| 785 |
+ } |
|
| 786 |
+ |
|
| 787 |
+ public void removeHeaderView(View v) {
|
|
| 788 |
+ mList.removeHeaderView(v); |
|
| 789 |
+ } |
|
| 790 |
+ |
|
| 791 |
+ public int getHeaderViewsCount() {
|
|
| 792 |
+ return mList.getHeaderViewsCount(); |
|
| 793 |
+ } |
|
| 794 |
+ |
|
| 795 |
+ public void addFooterView(View v, Object data, boolean isSelectable) {
|
|
| 796 |
+ mList.addFooterView(v, data, isSelectable); |
|
| 797 |
+ } |
|
| 798 |
+ |
|
| 799 |
+ public void addFooterView(View v) {
|
|
| 800 |
+ mList.addFooterView(v); |
|
| 801 |
+ } |
|
| 802 |
+ |
|
| 803 |
+ public void removeFooterView(View v) {
|
|
| 804 |
+ mList.removeFooterView(v); |
|
| 805 |
+ } |
|
| 806 |
+ |
|
| 807 |
+ public int getFooterViewsCount() {
|
|
| 808 |
+ return mList.getFooterViewsCount(); |
|
| 809 |
+ } |
|
| 810 |
+ |
|
| 811 |
+ public void setEmptyView(View v) {
|
|
| 812 |
+ mList.setEmptyView(v); |
|
| 813 |
+ } |
|
| 814 |
+ |
|
| 815 |
+ public View getEmptyView() {
|
|
| 816 |
+ return mList.getEmptyView(); |
|
| 817 |
+ } |
|
| 818 |
+ |
|
| 819 |
+ @Override |
|
| 820 |
+ public boolean isVerticalScrollBarEnabled() {
|
|
| 821 |
+ return mList.isVerticalScrollBarEnabled(); |
|
| 822 |
+ } |
|
| 823 |
+ |
|
| 824 |
+ @Override |
|
| 825 |
+ public boolean isHorizontalScrollBarEnabled() {
|
|
| 826 |
+ return mList.isHorizontalScrollBarEnabled(); |
|
| 827 |
+ } |
|
| 828 |
+ |
|
| 829 |
+ @Override |
|
| 830 |
+ public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
|
|
| 831 |
+ mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled); |
|
| 832 |
+ } |
|
| 833 |
+ |
|
| 834 |
+ @Override |
|
| 835 |
+ public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) {
|
|
| 836 |
+ mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled); |
|
| 837 |
+ } |
|
| 838 |
+ |
|
| 839 |
+ @Override |
|
| 840 |
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD) |
|
| 841 |
+ public int getOverScrollMode() {
|
|
| 842 |
+ if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) {
|
|
| 843 |
+ return mList.getOverScrollMode(); |
|
| 844 |
+ } |
|
| 845 |
+ return 0; |
|
| 846 |
+ } |
|
| 847 |
+ |
|
| 848 |
+ @Override |
|
| 849 |
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD) |
|
| 850 |
+ public void setOverScrollMode(int mode) {
|
|
| 851 |
+ if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) {
|
|
| 852 |
+ if (mList != null) {
|
|
| 853 |
+ mList.setOverScrollMode(mode); |
|
| 854 |
+ } |
|
| 855 |
+ } |
|
| 856 |
+ } |
|
| 857 |
+ |
|
| 858 |
+ @TargetApi(Build.VERSION_CODES.FROYO) |
|
| 859 |
+ public void smoothScrollBy(int distance, int duration) {
|
|
| 860 |
+ if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
|
|
| 861 |
+ mList.smoothScrollBy(distance, duration); |
|
| 862 |
+ } |
|
| 863 |
+ } |
|
| 864 |
+ |
|
| 865 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 866 |
+ public void smoothScrollByOffset(int offset) {
|
|
| 867 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 868 |
+ mList.smoothScrollByOffset(offset); |
|
| 869 |
+ } |
|
| 870 |
+ } |
|
| 871 |
+ |
|
| 872 |
+ @SuppressLint("NewApi")
|
|
| 873 |
+ @TargetApi(Build.VERSION_CODES.FROYO) |
|
| 874 |
+ public void smoothScrollToPosition(int position) {
|
|
| 875 |
+ if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
|
|
| 876 |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
| 877 |
+ mList.smoothScrollToPosition(position); |
|
| 878 |
+ } else {
|
|
| 879 |
+ int offset = mAdapter == null ? 0 : getHeaderOverlap(position); |
|
| 880 |
+ offset -= mClippingToPadding ? 0 : mPaddingTop; |
|
| 881 |
+ mList.smoothScrollToPositionFromTop(position, offset); |
|
| 882 |
+ } |
|
| 883 |
+ } |
|
| 884 |
+ } |
|
| 885 |
+ |
|
| 886 |
+ @TargetApi(Build.VERSION_CODES.FROYO) |
|
| 887 |
+ public void smoothScrollToPosition(int position, int boundPosition) {
|
|
| 888 |
+ if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
|
|
| 889 |
+ mList.smoothScrollToPosition(position, boundPosition); |
|
| 890 |
+ } |
|
| 891 |
+ } |
|
| 892 |
+ |
|
| 893 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 894 |
+ public void smoothScrollToPositionFromTop(int position, int offset) {
|
|
| 895 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 896 |
+ offset += mAdapter == null ? 0 : getHeaderOverlap(position); |
|
| 897 |
+ offset -= mClippingToPadding ? 0 : mPaddingTop; |
|
| 898 |
+ mList.smoothScrollToPositionFromTop(position, offset); |
|
| 899 |
+ } |
|
| 900 |
+ } |
|
| 901 |
+ |
|
| 902 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 903 |
+ public void smoothScrollToPositionFromTop(int position, int offset, |
|
| 904 |
+ int duration) {
|
|
| 905 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 906 |
+ offset += mAdapter == null ? 0 : getHeaderOverlap(position); |
|
| 907 |
+ offset -= mClippingToPadding ? 0 : mPaddingTop; |
|
| 908 |
+ mList.smoothScrollToPositionFromTop(position, offset, duration); |
|
| 909 |
+ } |
|
| 910 |
+ } |
|
| 911 |
+ |
|
| 912 |
+ public void setSelection(int position) {
|
|
| 913 |
+ setSelectionFromTop(position, 0); |
|
| 914 |
+ } |
|
| 915 |
+ |
|
| 916 |
+ public void setSelectionAfterHeaderView() {
|
|
| 917 |
+ mList.setSelectionAfterHeaderView(); |
|
| 918 |
+ } |
|
| 919 |
+ |
|
| 920 |
+ public void setSelectionFromTop(int position, int y) {
|
|
| 921 |
+ y += mAdapter == null ? 0 : getHeaderOverlap(position); |
|
| 922 |
+ y -= mClippingToPadding ? 0 : mPaddingTop; |
|
| 923 |
+ mList.setSelectionFromTop(position, y); |
|
| 924 |
+ } |
|
| 925 |
+ |
|
| 926 |
+ public void setSelector(Drawable sel) {
|
|
| 927 |
+ mList.setSelector(sel); |
|
| 928 |
+ } |
|
| 929 |
+ |
|
| 930 |
+ public void setSelector(int resID) {
|
|
| 931 |
+ mList.setSelector(resID); |
|
| 932 |
+ } |
|
| 933 |
+ |
|
| 934 |
+ public int getFirstVisiblePosition() {
|
|
| 935 |
+ return mList.getFirstVisiblePosition(); |
|
| 936 |
+ } |
|
| 937 |
+ |
|
| 938 |
+ public int getLastVisiblePosition() {
|
|
| 939 |
+ return mList.getLastVisiblePosition(); |
|
| 940 |
+ } |
|
| 941 |
+ |
|
| 942 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 943 |
+ public void setChoiceMode(int choiceMode) {
|
|
| 944 |
+ mList.setChoiceMode(choiceMode); |
|
| 945 |
+ } |
|
| 946 |
+ |
|
| 947 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 948 |
+ public void setItemChecked(int position, boolean value) {
|
|
| 949 |
+ mList.setItemChecked(position, value); |
|
| 950 |
+ } |
|
| 951 |
+ |
|
| 952 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 953 |
+ public int getCheckedItemCount() {
|
|
| 954 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 955 |
+ return mList.getCheckedItemCount(); |
|
| 956 |
+ } |
|
| 957 |
+ return 0; |
|
| 958 |
+ } |
|
| 959 |
+ |
|
| 960 |
+ @TargetApi(Build.VERSION_CODES.FROYO) |
|
| 961 |
+ public long[] getCheckedItemIds() {
|
|
| 962 |
+ if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
|
|
| 963 |
+ return mList.getCheckedItemIds(); |
|
| 964 |
+ } |
|
| 965 |
+ return null; |
|
| 966 |
+ } |
|
| 967 |
+ |
|
| 968 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 969 |
+ public int getCheckedItemPosition() {
|
|
| 970 |
+ return mList.getCheckedItemPosition(); |
|
| 971 |
+ } |
|
| 972 |
+ |
|
| 973 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 974 |
+ public SparseBooleanArray getCheckedItemPositions() {
|
|
| 975 |
+ return mList.getCheckedItemPositions(); |
|
| 976 |
+ } |
|
| 977 |
+ |
|
| 978 |
+ public int getCount() {
|
|
| 979 |
+ return mList.getCount(); |
|
| 980 |
+ } |
|
| 981 |
+ |
|
| 982 |
+ public Object getItemAtPosition(int position) {
|
|
| 983 |
+ return mList.getItemAtPosition(position); |
|
| 984 |
+ } |
|
| 985 |
+ |
|
| 986 |
+ public long getItemIdAtPosition(int position) {
|
|
| 987 |
+ return mList.getItemIdAtPosition(position); |
|
| 988 |
+ } |
|
| 989 |
+ |
|
| 990 |
+ @Override |
|
| 991 |
+ public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
|
|
| 992 |
+ mList.setOnCreateContextMenuListener(l); |
|
| 993 |
+ } |
|
| 994 |
+ |
|
| 995 |
+ @Override |
|
| 996 |
+ public boolean showContextMenu() {
|
|
| 997 |
+ return mList.showContextMenu(); |
|
| 998 |
+ } |
|
| 999 |
+ |
|
| 1000 |
+ public void invalidateViews() {
|
|
| 1001 |
+ mList.invalidateViews(); |
|
| 1002 |
+ } |
|
| 1003 |
+ |
|
| 1004 |
+ @Override |
|
| 1005 |
+ public void setClipToPadding(boolean clipToPadding) {
|
|
| 1006 |
+ if (mList != null) {
|
|
| 1007 |
+ mList.setClipToPadding(clipToPadding); |
|
| 1008 |
+ } |
|
| 1009 |
+ mClippingToPadding = clipToPadding; |
|
| 1010 |
+ } |
|
| 1011 |
+ |
|
| 1012 |
+ @Override |
|
| 1013 |
+ public void setPadding(int left, int top, int right, int bottom) {
|
|
| 1014 |
+ mPaddingLeft = left; |
|
| 1015 |
+ mPaddingTop = top; |
|
| 1016 |
+ mPaddingRight = right; |
|
| 1017 |
+ mPaddingBottom = bottom; |
|
| 1018 |
+ |
|
| 1019 |
+ if (mList != null) {
|
|
| 1020 |
+ mList.setPadding(left, top, right, bottom); |
|
| 1021 |
+ } |
|
| 1022 |
+ super.setPadding(0, 0, 0, 0); |
|
| 1023 |
+ requestLayout(); |
|
| 1024 |
+ } |
|
| 1025 |
+ |
|
| 1026 |
+ /* |
|
| 1027 |
+ * Overrides an @hide method in View |
|
| 1028 |
+ */ |
|
| 1029 |
+ protected void recomputePadding() {
|
|
| 1030 |
+ setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); |
|
| 1031 |
+ } |
|
| 1032 |
+ |
|
| 1033 |
+ @Override |
|
| 1034 |
+ public int getPaddingLeft() {
|
|
| 1035 |
+ return mPaddingLeft; |
|
| 1036 |
+ } |
|
| 1037 |
+ |
|
| 1038 |
+ @Override |
|
| 1039 |
+ public int getPaddingTop() {
|
|
| 1040 |
+ return mPaddingTop; |
|
| 1041 |
+ } |
|
| 1042 |
+ |
|
| 1043 |
+ @Override |
|
| 1044 |
+ public int getPaddingRight() {
|
|
| 1045 |
+ return mPaddingRight; |
|
| 1046 |
+ } |
|
| 1047 |
+ |
|
| 1048 |
+ @Override |
|
| 1049 |
+ public int getPaddingBottom() {
|
|
| 1050 |
+ return mPaddingBottom; |
|
| 1051 |
+ } |
|
| 1052 |
+ |
|
| 1053 |
+ public void setFastScrollEnabled(boolean fastScrollEnabled) {
|
|
| 1054 |
+ mList.setFastScrollEnabled(fastScrollEnabled); |
|
| 1055 |
+ } |
|
| 1056 |
+ |
|
| 1057 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 1058 |
+ public void setFastScrollAlwaysVisible(boolean alwaysVisible) {
|
|
| 1059 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 1060 |
+ mList.setFastScrollAlwaysVisible(alwaysVisible); |
|
| 1061 |
+ } |
|
| 1062 |
+ } |
|
| 1063 |
+ |
|
| 1064 |
+ /** |
|
| 1065 |
+ * @return true if the fast scroller will always show. False on pre-Honeycomb devices. |
|
| 1066 |
+ * @see AbsListView#isFastScrollAlwaysVisible() |
|
| 1067 |
+ */ |
|
| 1068 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 1069 |
+ public boolean isFastScrollAlwaysVisible() {
|
|
| 1070 |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
| 1071 |
+ return false; |
|
| 1072 |
+ } |
|
| 1073 |
+ return mList.isFastScrollAlwaysVisible(); |
|
| 1074 |
+ } |
|
| 1075 |
+ |
|
| 1076 |
+ public void setScrollBarStyle(int style) {
|
|
| 1077 |
+ mList.setScrollBarStyle(style); |
|
| 1078 |
+ } |
|
| 1079 |
+ |
|
| 1080 |
+ public int getScrollBarStyle() {
|
|
| 1081 |
+ return mList.getScrollBarStyle(); |
|
| 1082 |
+ } |
|
| 1083 |
+ |
|
| 1084 |
+ public int getPositionForView(View view) {
|
|
| 1085 |
+ return mList.getPositionForView(view); |
|
| 1086 |
+ } |
|
| 1087 |
+ |
|
| 1088 |
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
|
| 1089 |
+ public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
|
| 1090 |
+ if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
|
|
| 1091 |
+ mList.setMultiChoiceModeListener(listener); |
|
| 1092 |
+ } |
|
| 1093 |
+ } |
|
| 1094 |
+ |
|
| 1095 |
+ @Override |
|
| 1096 |
+ public Parcelable onSaveInstanceState() {
|
|
| 1097 |
+ Parcelable superState = super.onSaveInstanceState(); |
|
| 1098 |
+ if (superState != BaseSavedState.EMPTY_STATE) {
|
|
| 1099 |
+ throw new IllegalStateException("Handling non empty state of parent class is not implemented");
|
|
| 1100 |
+ } |
|
| 1101 |
+ return mList.onSaveInstanceState(); |
|
| 1102 |
+ } |
|
| 1103 |
+ |
|
| 1104 |
+ @Override |
|
| 1105 |
+ public void onRestoreInstanceState(Parcelable state) {
|
|
| 1106 |
+ super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); |
|
| 1107 |
+ mList.onRestoreInstanceState(state); |
|
| 1108 |
+ } |
|
| 1109 |
+ |
|
| 1110 |
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) |
|
| 1111 |
+ @Override |
|
| 1112 |
+ public boolean canScrollVertically(int direction) {
|
|
| 1113 |
+ return mList.canScrollVertically(direction); |
|
| 1114 |
+ } |
|
| 1115 |
+ |
|
| 1116 |
+ public void setTranscriptMode (int mode) {
|
|
| 1117 |
+ mList.setTranscriptMode(mode); |
|
| 1118 |
+ } |
|
| 1119 |
+ |
|
| 1120 |
+ public void setBlockLayoutChildren(boolean blockLayoutChildren) {
|
|
| 1121 |
+ mList.setBlockLayoutChildren(blockLayoutChildren); |
|
| 1122 |
+ } |
|
| 1123 |
+ |
|
| 1124 |
+ public void setStackFromBottom(boolean stackFromBottom) {
|
|
| 1125 |
+ mList.setStackFromBottom(stackFromBottom); |
|
| 1126 |
+ } |
|
| 1127 |
+ |
|
| 1128 |
+ public boolean isStackFromBottom() {
|
|
| 1129 |
+ return mList.isStackFromBottom(); |
|
| 1130 |
+ } |
|
| 1131 |
+} |
@@ -0,0 +1,156 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.graphics.Canvas; |
|
| 5 |
+import android.graphics.drawable.Drawable; |
|
| 6 |
+import android.os.Build; |
|
| 7 |
+import android.view.View; |
|
| 8 |
+import android.view.ViewGroup; |
|
| 9 |
+import android.view.ViewParent; |
|
| 10 |
+ |
|
| 11 |
+/** |
|
| 12 |
+ * |
|
| 13 |
+ * the view that wrapps a divider header and a normal list item. The listview sees this as 1 item |
|
| 14 |
+ * |
|
| 15 |
+ * @author Emil Sjölander |
|
| 16 |
+ */ |
|
| 17 |
+public class WrapperView extends ViewGroup {
|
|
| 18 |
+ |
|
| 19 |
+ View mItem; |
|
| 20 |
+ Drawable mDivider; |
|
| 21 |
+ int mDividerHeight; |
|
| 22 |
+ View mHeader; |
|
| 23 |
+ int mItemTop; |
|
| 24 |
+ |
|
| 25 |
+ WrapperView(Context c) {
|
|
| 26 |
+ super(c); |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ public boolean hasHeader() {
|
|
| 30 |
+ return mHeader != null; |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ public View getItem() {
|
|
| 34 |
+ return mItem; |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ public View getHeader() {
|
|
| 38 |
+ return mHeader; |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ void update(View item, View header, Drawable divider, int dividerHeight) {
|
|
| 42 |
+ |
|
| 43 |
+ //every wrapperview must have a list item |
|
| 44 |
+ if (item == null) {
|
|
| 45 |
+ throw new NullPointerException("List view item must not be null.");
|
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view |
|
| 49 |
+ if (this.mItem != item) {
|
|
| 50 |
+ removeView(this.mItem); |
|
| 51 |
+ this.mItem = item; |
|
| 52 |
+ final ViewParent parent = item.getParent(); |
|
| 53 |
+ if(parent != null && parent != this) {
|
|
| 54 |
+ if(parent instanceof ViewGroup) {
|
|
| 55 |
+ ((ViewGroup) parent).removeView(item); |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+ addView(item); |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ //same logik as above but for the header |
|
| 62 |
+ if (this.mHeader != header) {
|
|
| 63 |
+ if (this.mHeader != null) {
|
|
| 64 |
+ removeView(this.mHeader); |
|
| 65 |
+ } |
|
| 66 |
+ this.mHeader = header; |
|
| 67 |
+ if (header != null) {
|
|
| 68 |
+ addView(header); |
|
| 69 |
+ } |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ if (this.mDivider != divider) {
|
|
| 73 |
+ this.mDivider = divider; |
|
| 74 |
+ this.mDividerHeight = dividerHeight; |
|
| 75 |
+ invalidate(); |
|
| 76 |
+ } |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ @Override |
|
| 80 |
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
| 81 |
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); |
|
| 82 |
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, |
|
| 83 |
+ MeasureSpec.EXACTLY); |
|
| 84 |
+ int measuredHeight = 0; |
|
| 85 |
+ |
|
| 86 |
+ //measure header or divider. when there is a header visible it acts as the divider |
|
| 87 |
+ if (mHeader != null) {
|
|
| 88 |
+ LayoutParams params = mHeader.getLayoutParams(); |
|
| 89 |
+ if (params != null && params.height > 0) {
|
|
| 90 |
+ mHeader.measure(childWidthMeasureSpec, |
|
| 91 |
+ MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); |
|
| 92 |
+ } else {
|
|
| 93 |
+ mHeader.measure(childWidthMeasureSpec, |
|
| 94 |
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
|
| 95 |
+ } |
|
| 96 |
+ measuredHeight += mHeader.getMeasuredHeight(); |
|
| 97 |
+ } else if (mDivider != null&&mItem.getVisibility()!=View.GONE) {
|
|
| 98 |
+ measuredHeight += mDividerHeight; |
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ //measure item |
|
| 102 |
+ LayoutParams params = mItem.getLayoutParams(); |
|
| 103 |
+ //enable hiding listview item,ex. toggle off items in group |
|
| 104 |
+ if(mItem.getVisibility()==View.GONE){
|
|
| 105 |
+ mItem.measure(childWidthMeasureSpec, |
|
| 106 |
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY)); |
|
| 107 |
+ }else if (params != null && params.height >= 0) {
|
|
| 108 |
+ mItem.measure(childWidthMeasureSpec, |
|
| 109 |
+ MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); |
|
| 110 |
+ measuredHeight += mItem.getMeasuredHeight(); |
|
| 111 |
+ } else {
|
|
| 112 |
+ mItem.measure(childWidthMeasureSpec, |
|
| 113 |
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
|
| 114 |
+ measuredHeight += mItem.getMeasuredHeight(); |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ |
|
| 118 |
+ setMeasuredDimension(measuredWidth, measuredHeight); |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ @Override |
|
| 122 |
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
| 123 |
+ |
|
| 124 |
+ l = 0; |
|
| 125 |
+ t = 0; |
|
| 126 |
+ r = getWidth(); |
|
| 127 |
+ b = getHeight(); |
|
| 128 |
+ |
|
| 129 |
+ if (mHeader != null) {
|
|
| 130 |
+ int headerHeight = mHeader.getMeasuredHeight(); |
|
| 131 |
+ mHeader.layout(l, t, r, headerHeight); |
|
| 132 |
+ mItemTop = headerHeight; |
|
| 133 |
+ mItem.layout(l, headerHeight, r, b); |
|
| 134 |
+ } else if (mDivider != null) {
|
|
| 135 |
+ mDivider.setBounds(l, t, r, mDividerHeight); |
|
| 136 |
+ mItemTop = mDividerHeight; |
|
| 137 |
+ mItem.layout(l, mDividerHeight, r, b); |
|
| 138 |
+ } else {
|
|
| 139 |
+ mItemTop = t; |
|
| 140 |
+ mItem.layout(l, t, r, b); |
|
| 141 |
+ } |
|
| 142 |
+ } |
|
| 143 |
+ |
|
| 144 |
+ @Override |
|
| 145 |
+ protected void dispatchDraw(Canvas canvas) {
|
|
| 146 |
+ super.dispatchDraw(canvas); |
|
| 147 |
+ if (mHeader == null && mDivider != null&&mItem.getVisibility()!=View.GONE) {
|
|
| 148 |
+ // Drawable.setBounds() does not seem to work pre-honeycomb. So have |
|
| 149 |
+ // to do this instead |
|
| 150 |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
| 151 |
+ canvas.clipRect(0, 0, getWidth(), mDividerHeight); |
|
| 152 |
+ } |
|
| 153 |
+ mDivider.draw(canvas); |
|
| 154 |
+ } |
|
| 155 |
+ } |
|
| 156 |
+} |
@@ -0,0 +1,196 @@ |
||
| 1 |
+package com.android.views.stickylistheaders; |
|
| 2 |
+ |
|
| 3 |
+import android.content.Context; |
|
| 4 |
+import android.graphics.Canvas; |
|
| 5 |
+import android.graphics.Rect; |
|
| 6 |
+import android.os.Build; |
|
| 7 |
+import android.view.View; |
|
| 8 |
+import android.widget.AbsListView; |
|
| 9 |
+import android.widget.ListView; |
|
| 10 |
+ |
|
| 11 |
+import java.lang.reflect.Field; |
|
| 12 |
+import java.util.ArrayList; |
|
| 13 |
+import java.util.List; |
|
| 14 |
+ |
|
| 15 |
+class WrapperViewList extends ListView {
|
|
| 16 |
+ |
|
| 17 |
+ interface LifeCycleListener {
|
|
| 18 |
+ void onDispatchDrawOccurred(Canvas canvas); |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ private LifeCycleListener mLifeCycleListener; |
|
| 22 |
+ private List<View> mFooterViews; |
|
| 23 |
+ private int mTopClippingLength; |
|
| 24 |
+ private Rect mSelectorRect = new Rect();// for if reflection fails |
|
| 25 |
+ private Field mSelectorPositionField; |
|
| 26 |
+ private boolean mClippingToPadding = true; |
|
| 27 |
+ private boolean mBlockLayoutChildren = false; |
|
| 28 |
+ |
|
| 29 |
+ public WrapperViewList(Context context) {
|
|
| 30 |
+ super(context); |
|
| 31 |
+ |
|
| 32 |
+ // Use reflection to be able to change the size/position of the list |
|
| 33 |
+ // selector so it does not come under/over the header |
|
| 34 |
+ try {
|
|
| 35 |
+ Field selectorRectField = AbsListView.class.getDeclaredField("mSelectorRect");
|
|
| 36 |
+ selectorRectField.setAccessible(true); |
|
| 37 |
+ mSelectorRect = (Rect) selectorRectField.get(this); |
|
| 38 |
+ |
|
| 39 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
|
| 40 |
+ mSelectorPositionField = AbsListView.class.getDeclaredField("mSelectorPosition");
|
|
| 41 |
+ mSelectorPositionField.setAccessible(true); |
|
| 42 |
+ } |
|
| 43 |
+ } catch (NoSuchFieldException e) {
|
|
| 44 |
+ e.printStackTrace(); |
|
| 45 |
+ } catch (IllegalArgumentException e) {
|
|
| 46 |
+ e.printStackTrace(); |
|
| 47 |
+ } catch (IllegalAccessException e) {
|
|
| 48 |
+ e.printStackTrace(); |
|
| 49 |
+ } |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ @Override |
|
| 53 |
+ public boolean performItemClick(View view, int position, long id) {
|
|
| 54 |
+ if (view instanceof WrapperView) {
|
|
| 55 |
+ view = ((WrapperView) view).mItem; |
|
| 56 |
+ } |
|
| 57 |
+ return super.performItemClick(view, position, id); |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ private void positionSelectorRect() {
|
|
| 61 |
+ if (!mSelectorRect.isEmpty()) {
|
|
| 62 |
+ int selectorPosition = getSelectorPosition(); |
|
| 63 |
+ if (selectorPosition >= 0) {
|
|
| 64 |
+ int firstVisibleItem = getFixedFirstVisibleItem(); |
|
| 65 |
+ View v = getChildAt(selectorPosition - firstVisibleItem); |
|
| 66 |
+ if (v instanceof WrapperView) {
|
|
| 67 |
+ WrapperView wrapper = ((WrapperView) v); |
|
| 68 |
+ mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop; |
|
| 69 |
+ } |
|
| 70 |
+ } |
|
| 71 |
+ } |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ private int getSelectorPosition() {
|
|
| 75 |
+ if (mSelectorPositionField == null) { // not all supported andorid
|
|
| 76 |
+ // version have this variable |
|
| 77 |
+ for (int i = 0; i < getChildCount(); i++) {
|
|
| 78 |
+ if (getChildAt(i).getBottom() == mSelectorRect.bottom) {
|
|
| 79 |
+ return i + getFixedFirstVisibleItem(); |
|
| 80 |
+ } |
|
| 81 |
+ } |
|
| 82 |
+ } else {
|
|
| 83 |
+ try {
|
|
| 84 |
+ return mSelectorPositionField.getInt(this); |
|
| 85 |
+ } catch (IllegalArgumentException e) {
|
|
| 86 |
+ e.printStackTrace(); |
|
| 87 |
+ } catch (IllegalAccessException e) {
|
|
| 88 |
+ e.printStackTrace(); |
|
| 89 |
+ } |
|
| 90 |
+ } |
|
| 91 |
+ return -1; |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ @Override |
|
| 95 |
+ protected void dispatchDraw(Canvas canvas) {
|
|
| 96 |
+ positionSelectorRect(); |
|
| 97 |
+ if (mTopClippingLength != 0) {
|
|
| 98 |
+ canvas.save(); |
|
| 99 |
+ Rect clipping = canvas.getClipBounds(); |
|
| 100 |
+ clipping.top = mTopClippingLength; |
|
| 101 |
+ canvas.clipRect(clipping); |
|
| 102 |
+ super.dispatchDraw(canvas); |
|
| 103 |
+ canvas.restore(); |
|
| 104 |
+ } else {
|
|
| 105 |
+ super.dispatchDraw(canvas); |
|
| 106 |
+ } |
|
| 107 |
+ mLifeCycleListener.onDispatchDrawOccurred(canvas); |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ void setLifeCycleListener(LifeCycleListener lifeCycleListener) {
|
|
| 111 |
+ mLifeCycleListener = lifeCycleListener; |
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ @Override |
|
| 115 |
+ public void addFooterView(View v) {
|
|
| 116 |
+ super.addFooterView(v); |
|
| 117 |
+ addInternalFooterView(v); |
|
| 118 |
+ } |
|
| 119 |
+ |
|
| 120 |
+ @Override |
|
| 121 |
+ public void addFooterView(View v, Object data, boolean isSelectable) {
|
|
| 122 |
+ super.addFooterView(v, data, isSelectable); |
|
| 123 |
+ addInternalFooterView(v); |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ private void addInternalFooterView(View v) {
|
|
| 127 |
+ if (mFooterViews == null) {
|
|
| 128 |
+ mFooterViews = new ArrayList<View>(); |
|
| 129 |
+ } |
|
| 130 |
+ mFooterViews.add(v); |
|
| 131 |
+ } |
|
| 132 |
+ |
|
| 133 |
+ @Override |
|
| 134 |
+ public boolean removeFooterView(View v) {
|
|
| 135 |
+ if (super.removeFooterView(v)) {
|
|
| 136 |
+ mFooterViews.remove(v); |
|
| 137 |
+ return true; |
|
| 138 |
+ } |
|
| 139 |
+ return false; |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ boolean containsFooterView(View v) {
|
|
| 143 |
+ if (mFooterViews == null) {
|
|
| 144 |
+ return false; |
|
| 145 |
+ } |
|
| 146 |
+ return mFooterViews.contains(v); |
|
| 147 |
+ } |
|
| 148 |
+ |
|
| 149 |
+ void setTopClippingLength(int topClipping) {
|
|
| 150 |
+ mTopClippingLength = topClipping; |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ int getFixedFirstVisibleItem() {
|
|
| 154 |
+ int firstVisibleItem = getFirstVisiblePosition(); |
|
| 155 |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
| 156 |
+ return firstVisibleItem; |
|
| 157 |
+ } |
|
| 158 |
+ |
|
| 159 |
+ // first getFirstVisiblePosition() reports items |
|
| 160 |
+ // outside the view sometimes on old versions of android |
|
| 161 |
+ for (int i = 0; i < getChildCount(); i++) {
|
|
| 162 |
+ if (getChildAt(i).getBottom() >= 0) {
|
|
| 163 |
+ firstVisibleItem += i; |
|
| 164 |
+ break; |
|
| 165 |
+ } |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 168 |
+ // work around to fix bug with firstVisibleItem being to high |
|
| 169 |
+ // because list view does not take clipToPadding=false into account |
|
| 170 |
+ // on old versions of android |
|
| 171 |
+ if (!mClippingToPadding && getPaddingTop() > 0 && firstVisibleItem > 0) {
|
|
| 172 |
+ if (getChildAt(0).getTop() > 0) {
|
|
| 173 |
+ firstVisibleItem -= 1; |
|
| 174 |
+ } |
|
| 175 |
+ } |
|
| 176 |
+ |
|
| 177 |
+ return firstVisibleItem; |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ @Override |
|
| 181 |
+ public void setClipToPadding(boolean clipToPadding) {
|
|
| 182 |
+ mClippingToPadding = clipToPadding; |
|
| 183 |
+ super.setClipToPadding(clipToPadding); |
|
| 184 |
+ } |
|
| 185 |
+ |
|
| 186 |
+ public void setBlockLayoutChildren(boolean block) {
|
|
| 187 |
+ mBlockLayoutChildren = block; |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ @Override |
|
| 191 |
+ protected void layoutChildren() {
|
|
| 192 |
+ if (!mBlockLayoutChildren) {
|
|
| 193 |
+ super.layoutChildren(); |
|
| 194 |
+ } |
|
| 195 |
+ } |
|
| 196 |
+} |
@@ -87,4 +87,38 @@ |
||
| 87 | 87 |
</declare-styleable> |
| 88 | 88 |
|
| 89 | 89 |
<attr name="SwipeBackLayoutStyle" format="reference"/> |
| 90 |
+ |
|
| 91 |
+ <declare-styleable name="StickyListHeadersListView"> |
|
| 92 |
+ <attr name="stickyListHeadersListViewStyle" format="reference"/> |
|
| 93 |
+ |
|
| 94 |
+ <!-- View attributes --> |
|
| 95 |
+ <attr name="android:clipToPadding" /> |
|
| 96 |
+ <attr name="android:scrollbars" /> |
|
| 97 |
+ <attr name="android:overScrollMode" /> |
|
| 98 |
+ <attr name="android:padding" /> |
|
| 99 |
+ <attr name="android:paddingLeft" /> |
|
| 100 |
+ <attr name="android:paddingTop" /> |
|
| 101 |
+ <attr name="android:paddingRight" /> |
|
| 102 |
+ <attr name="android:paddingBottom" /> |
|
| 103 |
+ |
|
| 104 |
+ <!-- ListView attributes --> |
|
| 105 |
+ <attr name="android:fadingEdgeLength" /> |
|
| 106 |
+ <attr name="android:requiresFadingEdge" /> |
|
| 107 |
+ <attr name="android:cacheColorHint" /> |
|
| 108 |
+ <attr name="android:choiceMode" /> |
|
| 109 |
+ <attr name="android:drawSelectorOnTop" /> |
|
| 110 |
+ <attr name="android:fastScrollEnabled" /> |
|
| 111 |
+ <attr name="android:fastScrollAlwaysVisible" /> |
|
| 112 |
+ <attr name="android:listSelector" /> |
|
| 113 |
+ <attr name="android:scrollingCache" /> |
|
| 114 |
+ <attr name="android:scrollbarStyle" /> |
|
| 115 |
+ <attr name="android:divider" /> |
|
| 116 |
+ <attr name="android:dividerHeight" /> |
|
| 117 |
+ <attr name="android:transcriptMode" /> |
|
| 118 |
+ <attr name="android:stackFromBottom" /> |
|
| 119 |
+ |
|
| 120 |
+ <!-- StickyListHeaders attributes --> |
|
| 121 |
+ <attr name="hasStickyHeaders" format="boolean" /> |
|
| 122 |
+ <attr name="isDrawingListUnderStickyHeader" format="boolean" /> |
|
| 123 |
+ </declare-styleable> |
|
| 90 | 124 |
</resources> |