当前位置: 代码迷 >> Android >> 联系人分组标签悬停滑入滑出的实现方法
  详细解决方案

联系人分组标签悬停滑入滑出的实现方法

热度:52   发布时间:2016-04-27 22:05:20.0
联系人分组标签悬停滑入滑出的实现方法。

《类似通讯录分组的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 }

xml文件:

 1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2     xmlns:tools="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>


运行效果图:

  相关解决方案