137
+        @BindView(R.id.tv_session_seq) TextView sessionSeq;
138 138
 
139 139
         @BindView(R.id.tv_session_upload_status) TextView uploadStatus;
140 140
 

+ 30 - 0
app/src/main/java/ai/pai/lensman/upload/UploadActivity.java

@@ -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
+}

+ 7 - 0
app/src/main/java/ai/pai/lensman/utils/BoxUrlContainer.java

@@ -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
 

+ 3 - 0
app/src/main/java/ai/pai/lensman/utils/UrlContainer.java

@@ -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
 }

+ 1 - 0
app/src/main/res/layout/activity_main.xml

@@ -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"

+ 68 - 0
app/src/main/res/layout/activity_upload.xml

@@ -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>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -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>

+ 225 - 0
views/src/main/java/com/android/views/stickylistheaders/AdapterWrapper.java

@@ -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
+}

+ 31 - 0
views/src/main/java/com/android/views/stickylistheaders/CheckableWrapperView.java

@@ -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
+}

+ 147 - 0
views/src/main/java/com/android/views/stickylistheaders/DistinctMultiHashMap.java

@@ -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
+}

+ 39 - 0
views/src/main/java/com/android/views/stickylistheaders/DualHashMap.java

@@ -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
+}

+ 131 - 0
views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersAdapter.java

@@ -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
+}

+ 126 - 0
views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersListView.java

@@ -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
+}

+ 32 - 0
views/src/main/java/com/android/views/stickylistheaders/SectionIndexerAdapterWrapper.java

@@ -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
+}

+ 38 - 0
views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersAdapter.java

@@ -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
+}

+ 1131 - 0
views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersListView.java

@@ -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
+}

+ 156 - 0
views/src/main/java/com/android/views/stickylistheaders/WrapperView.java

@@ -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
+}

+ 196 - 0
views/src/main/java/com/android/views/stickylistheaders/WrapperViewList.java

@@ -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
+}

+ 34 - 0
views/src/main/res/values/attrs.xml

@@ -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>

adminSystem - Gogs: Go Git Service

Няма описание

FFIB: 11e3a9652a first преди 8 години
..
dist 11e3a9652a first преди 8 години
node_modules 11e3a9652a first преди 8 години
CHANGELOG.md 11e3a9652a first преди 8 години
LICENSE 11e3a9652a first преди 8 години
README.md 11e3a9652a first преди 8 години
package.json 11e3a9652a first преди 8 години

README.md

npm node deps tests coverage chat

File Loader

Instructs webpack to emit the required object as file and to return its public URL

Install

npm install --save-dev file-loader

Usage

By default the filename of the resulting file is the MD5 hash of the file's contents with the original extension of the required resource.

import img from './file.png'

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {}  
          }
        ]
      }
    ]
  }
}

Emits file.png as file in the output directory and returns the public URL

"/public/path/0dcbbaa7013869e351f.png"

Options

Name Type Default Description
name {String\|Function} [hash].[ext] Configure a custom filename template for your file
regExp {RegExp} 'undefined' Let you extract some parts of the file path to reuse them in the name property
context {String} this.options.context Configure a custom file context, defaults to webpack.config.js context
publicPath {String\|Function} __webpack_public_path__ Configure a custom public path for your file
outputPath {String\|Function} 'undefined' Configure a custom output path for your file
useRelativePath {Boolean} false Should be true if you wish to generate a context relative URL for each file
emitFile {Boolean} true By default a file is emitted, however this can be disabled if required (e.g. for server side packages)

name

You can configure a custom filename template for your file using the query parameter name. For instance, to copy a file from your context directory into the output directory retaining the full directory structure, you might use

{String}

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[path][name].[ext]'
  }  
}

{Function}

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name (file) {
      if (env === 'development') {
        return '[path][name].[ext]'
      }

      return '[hash].[ext]'
    }
  }  
}

regExp

Defines a regExp to match some parts of the file path. These capture groups can be reused in the name property using [N] placeholder. Note that [0] will be replaced by the entire tested string, whereas [1] will contain the first capturing parenthesis of your regex and so on...

import img from './customer01/file.png'

webpack.config.js

{
  loader: 'file-loader',
  options: {
    regExp: /\/([a-z0-9]+)\/[a-z0-9]+\.png$/,
    name: '[1]-[name].[ext]'
  }  
}
customer01-file.png

placeholders

Name Type Default Description
[ext] {String} file.extname The extension of the resource
[name] {String} file.basename The basename of the resource
[path] {String} file.dirname The path of the resource relative to the context
[hash] {String} md5 The hash of the content, hashes below for more info
[N] {String} `` The n-th match obtained from matching the current file name against the regExp

hashes

[<hashType>:hash:<digestType>:<length>] optionally you can configure

Name Type Default Description
hashType {String} md5 sha1, md5, sha256, sha512
digestType {String} hex hex, base26, base32, base36, base49, base52, base58, base62, base64
length {Number} 9999 The length in chars

By default, the path and name you specify will output the file in that same directory and will also use that same URL path to access the file.

context

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[path][name].[ext]',
    context: ''
  }  
}

You can specify custom output and public paths by using outputPath, publicPath and useRelativePath

publicPath

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[path][name].[ext]',
    publicPath: 'assets/'
  }  
}

outputPath

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[path][name].[ext]',
    outputPath: 'images/'
  }  
}

useRelativePath

useRelativePath should be true if you wish to generate a relative URL to the for each file context.

{
  loader: 'file-loader',
  options: {
    useRelativePath: process.env.NODE_ENV === "production"
  }
}

emitFile

By default a file is emitted, however this can be disabled if required (e.g. for server side packages).

import img from './file.png'
{
  loader: 'file-loader',
  options: {
    emitFile: false
  }  
}

⚠️ Returns the public URL but does not emit a file

`${publicPath}/0dcbbaa701328e351f.png`

Examples

import png from 'image.png'

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: 'dirname/[hash].[ext]'
  }  
}
dirname/0dcbbaa701328ae351f.png

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[sha512:hash:base64:7].[ext]'
  }  
}
gdyb21L.png
import png from 'path/to/file.png'

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[path][name].[ext]?[hash]'
  }  
}
path/to/file.png?e43b20c069c4a01867c31e98cbce33c9

Maintainers


Juho Vepsäläinen

Joshua Wiens

Michael Ciniawsky

Alexander Krasnoyarov