├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── pinLetterListViewPreview.gif ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── ulex │ │ └── apps │ │ └── pinnedheaderletterlistview │ │ ├── lib │ │ ├── PinLetterBaseAdapter.java │ │ ├── PinnedHeaderLetterListView.java │ │ ├── entity │ │ │ ├── PinLetterBaseEntity.java │ │ │ └── PinLetterBaseItemEntity.java │ │ └── pinnedheaderlistview │ │ │ ├── PinnedHeaderListView.java │ │ │ └── SectionedBaseAdapter.java │ │ └── test │ │ ├── MainActivity.java │ │ ├── pin │ │ ├── ContactsEntity.java │ │ ├── ContactsItemEntity.java │ │ ├── ContactsListAdapter.java │ │ ├── ContactsModel.java │ │ └── PinnedHeaderActivity.java │ │ └── pinletter │ │ ├── ContactsPinLetterEntity.java │ │ ├── ContactsPinLetterItemEntity.java │ │ ├── ContactsPinLetterListAdapter.java │ │ ├── ContactsPinLetterModel.java │ │ └── PinLetterActivity.java │ └── res │ ├── anim │ ├── slide_left_in.xml │ ├── slide_left_out.xml │ ├── slide_right_in.xml │ └── slide_right_out.xml │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_main.xml │ ├── activity_pin_letter.xml │ ├── activity_pinned_header.xml │ ├── item_contacts_head_view.xml │ └── item_contacts_view.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── pin_letter_att.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PinnedHeaderLetterListView 2 | PinnedHeaderLetterListView提供一个类似通讯录列表的组件,具备两个主要功能点,1、滑动时首字母会定在顶端,2、右侧有个字母列表,滑动这个列表时,显示选中的字母,同时将列表定位到相应的位置上 3 | 4 | ##效果图 5 | ![](https://github.com/ulexzhong/PinnedHeaderLetterListView/raw/master/app/pinLetterListViewPreview.gif) 6 | 7 | ## 项目简介 8 | **lib**包为最终提供的公共库
9 | **test**包为测试用例
10 | 其中lib里面引用了一个开源库相关代码[PinnedHeaderListView](https://github.com/JimiSmith/PinnedHeaderListView "PinnedHeaderListView"),这个开源库已经实现将列表title定在顶端的功能,具体介绍可前往查阅 11 | 12 | ## 主要类介绍 13 | ### PinnedHeaderLetterListView.java 14 | [PinnedHeaderLetterListView.java](https://github.com/ulexzhong/PinnedHeaderLetterListView/blob/master/app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/PinnedHeaderLetterListView.java)为最终提供的组件,继承了[PinnedHeaderListView](https://github.com/ulexzhong/PinnedHeaderLetterListView/tree/master/app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/pinnedheaderlistview),本质上就是一个ListView,只不过是在ListView的基础上添加了一下操作,在具体使用的时候,跟ListView基本一致
15 | ```java 16 | private void initAttrs(AttributeSet attrs) { 17 | TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.pin_letter_list_view); 18 | mAlphaTextSize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaTextSize, 16); 19 | mAlphaTextColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaTextColor, Color.BLACK); 20 | mAlphaBackgroudNormalColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaBackgroundNormalColor, Color.TRANSPARENT); 21 | mAlphaBackgroudPressedColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaBackgroundPressedColor, Color.GRAY); 22 | mAlphaBackgroudColor = mAlphaBackgroudNormalColor; 23 | mAlphaPaddingLeft = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingLeft, 0); 24 | mAlphaPaddingRight = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingRight, 0); 25 | mAlphaPaddingTop = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingTop, 5); 26 | mAlphaPaddingBottom = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingBottom, 5); 27 | String alphaStr = typedArray.getString(R.styleable.pin_letter_list_view_alphaArrays); 28 | initAlphaList(alphaStr); 29 | 30 | mOverlayTextColor = typedArray.getColor(R.styleable.pin_letter_list_view_overlayTextColor, Color.WHITE); 31 | mOverlayTextSize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlayTextSize, 40); 32 | mOverlaySize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlaySize, 80); 33 | mOverlayColor = typedArray.getColor(R.styleable.pin_letter_list_view_overlayColor, Color.BLACK); 34 | mOverlayRadius = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlayRadius, 10); 35 | 36 | typedArray.recycle(); 37 | 38 | } 39 | ``` 40 | 提供了一些可定制项,例如,选中后提示的字体大小、颜色、弹框的大小、颜色、右侧字母表的相关的定制,对于字母表的数据,用户可直接用alphaArrays指定,缺省值为Adapter里面获取 41 | ### PinLetterBaseAdapter.java 42 | 继承了[SectionedBaseAdapter](https://github.com/ulexzhong/PinnedHeaderLetterListView/blob/master/app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/pinnedheaderlistview/SectionedBaseAdapter.java),扩展了两个方法,用于获取用户滑动字母表时对应的字母在ListView中的postion,使用的时候不需要理会,只要继承这个PinLetterBaseAdapter实现自己的Adapter就可以了 43 | ```java 44 | /** 45 | * for locate the position in the whole listview, 46 | * this index doesn't include with headViewCount 47 | * 48 | * @param section 49 | * @return 50 | */ 51 | public int getWholePosition(int section) { 52 | if (section <= 0) { 53 | return section; 54 | } 55 | int index = 0; 56 | for (int i = 0; i < section; i++) { 57 | index += getCountForSection(i) + 1; 58 | } 59 | return index; 60 | } 61 | 62 | public int getWholePosition(String letter) { 63 | if (TextUtils.isEmpty(letter)) { 64 | return -1; 65 | } 66 | int section = -1; 67 | for (int i = 0, size = list.size(); i < size; i++) { 68 | if (letter.equals(list.get(i).getLetter())) { 69 | section = i; 70 | break; 71 | } 72 | } 73 | return getWholePosition(section); 74 | } 75 | ``` 76 | ### SectionBaseAdapter.java 77 | 这个类是属于PinnedHeaderListView开源项目的,在BaseAdapter的基础上进行了扩展,将顶部和具体内容的itemView分离,让用户自行去实现相应的View 78 | ### entity 79 | 这里对放进ListView的数据结构做了一些约束
80 | #### PinLetterBaseEntity.java 81 | 所有的item的最外层数据继承该类 82 | ####PinLetterBaseItemEntity.java 83 | 所有letter里面的子item数据继承该类 84 | 85 | ## 使用方式 86 | #### 在values.xml中新建att.xml 87 | ```java 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ``` 108 | #### 在layout.xml文件中 109 | ```java 110 | 115 | 116 | 125 | 126 | 127 | ``` 128 | #### 编写实体类 129 | 继承PinLetterBaseEntity.java、PinLetterBaseItemEntity.java实现相应的实体类 130 | 131 | #### 继承[PinLetterBaseAdapter](https://github.com/ulexzhong/PinnedHeaderLetterListView/blob/master/app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/PinLetterBaseAdapter.java)实现Adapter类 132 | ```java 133 | public class ContactsPinLetterListAdapter extends PinLetterBaseAdapter { 134 | 135 | public ContactsPinLetterListAdapter(List list) { 136 | super(list); 137 | } 138 | 139 | @Override 140 | public Object getItem(int section, int position) { 141 | return null; 142 | } 143 | 144 | @Override 145 | public long getItemId(int section, int position) { 146 | return 0; 147 | } 148 | 149 | @Override 150 | public int getSectionCount() { 151 | return list.size(); 152 | } 153 | 154 | @Override 155 | public int getCountForSection(int section) { 156 | return list.get(section).getItemEntityList().size(); 157 | } 158 | 159 | @Override 160 | public View getItemView(int section, int position, View convertView, ViewGroup parent) { 161 | ItemViewHolder viewHolder; 162 | if (convertView == null) { 163 | viewHolder = new ItemViewHolder(parent); 164 | convertView = viewHolder.getView(); 165 | convertView.setTag(viewHolder); 166 | } else { 167 | viewHolder = (ItemViewHolder) convertView.getTag(); 168 | } 169 | ContactsPinLetterItemEntity entity = (ContactsPinLetterItemEntity) list.get(section).getItemEntityList().get(position); 170 | viewHolder.render(entity.getName()); 171 | return convertView; 172 | } 173 | 174 | @Override 175 | public View getSectionHeaderView(int section, View convertView, ViewGroup parent) { 176 | HeaderViewHolder viewHolder; 177 | if (convertView == null) { 178 | viewHolder = new HeaderViewHolder(parent); 179 | convertView = viewHolder.getView(); 180 | convertView.setTag(viewHolder); 181 | } else { 182 | viewHolder = (HeaderViewHolder) convertView.getTag(); 183 | } 184 | ContactsPinLetterEntity entity = (ContactsPinLetterEntity) list.get(section); 185 | viewHolder.render(entity.getLetter()); 186 | return convertView; 187 | } 188 | ``` 189 | #### 最后 190 | ```java 191 | List list = ContactsPinLetterModel.getContactsList(); 192 | ContactsPinLetterListAdapter listAdapter = new ContactsPinLetterListAdapter(list); 193 | TextView textView = new TextView(this); 194 | textView.setText("This is a headerView..."); 195 | AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200); 196 | textView.setLayoutParams(params); 197 | listView.addHeaderView(textView); 198 | listView.setAdapter(listAdapter); 199 | ``` 200 | 201 | 202 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.ulex.apps.pinnedheaderletterlistview" 9 | minSdkVersion 9 10 | targetSdkVersion 21 11 | versionCode 10000 12 | versionName "1.0.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | } 25 | -------------------------------------------------------------------------------- /app/pinLetterListViewPreview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulexzhong/PinnedHeaderLetterListView/71118ed6360957c58dfac856ae31d359c68c9ffc/app/pinLetterListViewPreview.gif -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\AndroidTools\adt-bundle-windows-x86-20130917\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/PinLetterBaseAdapter.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib; 2 | 3 | 4 | import android.text.TextUtils; 5 | 6 | import com.ulex.apps.pinnedheaderletterlistview.lib.entity.PinLetterBaseEntity; 7 | import com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview.SectionedBaseAdapter; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Created by ulex on 2016/6/15. 13 | */ 14 | public abstract class PinLetterBaseAdapter extends SectionedBaseAdapter { 15 | protected List list; 16 | 17 | public PinLetterBaseAdapter(List list) { 18 | this.list = list; 19 | } 20 | 21 | /** 22 | * for locate the position in the whole listview, 23 | * this index doesn't include with headViewCount 24 | * 25 | * @param section 26 | * @return 27 | */ 28 | public int getWholePosition(int section) { 29 | if (section <= 0) { 30 | return section; 31 | } 32 | int index = 0; 33 | for (int i = 0; i < section; i++) { 34 | index += getCountForSection(i) + 1; 35 | } 36 | return index; 37 | } 38 | 39 | public int getWholePosition(String letter) { 40 | if (TextUtils.isEmpty(letter)) { 41 | return -1; 42 | } 43 | int section = -1; 44 | for (int i = 0, size = list.size(); i < size; i++) { 45 | if (letter.equals(list.get(i).getLetter())) { 46 | section = i; 47 | break; 48 | } 49 | } 50 | return getWholePosition(section); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/PinnedHeaderLetterListView.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.RectF; 10 | import android.os.Build; 11 | import android.text.TextPaint; 12 | import android.text.TextUtils; 13 | import android.util.AttributeSet; 14 | import android.util.TypedValue; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.widget.ListAdapter; 18 | 19 | import com.ulex.apps.pinnedheaderletterlistview.R; 20 | import com.ulex.apps.pinnedheaderletterlistview.lib.entity.PinLetterBaseEntity; 21 | import com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview.PinnedHeaderListView; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | 27 | /** 28 | * Created by ulex on 2016/6/15. 29 | */ 30 | public class PinnedHeaderLetterListView extends PinnedHeaderListView { 31 | private final static String TAG = PinnedHeaderLetterListView.class.getSimpleName(); 32 | 33 | private final static int ALPHA_PADDING = 5;// pixels 34 | private PinLetterBaseAdapter mAdapter; 35 | 36 | private TextPaint mAlphaTextPaint; 37 | private Paint mOverlayPaint; 38 | private boolean isShowOverlay = false; 39 | private boolean isAlphaSelecting = false; 40 | private float mAlphaInternal = 0f; 41 | private int mCurrentAlphaIndex = -1; 42 | 43 | private int mAlphaTextColor; 44 | private int mAlphaTextSize; 45 | private int mAlphaBackgroudColor; 46 | private int mAlphaBackgroudNormalColor; 47 | private int mAlphaBackgroudPressedColor; 48 | private int mAlphaPaddingLeft; 49 | private int mAlphaPaddingRight; 50 | private int mAlphaPaddingTop; 51 | private int mAlphaPaddingBottom; 52 | private List mAlphaList; 53 | 54 | private int mOverlayColor; 55 | private int mOverlaySize; 56 | private int mOverlayTextColor; 57 | private int mOverlayTextSize; 58 | private int mOverlayRadius; 59 | 60 | public PinnedHeaderLetterListView(Context context) { 61 | super(context); 62 | init(); 63 | } 64 | 65 | public PinnedHeaderLetterListView(Context context, AttributeSet attrs) { 66 | super(context, attrs); 67 | init(); 68 | initAttrs(attrs); 69 | } 70 | 71 | public PinnedHeaderLetterListView(Context context, AttributeSet attrs, int defStyle) { 72 | super(context, attrs, defStyle); 73 | init(); 74 | initAttrs(attrs); 75 | } 76 | 77 | private void init() { 78 | this.setVerticalScrollBarEnabled(false); 79 | if (Build.VERSION.SDK_INT >= 9) { 80 | this.setOverScrollMode(View.OVER_SCROLL_NEVER); 81 | } 82 | initPaint(); 83 | } 84 | 85 | private void initAttrs(AttributeSet attrs) { 86 | TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.pin_letter_list_view); 87 | mAlphaTextSize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaTextSize, 16); 88 | mAlphaTextColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaTextColor, Color.BLACK); 89 | mAlphaBackgroudNormalColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaBackgroundNormalColor, Color.TRANSPARENT); 90 | mAlphaBackgroudPressedColor = typedArray.getColor(R.styleable.pin_letter_list_view_alphaBackgroundPressedColor, Color.GRAY); 91 | mAlphaBackgroudColor = mAlphaBackgroudNormalColor; 92 | mAlphaPaddingLeft = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingLeft, 0); 93 | mAlphaPaddingRight = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingRight, 0); 94 | mAlphaPaddingTop = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingTop, 5); 95 | mAlphaPaddingBottom = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_alphaPaddingBottom, 5); 96 | String alphaStr = typedArray.getString(R.styleable.pin_letter_list_view_alphaArrays); 97 | initAlphaList(alphaStr); 98 | 99 | mOverlayTextColor = typedArray.getColor(R.styleable.pin_letter_list_view_overlayTextColor, Color.WHITE); 100 | mOverlayTextSize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlayTextSize, 40); 101 | mOverlaySize = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlaySize, 80); 102 | mOverlayColor = typedArray.getColor(R.styleable.pin_letter_list_view_overlayColor, Color.BLACK); 103 | mOverlayRadius = typedArray.getDimensionPixelSize(R.styleable.pin_letter_list_view_overlayRadius, 10); 104 | 105 | typedArray.recycle(); 106 | 107 | } 108 | 109 | private void initAlphaList(String alphaStr) { 110 | if (alphaStr == null) { 111 | return; 112 | } 113 | alphaStr = alphaStr.trim(); 114 | if (alphaStr.length() <= 0) { 115 | return; 116 | } 117 | mAlphaList = new ArrayList<>(alphaStr.length()); 118 | char[] alphaArray = alphaStr.toCharArray(); 119 | for (char c : alphaArray) { 120 | String s = String.valueOf(c); 121 | if (!mAlphaList.contains(s)) { 122 | mAlphaList.add(s); 123 | } 124 | } 125 | } 126 | 127 | private void initPaint() { 128 | if (mAlphaTextPaint == null) { 129 | mAlphaTextPaint = new TextPaint(); 130 | mAlphaTextPaint.setColor(Color.BLACK); 131 | mAlphaTextPaint.setTextAlign(Paint.Align.CENTER); 132 | mAlphaTextPaint.setAntiAlias(true); 133 | } 134 | 135 | if (mOverlayPaint == null) { 136 | mOverlayPaint = new Paint(); 137 | mOverlayPaint.setTextAlign(Paint.Align.CENTER); 138 | mOverlayPaint.setAntiAlias(true); 139 | mOverlayPaint.setStyle(Paint.Style.FILL); 140 | } 141 | 142 | } 143 | 144 | /** 145 | * this method has abandon, throw UnsupportedOperationException, please use 146 | * {@link #setAdapter(PinLetterBaseAdapter adapter)} 147 | * 148 | * @throws UnsupportedOperationException 149 | */ 150 | @Deprecated 151 | public void setAdapter(ListAdapter adapter) { 152 | throw new UnsupportedOperationException("please use ContactListAdapter"); 153 | } 154 | 155 | public void setAdapter(PinLetterBaseAdapter adapter) { 156 | mAdapter = adapter; 157 | super.setAdapter(adapter); 158 | initAlphaListFromAdapterDataIfNeed(); 159 | } 160 | 161 | private void initAlphaListFromAdapterDataIfNeed() { 162 | if (mAlphaList != null && mAlphaList.size() > 0) { 163 | return; 164 | } 165 | List tempList = (List) mAdapter.list; 166 | for (PinLetterBaseEntity entity : tempList) { 167 | if (entity != null && entity.getLetter() != null && !mAlphaList.contains(entity.getLetter())) { 168 | mAlphaList.add(entity.getLetter()); 169 | } 170 | } 171 | } 172 | 173 | @Override 174 | protected void onDraw(Canvas canvas) { 175 | super.onDraw(canvas); 176 | } 177 | 178 | @Override 179 | public void draw(Canvas canvas) { 180 | super.draw(canvas); 181 | drawAlpha(canvas); 182 | drawOverlay(canvas); 183 | } 184 | 185 | @Override 186 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 187 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 188 | if (isAlphaListEmpty()) { 189 | mAlphaInternal = 0; 190 | } else { 191 | mAlphaInternal = getAlphaHeight() / mAlphaList.size(); 192 | } 193 | } 194 | 195 | @Override 196 | public boolean onInterceptTouchEvent(MotionEvent ev) { 197 | return ev.getAction() == MotionEvent.ACTION_DOWN 198 | && isInAlpha(getWidth() - ev.getX()) 199 | || super.onInterceptTouchEvent(ev); 200 | } 201 | 202 | @Override 203 | public boolean onTouchEvent(MotionEvent ev) { 204 | if (isAlphaSelecting && ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) { 205 | isAlphaSelecting = false; 206 | onAlphaEnd(); 207 | return true; 208 | } 209 | 210 | float distanceToRight = getWidth() - ev.getX(); 211 | float distanctToTop = ev.getY(); 212 | if (distanctToTop >= 0 && distanctToTop <= getHeight()) { 213 | if (ev.getAction() == MotionEvent.ACTION_DOWN 214 | && isInAlpha(distanceToRight)) { 215 | isAlphaSelecting = true; 216 | onAlphaStart(getTouchAlphaIndex(distanctToTop)); 217 | return true; 218 | } else if (isAlphaSelecting 219 | && ev.getAction() == MotionEvent.ACTION_MOVE) { 220 | onAlphaSelected(getTouchAlphaIndex(distanctToTop)); 221 | return true; 222 | } 223 | } 224 | return super.onTouchEvent(ev); 225 | } 226 | 227 | private void onAlphaStart(int index) { 228 | mCurrentAlphaIndex = index; 229 | isShowOverlay = true; 230 | mAlphaBackgroudColor = mAlphaBackgroudPressedColor; 231 | scrollToPosition(index); 232 | invalidate(); 233 | } 234 | 235 | private void onAlphaSelected(int index) { 236 | if (mCurrentAlphaIndex >= 0 && mCurrentAlphaIndex == index) { 237 | //on same select 238 | return; 239 | } 240 | mCurrentAlphaIndex = index; 241 | isShowOverlay = true; 242 | mAlphaBackgroudColor = mAlphaBackgroudPressedColor; 243 | scrollToPosition(index); 244 | invalidate(); 245 | } 246 | 247 | private void onAlphaEnd() { 248 | mAlphaBackgroudColor = mAlphaBackgroudNormalColor; 249 | invalidate(); 250 | hideOverlay(); 251 | } 252 | 253 | private void scrollToPosition(int index) { 254 | int headerViewCount = getHeaderViewsCount(); 255 | String letter = mAlphaList.get(index); 256 | int wholePosition = mAdapter.getWholePosition(letter); 257 | setSelection(headerViewCount + wholePosition); 258 | } 259 | 260 | private boolean isInAlpha(float distanceToRight) { 261 | return mAlphaInternal > 0 && distanceToRight < (mAlphaInternal + mAlphaPaddingLeft + mAlphaPaddingRight); 262 | } 263 | 264 | private int getTouchAlphaIndex(float y) { 265 | if (isAlphaListEmpty()) { 266 | return 0; 267 | } 268 | y = y - ALPHA_PADDING - mAlphaPaddingTop; 269 | int index = ((int) (y / mAlphaInternal)); 270 | return index >= mAlphaList.size() ? mAlphaList.size() - 1 : (index <= 0 ? 0 : index); 271 | } 272 | 273 | private float getAlphaHeight() { 274 | return getHeight() - 2 * ALPHA_PADDING - mAlphaPaddingTop - mAlphaPaddingBottom; 275 | } 276 | 277 | private boolean isAlphaListEmpty() { 278 | if (mAlphaList == null || mAlphaList.size() <= 0) { 279 | return true; 280 | } 281 | return false; 282 | } 283 | 284 | private void drawAlpha(Canvas canvas) { 285 | if (isAlphaListEmpty()) { 286 | return; 287 | } 288 | drawAlphaBackgroud(canvas); 289 | 290 | float textSizePixel = translateTextSize(mAlphaTextSize); 291 | mAlphaTextPaint.setTextSize(mAlphaInternal > textSizePixel ? textSizePixel : mAlphaInternal); 292 | mAlphaTextPaint.setColor(mAlphaTextColor); 293 | 294 | canvas.save(); 295 | float yStart = mAlphaInternal + ALPHA_PADDING + mAlphaPaddingTop; 296 | float xStart = getMeasuredWidth() - (mAlphaInternal / 2) - mAlphaPaddingRight; 297 | for (int i = 0; i < mAlphaList.size(); i++) { 298 | String alpha = mAlphaList.get(i); 299 | canvas.drawText(alpha, xStart, yStart, mAlphaTextPaint); 300 | yStart = yStart + mAlphaInternal; 301 | } 302 | canvas.restore(); 303 | } 304 | 305 | private void drawOverlay(Canvas canvas) { 306 | if (!isShowOverlay || mCurrentAlphaIndex < 0) { 307 | return; 308 | } 309 | String alpha = mAlphaList.get(mCurrentAlphaIndex); 310 | if (TextUtils.isEmpty(alpha)) { 311 | return; 312 | } 313 | 314 | mOverlayPaint.setColor(mOverlayColor); 315 | 316 | float size = dp2px(getContext(), mOverlaySize); 317 | canvas.save(); 318 | float left = (getWidth() - size) / 2; 319 | float top = (getHeight() - size) / 2; 320 | float radius = dp2px(getContext(), mOverlayRadius); 321 | canvas.drawRoundRect(new RectF(left, top, left + size, top + size), radius, radius, mOverlayPaint); 322 | 323 | mOverlayPaint.setColor(mOverlayTextColor); 324 | 325 | float textSize = translateTextSize(mOverlayTextSize); 326 | mOverlayPaint.setTextSize(textSize); 327 | 328 | canvas.drawText(alpha, getWidth() / 2, getHeight() / 2 + textSize / 2 - 10, mOverlayPaint); 329 | 330 | canvas.restore(); 331 | 332 | } 333 | 334 | private void drawAlphaBackgroud(Canvas canvas) { 335 | mAlphaTextPaint.setColor(mAlphaBackgroudColor); 336 | canvas.save(); 337 | canvas.drawRect(getWidth() - mAlphaInternal - mAlphaPaddingLeft 338 | - mAlphaPaddingRight, 0, getWidth(), getHeight(), mAlphaTextPaint); 339 | canvas.restore(); 340 | } 341 | 342 | /** 343 | * @param textSize 344 | * @return translate from sp to pixel 345 | */ 346 | private float translateTextSize(int textSize) { 347 | Context c = getContext(); 348 | Resources r; 349 | 350 | if (c == null) 351 | r = Resources.getSystem(); 352 | else 353 | r = c.getResources(); 354 | 355 | return TypedValue.applyDimension( 356 | TypedValue.COMPLEX_UNIT_SP, textSize, r.getDisplayMetrics()); 357 | } 358 | 359 | private void hideOverlay() { 360 | postDelayed(delayHideOverlayRunnable, 500); 361 | } 362 | 363 | private Runnable delayHideOverlayRunnable = new Runnable() { 364 | @Override 365 | public void run() { 366 | if (isShowOverlay) { 367 | isShowOverlay = false; 368 | mCurrentAlphaIndex = -1; 369 | invalidate(); 370 | } 371 | } 372 | }; 373 | 374 | private int dp2px(Context context, float dpValue) { 375 | float scale = context.getResources().getDisplayMetrics().density; 376 | return (int) (dpValue * scale + 0.5F); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/entity/PinLetterBaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib.entity; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by ulex on 2016/6/15. 7 | */ 8 | public class PinLetterBaseEntity { 9 | protected String letter; 10 | protected List itemEntityList; 11 | 12 | public PinLetterBaseEntity(String letter, List itemEntityList) { 13 | this.letter = letter; 14 | this.itemEntityList = itemEntityList; 15 | } 16 | 17 | public String getLetter() { 18 | return letter; 19 | } 20 | 21 | public List getItemEntityList() { 22 | return itemEntityList; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/entity/PinLetterBaseItemEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib.entity; 2 | 3 | /** 4 | * Created by ulex on 2016/6/15. 5 | */ 6 | public class PinLetterBaseItemEntity { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/pinnedheaderlistview/PinnedHeaderListView.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.AbsListView; 9 | import android.widget.AbsListView.OnScrollListener; 10 | import android.widget.AdapterView; 11 | import android.widget.HeaderViewListAdapter; 12 | import android.widget.ListAdapter; 13 | import android.widget.ListView; 14 | 15 | public class PinnedHeaderListView extends ListView implements OnScrollListener { 16 | 17 | private OnScrollListener mOnScrollListener; 18 | 19 | public static interface PinnedSectionedHeaderAdapter { 20 | public boolean isSectionHeader(int position); 21 | 22 | public int getSectionForPosition(int position); 23 | 24 | public View getSectionHeaderView(int section, View convertView, ViewGroup parent); 25 | 26 | public int getSectionHeaderViewType(int section); 27 | 28 | public int getCount(); 29 | 30 | } 31 | 32 | private PinnedSectionedHeaderAdapter mAdapter; 33 | private View mCurrentHeader; 34 | private int mCurrentHeaderViewType = 0; 35 | private float mHeaderOffset; 36 | private boolean mShouldPin = true; 37 | private int mCurrentSection = 0; 38 | private int mWidthMode; 39 | private int mHeightMode; 40 | 41 | public PinnedHeaderListView(Context context) { 42 | super(context); 43 | super.setOnScrollListener(this); 44 | } 45 | 46 | public PinnedHeaderListView(Context context, AttributeSet attrs) { 47 | super(context, attrs); 48 | super.setOnScrollListener(this); 49 | } 50 | 51 | public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { 52 | super(context, attrs, defStyle); 53 | super.setOnScrollListener(this); 54 | } 55 | 56 | public void setPinHeaders(boolean shouldPin) { 57 | mShouldPin = shouldPin; 58 | } 59 | 60 | @Override 61 | public void setAdapter(ListAdapter adapter) { 62 | mCurrentHeader = null; 63 | mAdapter = (PinnedSectionedHeaderAdapter) adapter; 64 | super.setAdapter(adapter); 65 | } 66 | 67 | @Override 68 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 69 | if (mOnScrollListener != null) { 70 | mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); 71 | } 72 | 73 | if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < getHeaderViewsCount())) { 74 | mCurrentHeader = null; 75 | mHeaderOffset = 0.0f; 76 | for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) { 77 | View header = getChildAt(i); 78 | if (header != null) { 79 | header.setVisibility(VISIBLE); 80 | } 81 | } 82 | return; 83 | } 84 | 85 | firstVisibleItem -= getHeaderViewsCount(); 86 | 87 | int section = mAdapter.getSectionForPosition(firstVisibleItem); 88 | int viewType = mAdapter.getSectionHeaderViewType(section); 89 | mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader); 90 | ensurePinnedHeaderLayout(mCurrentHeader); 91 | mCurrentHeaderViewType = viewType; 92 | 93 | mHeaderOffset = 0.0f; 94 | 95 | for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) { 96 | if (mAdapter.isSectionHeader(i)) { 97 | View header = getChildAt(i - firstVisibleItem); 98 | float headerTop = header.getTop(); 99 | float pinnedHeaderHeight = mCurrentHeader.getMeasuredHeight(); 100 | header.setVisibility(VISIBLE); 101 | if (pinnedHeaderHeight >= headerTop && headerTop > 0) { 102 | mHeaderOffset = headerTop - header.getHeight(); 103 | } else if (headerTop <= 0) { 104 | header.setVisibility(INVISIBLE); 105 | } 106 | } 107 | } 108 | 109 | invalidate(); 110 | } 111 | 112 | @Override 113 | public void onScrollStateChanged(AbsListView view, int scrollState) { 114 | if (mOnScrollListener != null) { 115 | mOnScrollListener.onScrollStateChanged(view, scrollState); 116 | } 117 | } 118 | 119 | private View getSectionHeaderView(int section, View oldView) { 120 | boolean shouldLayout = section != mCurrentSection || oldView == null; 121 | 122 | View view = mAdapter.getSectionHeaderView(section, oldView, this); 123 | if (shouldLayout) { 124 | // a new section, thus a new header. We should lay it out again 125 | ensurePinnedHeaderLayout(view); 126 | mCurrentSection = section; 127 | } 128 | return view; 129 | } 130 | 131 | private void ensurePinnedHeaderLayout(View header) { 132 | if (header.isLayoutRequested()) { 133 | int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), mWidthMode); 134 | 135 | int heightSpec; 136 | ViewGroup.LayoutParams layoutParams = header.getLayoutParams(); 137 | if (layoutParams != null && layoutParams.height > 0) { 138 | heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); 139 | } else { 140 | heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 141 | } 142 | header.measure(widthSpec, heightSpec); 143 | header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight()); 144 | } 145 | } 146 | 147 | @Override 148 | protected void dispatchDraw(Canvas canvas) { 149 | super.dispatchDraw(canvas); 150 | if (mAdapter == null || !mShouldPin || mCurrentHeader == null) 151 | return; 152 | int saveCount = canvas.save(); 153 | canvas.translate(0, mHeaderOffset); 154 | canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight()); // needed 155 | // for 156 | // < 157 | // HONEYCOMB 158 | mCurrentHeader.draw(canvas); 159 | canvas.restoreToCount(saveCount); 160 | } 161 | 162 | @Override 163 | public void setOnScrollListener(OnScrollListener l) { 164 | mOnScrollListener = l; 165 | } 166 | 167 | @Override 168 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 169 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 170 | 171 | mWidthMode = MeasureSpec.getMode(widthMeasureSpec); 172 | mHeightMode = MeasureSpec.getMode(heightMeasureSpec); 173 | } 174 | 175 | public void setOnItemClickListener(OnItemClickListener listener) { 176 | super.setOnItemClickListener(listener); 177 | } 178 | 179 | public static abstract class OnItemClickListener implements AdapterView.OnItemClickListener { 180 | @Override 181 | public void onItemClick(AdapterView adapterView, View view, int rawPosition, long id) { 182 | SectionedBaseAdapter adapter; 183 | if (adapterView.getAdapter().getClass().equals(HeaderViewListAdapter.class)) { 184 | HeaderViewListAdapter wrapperAdapter = (HeaderViewListAdapter) adapterView.getAdapter(); 185 | adapter = (SectionedBaseAdapter) wrapperAdapter.getWrappedAdapter(); 186 | } else { 187 | adapter = (SectionedBaseAdapter) adapterView.getAdapter(); 188 | } 189 | int section = adapter.getSectionForPosition(rawPosition); 190 | int position = adapter.getPositionInSectionForPosition(rawPosition); 191 | 192 | if (position == -1) { 193 | onSectionClick(adapterView, view, section, id); 194 | } else { 195 | onItemClick(adapterView, view, section, position, id); 196 | } 197 | } 198 | 199 | public abstract void onItemClick(AdapterView adapterView, View view, int section, int position, long id); 200 | 201 | public abstract void onSectionClick(AdapterView adapterView, View view, int section, long id); 202 | 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/lib/pinnedheaderlistview/SectionedBaseAdapter.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview; 2 | 3 | import android.util.SparseArray; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.BaseAdapter; 7 | 8 | 9 | public abstract class SectionedBaseAdapter extends BaseAdapter implements PinnedHeaderListView.PinnedSectionedHeaderAdapter { 10 | 11 | private static int HEADER_VIEW_TYPE = 0; 12 | private static int ITEM_VIEW_TYPE = 0; 13 | 14 | /** 15 | * Holds the calculated values of @{link getPositionInSectionForPosition} 16 | */ 17 | private SparseArray mSectionPositionCache; 18 | /** 19 | * Holds the calculated values of @{link getSectionForPosition} 20 | */ 21 | private SparseArray mSectionCache; 22 | /** 23 | * Holds the calculated values of @{link getCountForSection} 24 | */ 25 | private SparseArray mSectionCountCache; 26 | 27 | /** 28 | * Caches the item count 29 | */ 30 | private int mCount; 31 | /** 32 | * Caches the section count 33 | */ 34 | private int mSectionCount; 35 | 36 | public SectionedBaseAdapter() { 37 | super(); 38 | mSectionCache = new SparseArray(); 39 | mSectionPositionCache = new SparseArray(); 40 | mSectionCountCache = new SparseArray(); 41 | mCount = -1; 42 | mSectionCount = -1; 43 | } 44 | 45 | @Override 46 | public void notifyDataSetChanged() { 47 | mSectionCache.clear(); 48 | mSectionPositionCache.clear(); 49 | mSectionCountCache.clear(); 50 | mCount = -1; 51 | mSectionCount = -1; 52 | super.notifyDataSetChanged(); 53 | } 54 | 55 | @Override 56 | public void notifyDataSetInvalidated() { 57 | mSectionCache.clear(); 58 | mSectionPositionCache.clear(); 59 | mSectionCountCache.clear(); 60 | mCount = -1; 61 | mSectionCount = -1; 62 | super.notifyDataSetInvalidated(); 63 | } 64 | 65 | @Override 66 | public final int getCount() { 67 | if (mCount >= 0) { 68 | return mCount; 69 | } 70 | int count = 0; 71 | for (int i = 0; i < internalGetSectionCount(); i++) { 72 | count += internalGetCountForSection(i); 73 | count++; // for the header view 74 | } 75 | mCount = count; 76 | return count; 77 | } 78 | 79 | @Override 80 | public final Object getItem(int position) { 81 | return getItem(getSectionForPosition(position), getPositionInSectionForPosition(position)); 82 | } 83 | 84 | @Override 85 | public final long getItemId(int position) { 86 | return getItemId(getSectionForPosition(position), getPositionInSectionForPosition(position)); 87 | } 88 | 89 | @Override 90 | public final View getView(int position, View convertView, ViewGroup parent) { 91 | if (isSectionHeader(position)) { 92 | return getSectionHeaderView(getSectionForPosition(position), convertView, parent); 93 | } 94 | return getItemView(getSectionForPosition(position), getPositionInSectionForPosition(position), convertView, parent); 95 | } 96 | 97 | @Override 98 | public final int getItemViewType(int position) { 99 | if (isSectionHeader(position)) { 100 | return getItemViewTypeCount() + getSectionHeaderViewType(getSectionForPosition(position)); 101 | } 102 | return getItemViewType(getSectionForPosition(position), getPositionInSectionForPosition(position)); 103 | } 104 | 105 | @Override 106 | public final int getViewTypeCount() { 107 | return getItemViewTypeCount() + getSectionHeaderViewTypeCount(); 108 | } 109 | 110 | public final int getSectionForPosition(int position) { 111 | // first try to retrieve values from cache 112 | Integer cachedSection = mSectionCache.get(position); 113 | if (cachedSection != null) { 114 | return cachedSection; 115 | } 116 | int sectionStart = 0; 117 | for (int i = 0; i < internalGetSectionCount(); i++) { 118 | int sectionCount = internalGetCountForSection(i); 119 | int sectionEnd = sectionStart + sectionCount + 1; 120 | if (position >= sectionStart && position < sectionEnd) { 121 | mSectionCache.put(position, i); 122 | return i; 123 | } 124 | sectionStart = sectionEnd; 125 | } 126 | return 0; 127 | } 128 | 129 | public int getPositionInSectionForPosition(int position) { 130 | // first try to retrieve values from cache 131 | Integer cachedPosition = mSectionPositionCache.get(position); 132 | if (cachedPosition != null) { 133 | return cachedPosition; 134 | } 135 | int sectionStart = 0; 136 | for (int i = 0; i < internalGetSectionCount(); i++) { 137 | int sectionCount = internalGetCountForSection(i); 138 | int sectionEnd = sectionStart + sectionCount + 1; 139 | if (position >= sectionStart && position < sectionEnd) { 140 | int positionInSection = position - sectionStart - 1; 141 | mSectionPositionCache.put(position, positionInSection); 142 | return positionInSection; 143 | } 144 | sectionStart = sectionEnd; 145 | } 146 | return 0; 147 | } 148 | 149 | public final boolean isSectionHeader(int position) { 150 | int sectionStart = 0; 151 | for (int i = 0; i < internalGetSectionCount(); i++) { 152 | if (position == sectionStart) { 153 | return true; 154 | } else if (position < sectionStart) { 155 | return false; 156 | } 157 | sectionStart += internalGetCountForSection(i) + 1; 158 | } 159 | return false; 160 | } 161 | 162 | public int getItemViewType(int section, int position) { 163 | return ITEM_VIEW_TYPE; 164 | } 165 | 166 | public int getItemViewTypeCount() { 167 | return 1; 168 | } 169 | 170 | public int getSectionHeaderViewType(int section) { 171 | return HEADER_VIEW_TYPE; 172 | } 173 | 174 | public int getSectionHeaderViewTypeCount() { 175 | return 1; 176 | } 177 | 178 | public abstract Object getItem(int section, int position); 179 | 180 | public abstract long getItemId(int section, int position); 181 | 182 | public abstract int getSectionCount(); 183 | 184 | public abstract int getCountForSection(int section); 185 | 186 | public abstract View getItemView(int section, int position, View convertView, ViewGroup parent); 187 | 188 | public abstract View getSectionHeaderView(int section, View convertView, ViewGroup parent); 189 | 190 | private int internalGetCountForSection(int section) { 191 | Integer cachedSectionCount = mSectionCountCache.get(section); 192 | if (cachedSectionCount != null) { 193 | return cachedSectionCount; 194 | } 195 | int sectionCount = getCountForSection(section); 196 | mSectionCountCache.put(section, sectionCount); 197 | return sectionCount; 198 | } 199 | 200 | private int internalGetSectionCount() { 201 | if (mSectionCount >= 0) { 202 | return mSectionCount; 203 | } 204 | mSectionCount = getSectionCount(); 205 | return mSectionCount; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import com.ulex.apps.pinnedheaderletterlistview.R; 8 | import com.ulex.apps.pinnedheaderletterlistview.test.pin.PinnedHeaderActivity; 9 | import com.ulex.apps.pinnedheaderletterlistview.test.pinletter.PinLetterActivity; 10 | 11 | public class MainActivity extends Activity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | initView(); 18 | } 19 | 20 | private void initView() { 21 | findViewById(R.id.pinned_btn).setOnClickListener(new View.OnClickListener() { 22 | @Override 23 | public void onClick(View v) { 24 | PinnedHeaderActivity.start(MainActivity.this); 25 | } 26 | }); 27 | findViewById(R.id.pin_letter_btn).setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View v) { 30 | PinLetterActivity.start(MainActivity.this); 31 | } 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pin/ContactsEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pin; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by ulex on 2016/6/15. 7 | */ 8 | public class ContactsEntity { 9 | private String title; 10 | private List contactsItemEntityList; 11 | 12 | public ContactsEntity(String title, List contactsItemEntityList) { 13 | this.title = title; 14 | this.contactsItemEntityList = contactsItemEntityList; 15 | } 16 | 17 | public String getTitle() { 18 | return title; 19 | } 20 | 21 | public List getContactsItemEntityList() { 22 | return contactsItemEntityList; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pin/ContactsItemEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pin; 2 | 3 | /** 4 | * Created by ulex on 2016/6/15. 5 | */ 6 | public class ContactsItemEntity { 7 | private String name; 8 | 9 | public ContactsItemEntity(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pin/ContactsListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pin; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | 8 | import com.ulex.apps.pinnedheaderletterlistview.R; 9 | import com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview.SectionedBaseAdapter; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Created by ulex on 2016/6/15. 15 | */ 16 | public class ContactsListAdapter extends SectionedBaseAdapter { 17 | private List list; 18 | 19 | public ContactsListAdapter(List list) { 20 | this.list = list; 21 | } 22 | 23 | @Override 24 | public Object getItem(int section, int position) { 25 | return null; 26 | } 27 | 28 | @Override 29 | public long getItemId(int section, int position) { 30 | return 0; 31 | } 32 | 33 | @Override 34 | public int getSectionCount() { 35 | return list.size(); 36 | } 37 | 38 | @Override 39 | public int getCountForSection(int section) { 40 | return list.get(section).getContactsItemEntityList().size(); 41 | } 42 | 43 | @Override 44 | public View getItemView(int section, int position, View convertView, ViewGroup parent) { 45 | ItemViewHolder viewHolder; 46 | if (convertView == null) { 47 | viewHolder = new ItemViewHolder(parent); 48 | convertView = viewHolder.getView(); 49 | convertView.setTag(viewHolder); 50 | } else { 51 | viewHolder = (ItemViewHolder) convertView.getTag(); 52 | } 53 | ContactsItemEntity entity = list.get(section).getContactsItemEntityList().get(position); 54 | viewHolder.render(entity.getName()); 55 | return convertView; 56 | } 57 | 58 | @Override 59 | public View getSectionHeaderView(int section, View convertView, ViewGroup parent) { 60 | HeaderViewHolder viewHolder; 61 | if (convertView == null) { 62 | viewHolder = new HeaderViewHolder(parent); 63 | convertView = viewHolder.getView(); 64 | convertView.setTag(viewHolder); 65 | } else { 66 | viewHolder = (HeaderViewHolder) convertView.getTag(); 67 | } 68 | ContactsEntity entity = list.get(section); 69 | viewHolder.render(entity.getTitle()); 70 | return convertView; 71 | } 72 | 73 | private static class ItemViewHolder { 74 | private TextView contentTxt; 75 | private View view; 76 | 77 | public ItemViewHolder(ViewGroup parent) { 78 | view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contacts_view, parent, false); 79 | initView(); 80 | } 81 | 82 | public View getView() { 83 | return view; 84 | } 85 | 86 | private void initView() { 87 | contentTxt = (TextView) view.findViewById(R.id.item_contacts_content_txt); 88 | } 89 | 90 | public void render(String content) { 91 | contentTxt.setText(content); 92 | } 93 | } 94 | 95 | private static class HeaderViewHolder { 96 | private TextView titleTxt; 97 | private View view; 98 | 99 | public HeaderViewHolder(ViewGroup parent) { 100 | view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contacts_head_view, parent, false); 101 | initView(); 102 | } 103 | 104 | public View getView() { 105 | return view; 106 | } 107 | 108 | private void initView() { 109 | titleTxt = (TextView) view.findViewById(R.id.item_contacts_head_txt); 110 | } 111 | 112 | public void render(String title) { 113 | titleTxt.setText(title); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pin/ContactsModel.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pin; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by ulex on 2016/6/15. 8 | */ 9 | public class ContactsModel { 10 | 11 | public static List getContactsList() { 12 | List contactsList = new ArrayList<>(); 13 | for (int i = 0; i < 26; i++) { 14 | contactsList.add(getContactEntity(String.valueOf((char) ('A' + i)))); 15 | } 16 | return contactsList; 17 | } 18 | 19 | private static ContactsEntity getContactEntity(String title) { 20 | ArrayList itemList = new ArrayList<>(); 21 | for (int i = 0; i < 10; i++) { 22 | itemList.add(new ContactsItemEntity(title + i)); 23 | } 24 | return new ContactsEntity(title, itemList); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pin/PinnedHeaderActivity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pin; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | 8 | import com.ulex.apps.pinnedheaderletterlistview.R; 9 | import com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview.PinnedHeaderListView; 10 | 11 | import java.util.List; 12 | 13 | public class PinnedHeaderActivity extends Activity { 14 | private PinnedHeaderListView listView; 15 | 16 | public static void start(Context context) { 17 | Intent starter = new Intent(context, PinnedHeaderActivity.class); 18 | context.startActivity(starter); 19 | } 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_pinned_header); 25 | initView(); 26 | initData(); 27 | } 28 | 29 | private void initView() { 30 | listView = (PinnedHeaderListView) findViewById(R.id.listView); 31 | } 32 | 33 | private void initData() { 34 | List list = ContactsModel.getContactsList(); 35 | ContactsListAdapter listAdapter = new ContactsListAdapter(list); 36 | listView.setAdapter(listAdapter); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pinletter/ContactsPinLetterEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pinletter; 2 | 3 | import com.ulex.apps.pinnedheaderletterlistview.lib.entity.PinLetterBaseEntity; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by ulex on 2016/6/15. 9 | */ 10 | public class ContactsPinLetterEntity extends PinLetterBaseEntity{ 11 | 12 | public ContactsPinLetterEntity(String title, List itemEntityList) { 13 | super(title, itemEntityList); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pinletter/ContactsPinLetterItemEntity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pinletter; 2 | 3 | 4 | import com.ulex.apps.pinnedheaderletterlistview.lib.entity.PinLetterBaseItemEntity; 5 | 6 | /** 7 | * Created by ulex on 2016/6/15. 8 | */ 9 | public class ContactsPinLetterItemEntity extends PinLetterBaseItemEntity { 10 | private String name; 11 | 12 | public ContactsPinLetterItemEntity(String name) { 13 | this.name = name; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pinletter/ContactsPinLetterListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pinletter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | 8 | import com.ulex.apps.pinnedheaderletterlistview.R; 9 | import com.ulex.apps.pinnedheaderletterlistview.lib.PinLetterBaseAdapter; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Created by ulex on 2016/6/15. 15 | */ 16 | public class ContactsPinLetterListAdapter extends PinLetterBaseAdapter { 17 | 18 | public ContactsPinLetterListAdapter(List list) { 19 | super(list); 20 | } 21 | 22 | @Override 23 | public Object getItem(int section, int position) { 24 | return null; 25 | } 26 | 27 | @Override 28 | public long getItemId(int section, int position) { 29 | return 0; 30 | } 31 | 32 | @Override 33 | public int getSectionCount() { 34 | return list.size(); 35 | } 36 | 37 | @Override 38 | public int getCountForSection(int section) { 39 | return list.get(section).getItemEntityList().size(); 40 | } 41 | 42 | @Override 43 | public View getItemView(int section, int position, View convertView, ViewGroup parent) { 44 | ItemViewHolder viewHolder; 45 | if (convertView == null) { 46 | viewHolder = new ItemViewHolder(parent); 47 | convertView = viewHolder.getView(); 48 | convertView.setTag(viewHolder); 49 | } else { 50 | viewHolder = (ItemViewHolder) convertView.getTag(); 51 | } 52 | ContactsPinLetterItemEntity entity = (ContactsPinLetterItemEntity) list.get(section).getItemEntityList().get(position); 53 | viewHolder.render(entity.getName()); 54 | return convertView; 55 | } 56 | 57 | @Override 58 | public View getSectionHeaderView(int section, View convertView, ViewGroup parent) { 59 | HeaderViewHolder viewHolder; 60 | if (convertView == null) { 61 | viewHolder = new HeaderViewHolder(parent); 62 | convertView = viewHolder.getView(); 63 | convertView.setTag(viewHolder); 64 | } else { 65 | viewHolder = (HeaderViewHolder) convertView.getTag(); 66 | } 67 | ContactsPinLetterEntity entity = (ContactsPinLetterEntity) list.get(section); 68 | viewHolder.render(entity.getLetter()); 69 | return convertView; 70 | } 71 | 72 | private static class ItemViewHolder { 73 | private TextView contentTxt; 74 | private View view; 75 | 76 | public ItemViewHolder(ViewGroup parent) { 77 | view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contacts_view, parent, false); 78 | initView(); 79 | } 80 | 81 | public View getView() { 82 | return view; 83 | } 84 | 85 | private void initView() { 86 | contentTxt = (TextView) view.findViewById(R.id.item_contacts_content_txt); 87 | } 88 | 89 | public void render(String content) { 90 | contentTxt.setText(content); 91 | } 92 | } 93 | 94 | private static class HeaderViewHolder { 95 | private TextView titleTxt; 96 | private View view; 97 | 98 | public HeaderViewHolder(ViewGroup parent) { 99 | view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contacts_head_view, parent, false); 100 | initView(); 101 | } 102 | 103 | public View getView() { 104 | return view; 105 | } 106 | 107 | private void initView() { 108 | titleTxt = (TextView) view.findViewById(R.id.item_contacts_head_txt); 109 | } 110 | 111 | public void render(String title) { 112 | titleTxt.setText(title); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pinletter/ContactsPinLetterModel.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pinletter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by ulex on 2016/6/15. 8 | */ 9 | public class ContactsPinLetterModel { 10 | 11 | public static List getContactsList() { 12 | List contactsList = new ArrayList<>(); 13 | for (int i = 0; i < 26; i++) { 14 | contactsList.add(getContactEntity(String.valueOf((char) ('A' + i)))); 15 | } 16 | return contactsList; 17 | } 18 | 19 | private static ContactsPinLetterEntity getContactEntity(String title) { 20 | ArrayList itemList = new ArrayList<>(); 21 | for (int i = 0; i < 10; i++) { 22 | itemList.add(new ContactsPinLetterItemEntity(title + i)); 23 | } 24 | return new ContactsPinLetterEntity(title, itemList); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/ulex/apps/pinnedheaderletterlistview/test/pinletter/PinLetterActivity.java: -------------------------------------------------------------------------------- 1 | package com.ulex.apps.pinnedheaderletterlistview.test.pinletter; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.AbsListView; 10 | import android.widget.AdapterView; 11 | import android.widget.TextView; 12 | 13 | import com.ulex.apps.pinnedheaderletterlistview.R; 14 | import com.ulex.apps.pinnedheaderletterlistview.lib.PinnedHeaderLetterListView; 15 | import com.ulex.apps.pinnedheaderletterlistview.lib.pinnedheaderlistview.PinnedHeaderListView; 16 | 17 | import java.util.List; 18 | 19 | public class PinLetterActivity extends Activity { 20 | private PinnedHeaderLetterListView listView; 21 | 22 | public static void start(Context context) { 23 | Intent starter = new Intent(context, PinLetterActivity.class); 24 | context.startActivity(starter); 25 | } 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_pin_letter); 31 | initView(); 32 | initData(); 33 | } 34 | 35 | private void initView() { 36 | listView = (PinnedHeaderLetterListView) findViewById(R.id.pinned_indicated_listView); 37 | } 38 | 39 | private void initData() { 40 | List list = ContactsPinLetterModel.getContactsList(); 41 | ContactsPinLetterListAdapter listAdapter = new ContactsPinLetterListAdapter(list); 42 | TextView textView = new TextView(this); 43 | textView.setText("This is a headerView..."); 44 | AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200); 45 | textView.setLayoutParams(params); 46 | listView.addHeaderView(textView); 47 | listView.setAdapter(listAdapter); 48 | listView.setOnItemClickListener(new PinnedHeaderListView.OnItemClickListener() { 49 | @Override 50 | public void onItemClick(AdapterView adapterView, View view, int section, int position, long id) { 51 | 52 | } 53 | 54 | @Override 55 | public void onSectionClick(AdapterView adapterView, View view, int section, long id) { 56 | 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_left_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_left_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_right_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulexzhong/PinnedHeaderLetterListView/71118ed6360957c58dfac856ae31d359c68c9ffc/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulexzhong/PinnedHeaderLetterListView/71118ed6360957c58dfac856ae31d359c68c9ffc/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulexzhong/PinnedHeaderLetterListView/71118ed6360957c58dfac856ae31d359c68c9ffc/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulexzhong/PinnedHeaderLetterListView/71118ed6360957c58dfac856ae31d359c68c9ffc/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |