你的位置:首页 > 操作系统

[操作系统]联系人分组标签悬停滑入滑出的实现方法。


《类似通讯录分组的Android PinnedSectionListView,分组标签悬停滑入滑出》

常用的联系人、通讯录,会按照联系人的姓氏从A,B,C,,,X,Y,Z,这样归类排列下去,方便用户快速查找和定位。PinnedSectionListView是一个第三方的开源框架,在github上的链接地址是:https://github.com/beworker/pinned-section-listview 。分组的标签会悬停在ListView的顶部,直到该分组被滑出/滑入整个ListView的可视界面而呈现出弹入弹出效果。

 

将PinnedSectionListView类放入自己的项目中:

PinnedSectionListView类代码:

 1 package com.lixu.biaoqianxuanting; 2 /* 3  * Copyright (C) 2013 Sergej Shafarenka, halfbit.de 4  * 5  * Licensed under the Apache License, Version 2.0 (the "License"); 6  * you may not use this file kt in compliance with the License. 7  * You may obtain a copy of the License at 8  * 9  * http://www.apache.org/licenses/LICENSE-2.0 10  * 11  * Unless required by applicable law or agreed to in writing, software 12  * distributed under the License is distributed on an "AS IS" BASIS, 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14  * See the License for the specific language governing permissions and 15  * limitations under the License. 16 */ 17  18 import android.content.Context; 19 import android.database.DataSetObserver; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.PointF; 23 import android.graphics.Rect; 24 import android.graphics.drawable.GradientDrawable; 25 import android.graphics.drawable.GradientDrawable.Orientation; 26 import android.os.Parcelable; 27 import android.util.AttributeSet; 28 import android.view.MotionEvent; 29 import android.view.SoundEffectConstants; 30 import android.view.View; 31 import android.view.ViewConfiguration; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.widget.AbsListView; 34 import android.widget.HeaderViewListAdapter; 35 import android.widget.ListAdapter; 36 import android.widget.ListView; 37 import android.widget.SectionIndexer; 38  39  40 /** 41  * ListView, which is capable to pin section views at its top while the rest is still scrolled. 42 */ 43 public class PinnedSectionListView extends ListView { 44  45   //-- inner classes 46  47   /** List adapter to be implemented for being used with PinnedSectionListView adapter. */ 48   public static interface PinnedSectionListAdapter extends ListAdapter { 49     /** This method shall return 'true' if views of given type has to be pinned. */ 50     boolean isItemViewTypePinned(int viewType); 51   } 52  53   /** Wrapper class for pinned section view and its position in the list. */ 54   static class PinnedSection { 55     public View view; 56     public int position; 57     public long id; 58   } 59  60   //-- class fields 61  62   // fields used for handling touch events 63   private final Rect mTouchRect = new Rect(); 64   private final PointF mTouchPoint = new PointF(); 65   private int mTouchSlop; 66   private View mTouchTarget; 67   private MotionEvent mDownEvent; 68  69   // fields used for drawing shadow under a pinned section 70   private GradientDrawable mShadowDrawable; 71   private int mSectionsDistanceY; 72   private int mShadowHeight; 73  74   /** Delegating listener, can be null. */ 75   OnScrollListener mDelegateOnScrollListener; 76  77   /** Shadow for being recycled, can be null. */ 78   PinnedSection mRecycleSection; 79  80   /** shadow instance with a pinned view, can be null. */ 81   PinnedSection mPinnedSection; 82  83   /** Pinned view Y-translation. We use it to stick pinned view to the next section. */ 84   int mTranslateY; 85  86   /** Scroll listener which does the magic */ 87   private final OnScrollListener mOnScrollListener = new OnScrollListener() { 88  89     @Override public void onScrollStateChanged(AbsListView view, int scrollState) { 90       if (mDelegateOnScrollListener != null) { // delegate 91         mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); 92       } 93     } 94  95     @Override 96     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 97  98       if (mDelegateOnScrollListener != null) { // delegate 99         mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);100       }101 102       // get expected adapter or fail fast103       ListAdapter adapter = getAdapter();104       if (adapter == null || visibleItemCount == 0) return; // nothing to do105 106       final boolean isFirstVisibleItemSection =107           isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem));108 109       if (isFirstVisibleItemSection) {110         View sectionView = getChildAt(0);111         if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow112           destroyPinnedShadow();113         } else { // section doesn't stick to the top, make sure we have a pinned shadow114           ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount);115         }116 117       } else { // section is not at the first visible position118         int sectionPosition = findCurrentSectionPosition(firstVisibleItem);119         if (sectionPosition > -1) { // we have section position120           ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount);121         } else { // there is no section for the first visible item, destroy shadow122           destroyPinnedShadow();123         }124       }125     };126 127   };128 129   /** Default change observer. */130   private final DataSetObserver mDataSetObserver = new DataSetObserver() {131     @Override public void onChanged() {132       recreatePinnedShadow();133     };134     @Override public void onInvalidated() {135       recreatePinnedShadow();136     }137   };138 139   //-- constructors140 141   public PinnedSectionListView(Context context, AttributeSet attrs) {142     super(context, attrs);143     initView();144   }145 146   public PinnedSectionListView(Context context, AttributeSet attrs, int defStyle) {147     super(context, attrs, defStyle);148     initView();149   }150 151   private void initView() {152     setOnScrollListener(mOnScrollListener);153     mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();154     initShadow(true);155   }156 157   //-- public API methods158 159   public void setShadowVisible(boolean visible) {160     initShadow(visible);161     if (mPinnedSection != null) {162       View v = mPinnedSection.view;163       invalidate(v.getLeft(), v.getTop(), v.getRight(), v.getBottom() + mShadowHeight);164     }165   }166 167   //-- pinned section drawing methods168 169   public void initShadow(boolean visible) {170     if (visible) {171       if (mShadowDrawable == null) {172         mShadowDrawable = new GradientDrawable(Orientation.TOP_BOTTOM,173             new int[] { Color.parseColor("#ffa0a0a0"), Color.parseColor("#50a0a0a0"), Color.parseColor("#00a0a0a0")});174         mShadowHeight = (int) (8 * getResources().getDisplayMetrics().density);175       }176     } else {177       if (mShadowDrawable != null) {178         mShadowDrawable = null;179         mShadowHeight = 0;180       }181     }182   }183 184   /** Create shadow wrapper with a pinned view for a view at given position */185   void createPinnedShadow(int position) {186 187     // try to recycle shadow188     PinnedSection pinnedShadow = mRecycleSection;189     mRecycleSection = null;190 191     // create new shadow, if needed192     if (pinnedShadow == null) pinnedShadow = new PinnedSection();193     // request new view using recycled view, if such194     View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this);195 196     // read layout parameters197     LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams();198     if (layoutParams == null) {199       layoutParams = (LayoutParams) generateDefaultLayoutParams();200       pinnedView.setLayoutParams(layoutParams);201     }202 203     int heightMode = MeasureSpec.getMode(layoutParams.height);204     int heightSize = MeasureSpec.getSize(layoutParams.height);205 206     if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY;207 208     int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom();209     if (heightSize > maxHeight) heightSize = maxHeight;210 211     // measure & layout212     int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY);213     int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode);214     pinnedView.measure(ws, hs);215     pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight());216     mTranslateY = 0;217 218     // initialize pinned shadow219     pinnedShadow.view = pinnedView;220     pinnedShadow.position = position;221     pinnedShadow.id = getAdapter().getItemId(position);222 223     // store pinned shadow224     mPinnedSection = pinnedShadow;225   }226 227   /** Destroy shadow wrapper for currently pinned view */228   void destroyPinnedShadow() {229     if (mPinnedSection != null) {230       // keep shadow for being recycled later231       mRecycleSection = mPinnedSection;232       mPinnedSection = null;233     }234   }235 236   /** Makes sure we have an actual pinned shadow for given position. */237   void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) {238     if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item239       destroyPinnedShadow();240       return;241     }242 243     if (mPinnedSection != null244         && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required245       destroyPinnedShadow();246     }247 248     if (mPinnedSection == null) { // create shadow, if empty249       createPinnedShadow(sectionPosition);250     }251 252     // align shadow according to next section position, if needed253     int nextPosition = sectionPosition + 1;254     if (nextPosition < getCount()) {255       int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition,256           visibleItemCount - (nextPosition - firstVisibleItem));257       if (nextSectionPosition > -1) {258         View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem);259         final int bottom = mPinnedSection.view.getBottom() + getPaddingTop();260         mSectionsDistanceY = nextSectionView.getTop() - bottom;261         if (mSectionsDistanceY < 0) {262           // next section overlaps pinned shadow, move it up263           mTranslateY = mSectionsDistanceY;264         } else {265           // next section does not overlap with pinned, stick to top266           mTranslateY = 0;267         }268       } else {269         // no other sections are visible, stick to top270         mTranslateY = 0;271         mSectionsDistanceY = Integer.MAX_VALUE;272       }273     }274 275   }276 277   int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) {278     ListAdapter adapter = getAdapter();279 280     int adapterDataCount = adapter.getCount();281     if (getLastVisiblePosition() >= adapterDataCount) return -1; // dataset has changed, no candidate282 283     if (firstVisibleItem+visibleItemCount >= adapterDataCount){//added to prevent index Outofbound (in case)284       visibleItemCount = adapterDataCount-firstVisibleItem;285     }286 287     for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) {288       int position = firstVisibleItem + childIndex;289       int viewType = adapter.getItemViewType(position);290       if (isItemViewTypePinned(adapter, viewType)) return position;291     }292     return -1;293   }294 295   int findCurrentSectionPosition(int fromPosition) {296     ListAdapter adapter = getAdapter();297 298     if (fromPosition >= adapter.getCount()) return -1; // dataset has changed, no candidate299     300     if (adapter instanceof SectionIndexer) {301       // try fast way by asking section indexer302       SectionIndexer indexer = (SectionIndexer) adapter;303       int sectionPosition = indexer.getSectionForPosition(fromPosition);304       int itemPosition = indexer.getPositionForSection(sectionPosition);305       int typeView = adapter.getItemViewType(itemPosition);306       if (isItemViewTypePinned(adapter, typeView)) {307         return itemPosition;308       } // else, no luck309     }310 311     // try slow way by looking through to the next section item above312     for (int position=fromPosition; position>=0; position--) {313       int viewType = adapter.getItemViewType(position);314       if (isItemViewTypePinned(adapter, viewType)) return position;315     }316     return -1; // no candidate found317   }318 319   void recreatePinnedShadow() {320     destroyPinnedShadow();321     ListAdapter adapter = getAdapter();322     if (adapter != null && adapter.getCount() > 0) {323       int firstVisiblePosition = getFirstVisiblePosition();324       int sectionPosition = findCurrentSectionPosition(firstVisiblePosition);325       if (sectionPosition == -1) return; // no views to pin, exit326       ensureShadowForPosition(sectionPosition,327           firstVisiblePosition, getLastVisiblePosition() - firstVisiblePosition);328     }329   }330 331   @Override332   public void setOnScrollListener(OnScrollListener listener) {333     if (listener == mOnScrollListener) {334       super.setOnScrollListener(listener);335     } else {336       mDelegateOnScrollListener = listener;337     }338   }339 340   @Override341   public void onRestoreInstanceState(Parcelable state) {342     super.onRestoreInstanceState(state);343     post(new Runnable() {344       @Override public void run() { // restore pinned view after configuration change345         recreatePinnedShadow();346       }347     });348   }349 350   @Override351   public void setAdapter(ListAdapter adapter) {352 353     // assert adapter in debug mode354     if (BuildConfig.DEBUG && adapter != null) {355       if (!(adapter instanceof PinnedSectionListAdapter))356         throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?");357       if (adapter.getViewTypeCount() < 2)358         throw new IllegalArgumentException("Does your adapter handle at least two types" +359             " of views in getViewTypeCount() method: items and sections?");360     }361 362     // unregister observer at old adapter and register on new one363     ListAdapter oldAdapter = getAdapter();364     if (oldAdapter != null) oldAdapter.unregisterDataSetObserver(mDataSetObserver);365     if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver);366 367     // destroy pinned shadow, if new adapter is not same as old one368     if (oldAdapter != adapter) destroyPinnedShadow();369 370     super.setAdapter(adapter);371   }372 373   @Override374   protected void onLayout(boolean changed, int l, int t, int r, int b) {375     super.onLayout(changed, l, t, r, b);376     if (mPinnedSection != null) {377       int parentWidth = r - l - getPaddingLeft() - getPaddingRight();378       int shadowWidth = mPinnedSection.view.getWidth();379       if (parentWidth != shadowWidth) {380         recreatePinnedShadow();381       }382     }383   }384 385   @Override386   protected void dispatchDraw(Canvas canvas) {387     super.dispatchDraw(canvas);388 389     if (mPinnedSection != null) {390 391       // prepare variables392       int pLeft = getListPaddingLeft();393       int pTop = getListPaddingTop();394       View view = mPinnedSection.view;395 396       // draw child397       canvas.save();398 399       int clipHeight = view.getHeight() +400           (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY));401       canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight);402 403       canvas.translate(pLeft, pTop + mTranslateY);404       drawChild(canvas, mPinnedSection.view, getDrawingTime());405 406       if (mShadowDrawable != null && mSectionsDistanceY > 0) {407         mShadowDrawable.setBounds(mPinnedSection.view.getLeft(),408             mPinnedSection.view.getBottom(),409             mPinnedSection.view.getRight(),410             mPinnedSection.view.getBottom() + mShadowHeight);411         mShadowDrawable.draw(canvas);412       }413 414       canvas.restore();415     }416   }417 418   //-- touch handling methods419 420   @Override421   public boolean dispatchTouchEvent(MotionEvent ev) {422 423     final float x = ev.getX();424     final float y = ev.getY();425     final int action = ev.getAction();426 427     if (action == MotionEvent.ACTION_DOWN428         && mTouchTarget == null429         && mPinnedSection != null430         && isPinnedViewTouched(mPinnedSection.view, x, y)) { // create touch target431 432       // user touched pinned view433       mTouchTarget = mPinnedSection.view;434       mTouchPoint.x = x;435       mTouchPoint.y = y;436 437       // copy down event for eventually be used later438       mDownEvent = MotionEvent.obtain(ev);439     }440 441     if (mTouchTarget != null) {442       if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view443         mTouchTarget.dispatchTouchEvent(ev);444       }445 446       if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view447         super.dispatchTouchEvent(ev);448         performPinnedItemClick();449         clearTouchTarget();450 451       } else if (action == MotionEvent.ACTION_CANCEL) { // cancel452         clearTouchTarget();453 454       } else if (action == MotionEvent.ACTION_MOVE) {455         if (Math.abs(y - mTouchPoint.y) > mTouchSlop) {456 457           // cancel sequence on touch target458           MotionEvent event = MotionEvent.obtain(ev);459           event.setAction(MotionEvent.ACTION_CANCEL);460           mTouchTarget.dispatchTouchEvent(event);461           event.recycle();462 463           // provide correct sequence to super class for further handling464           super.dispatchTouchEvent(mDownEvent);465           super.dispatchTouchEvent(ev);466           clearTouchTarget();467 468         }469       }470 471       return true;472     }473 474     // call super if this was not our pinned view475     return super.dispatchTouchEvent(ev);476   }477 478   private boolean isPinnedViewTouched(View view, float x, float y) {479     view.getHitRect(mTouchRect);480 481     // by taping top or bottom padding, the list performs on click on a border item.482     // we don't add top padding here to keep behavior consistent.483     mTouchRect.top += mTranslateY;484 485     mTouchRect.bottom += mTranslateY + getPaddingTop();486     mTouchRect.left += getPaddingLeft();487     mTouchRect.right -= getPaddingRight();488     return mTouchRect.contains((int)x, (int)y);489   }490 491   private void clearTouchTarget() {492     mTouchTarget = null;493     if (mDownEvent != null) {494       mDownEvent.recycle();495       mDownEvent = null;496     }497   }498 499   private boolean performPinnedItemClick() {500     if (mPinnedSection == null) return false;501 502     OnItemClickListener listener = getOnItemClickListener();503     if (listener != null && getAdapter().isEnabled(mPinnedSection.position)) {504       View view = mPinnedSection.view;505       playSoundEffect(SoundEffectConstants.CLICK);506       if (view != null) {507         view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);508       }509       listener.onItemClick(this, view, mPinnedSection.position, mPinnedSection.id);510       return true;511     }512     return false;513   }514 515   public static boolean isItemViewTypePinned(ListAdapter adapter, int viewType) {516     if (adapter instanceof HeaderViewListAdapter) {517       adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();518     }519     return ((PinnedSectionListAdapter) adapter).isItemViewTypePinned(viewType);520   }521 522 }

 

自己app的代码:

 1 package com.lixu.biaoqianxuanting; 2  3 import java.util.ArrayList; 4 import com.lixu.biaoqianxuanting.PinnedSectionListView.PinnedSectionListAdapter; 5 import android.app.Activity; 6 import android.content.Context; 7 import android.graphics.Color; 8 import android.os.Bundle; 9 import android.view.LayoutInflater; 10 import android.view.View; 11 import android.view.ViewGroup; 12 import android.widget.ArrayAdapter; 13 import android.widget.TextView; 14  15 public class MainActivity extends Activity { 16   private ArrayList<Data> data; 17  18   @Override 19   protected void onCreate(Bundle savedInstanceState) { 20     super.onCreate(savedInstanceState); 21     setContentView(R.layout.activity_main); 22  23     String[] s = { "家人", "朋友", "同事", "同学", "基友", "情人", "老婆" }; 24  25     data = new ArrayList<Data>(); 26     for (String n : s) { 27       Data group = new Data(); 28       group.type = Data.GROUP; 29       group.text = n; 30       data.add(group); 31       for (int i = 0; i < 10; i++) { 32         Data child = new Data(); 33         child.type = Data.CHILD; 34         child.text = "联系人" + i; 35         data.add(child); 36       } 37     } 38  39     PinnedSectionListView pslv = (PinnedSectionListView) findViewById(R.id.pslv); 40     MyAdapter mMyAdapter = new MyAdapter(this, -1); 41     pslv.setAdapter(mMyAdapter); 42   } 43  44   // 定义一个存放数据类型的类 45   private class Data { 46     public static final int GROUP = 0; 47     public static final int CHILD = 1; 48     public int type; 49     public String text; 50     // 2个type child和group 51     public static final int TYPE_COUNT = 2; 52  53   } 54  55   // 定义适配器要实现PinnedSectionListAdapter接口 来调用isItemViewTypePinned(int 56   // viewType)方法 57   private class MyAdapter extends ArrayAdapter implements PinnedSectionListAdapter { 58     private LayoutInflater flater; 59  60     public MyAdapter(Context context, int resource) { 61       super(context, resource); 62  63       flater = LayoutInflater.from(context); 64     } 65  66     @Override 67     public int getCount() { 68       return data.size(); 69     } 70  71     @Override 72     public View getView(int position, View convertView, ViewGroup parent) { 73       int type = getItemViewType(position); 74       switch (type) { 75       case Data.GROUP: 76         if (convertView == null) 77           convertView = flater.inflate(android.R.layout.simple_list_item_1, null); 78  79         TextView tv1 = (TextView) convertView.findViewById(android.R.id.text1); 80  81         tv1.setText(data.get(position).text); 82         tv1.setBackgroundColor(Color.BLUE); 83         tv1.setTextSize(35); 84  85         break; 86  87       case Data.CHILD: 88         if (convertView == null) 89           convertView = flater.inflate(android.R.layout.simple_list_item_1, null); 90  91         TextView tv2 = (TextView) convertView.findViewById(android.R.id.text1); 92  93         tv2.setText(data.get(position).text); 94  95         tv2.setTextSize(15); 96  97         break; 98  99       default:100         break;101       }102 103       return convertView;104     }105 106     // 返回列表类型 两种 child和group107     @Override108     public int getViewTypeCount() {109 110       return Data.TYPE_COUNT;111     }112 113     // 获取列表类型114     @Override115     public int getItemViewType(int position) {116       return data.get(position).type;117     }118 119     @Override120     public Object getItem(int position) {121       return data.get(position);122     }123 124     // 假设此方法返回皆为false。那么PinnedSectionListView将退化成为一个基础的ListView.125     // 只不过退化后的ListView只是一个拥有两个View Type的ListView。126     // 从某种角度上讲,此方法对于PinnedSectionListView至关重要,因为返回值true或false,将直接导致PinnedSectionListView是一个PinnedSectionListView,还是一个普通的ListView。127     @Override128     public boolean isItemViewTypePinned(int viewType) {129       boolean type = false;130       switch (viewType) {131       case Data.GROUP:132 133         type = true;134 135         break;136 137       case Data.CHILD:138 139         type = false;140 141         break;142       default:143         type = false;144         break;145       }146       return type;147     }148 149   }150 151 }

 1 <RelativeLayout ="http://schemas.android.com/apk/res/android" 2   ="http://schemas.android.com/tools" 3   android:layout_width="match_parent" 4   android:layout_height="match_parent" > 5  6   <com.lixu.biaoqianxuanting.PinnedSectionListView 7     android:id="@+id/pslv" 8     android:layout_width="match_parent" 9     android:layout_height="match_parent" />10 11 </RelativeLayout>


运行效果图: