PageRenderTime 29ms CodeModel.GetById 2ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/CVox/src/com/android/contacts/FastScrollView.java

http://eyes-free.googlecode.com/
Java | 453 lines | 341 code | 63 blank | 49 comment | 75 complexity | 46eecf134f722326f65649656020489e MD5 | raw file
  1/*
  2 * Copyright (C) 2008 The Android Open Source Project
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License");
  5 * you may not use this file except in compliance with the License.
  6 * You may obtain a copy of the License at
  7 *
  8 *      http://www.apache.org/licenses/LICENSE-2.0
  9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS,
 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 * See the License for the specific language governing permissions and
 14 * limitations under the License.
 15 */
 16
 17//package com.android.contacts;
 18package com.android.contacts;
 19
 20import com.cvox.browser.R;
 21
 22import android.content.Context;
 23import android.content.res.Resources;
 24import android.graphics.Canvas;
 25import android.graphics.Paint;
 26import android.graphics.RectF;
 27import android.graphics.drawable.Drawable;
 28import android.os.Handler;
 29import android.os.SystemClock;
 30import android.util.AttributeSet;
 31import android.view.MotionEvent;
 32import android.view.View;
 33import android.view.ViewGroup.OnHierarchyChangeListener;
 34import android.widget.AbsListView;
 35import android.widget.Adapter;
 36import android.widget.BaseAdapter;
 37import android.widget.FrameLayout;
 38import android.widget.HeaderViewListAdapter;
 39import android.widget.ListView;
 40import android.widget.AbsListView.OnScrollListener;
 41
 42/**
 43 * FastScrollView is meant for embedding {@link ListView}s that contain a large number of 
 44 * items that can be indexed in some fashion. It displays a special scroll bar that allows jumping
 45 * quickly to indexed sections of the list in touch-mode. Only one child can be added to this 
 46 * view group and it must be a {@link ListView}, with an adapter that is derived from 
 47 * {@link BaseAdapter}.
 48 */
 49public class FastScrollView extends FrameLayout 
 50        implements OnScrollListener, OnHierarchyChangeListener {
 51
 52    private Drawable mCurrentThumb;
 53    private Drawable mOverlayDrawable;
 54
 55    private int mThumbH;
 56    private int mThumbW;
 57    private int mThumbY;
 58
 59    private RectF mOverlayPos;
 60    
 61    // Hard coding these for now
 62    private int mOverlaySize = 104;
 63
 64    private boolean mDragging;
 65    private ListView mList;
 66    private boolean mScrollCompleted;
 67    private boolean mThumbVisible;
 68    private int mVisibleItem;
 69    private Paint mPaint;
 70    private int mListOffset;
 71    
 72    private Object [] mSections;
 73    private String mSectionText;
 74    private boolean mDrawOverlay;
 75    private ScrollFade mScrollFade;
 76    
 77    private Handler mHandler = new Handler();
 78    
 79    private BaseAdapter mListAdapter;
 80
 81    private boolean mChangedBounds;
 82
 83    interface SectionIndexer {
 84        Object[] getSections();
 85        
 86        int getPositionForSection(int section);
 87        
 88        int getSectionForPosition(int position);
 89    }
 90    
 91    public FastScrollView(Context context) {
 92        super(context);
 93
 94        init(context);
 95    }
 96
 97
 98    public FastScrollView(Context context, AttributeSet attrs) {
 99        super(context, attrs);
100        
101        init(context);
102    }
103
104    public FastScrollView(Context context, AttributeSet attrs, int defStyle) {
105        super(context, attrs, defStyle);
106
107        init(context);
108    }
109
110    private void useThumbDrawable(Drawable drawable) {
111        mCurrentThumb = drawable;
112        mThumbW = 64; //mCurrentThumb.getIntrinsicWidth();
113        mThumbH = 52; //mCurrentThumb.getIntrinsicHeight();
114        mChangedBounds = true;
115    }
116
117    private void init(Context context) {
118        // Get both the scrollbar states drawables
119        final Resources res = context.getResources();
120        useThumbDrawable(res.getDrawable(
121                R.drawable.scrollbar_handle_accelerated_anim2));
122        
123        mOverlayDrawable = res.getDrawable(R.drawable.dialog_full_dark);
124        
125        mScrollCompleted = true;
126        setWillNotDraw(false);
127        
128        // Need to know when the ListView is added
129        setOnHierarchyChangeListener(this);
130        
131        mOverlayPos = new RectF();
132        mScrollFade = new ScrollFade();
133        mPaint = new Paint();
134        mPaint.setAntiAlias(true);
135        mPaint.setTextAlign(Paint.Align.CENTER);
136        mPaint.setTextSize(mOverlaySize / 2);
137        mPaint.setColor(0xFFFFFFFF);
138        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
139    }
140    
141    private void removeThumb() {
142        
143        mThumbVisible = false;
144        // Draw one last time to remove thumb
145        invalidate();
146    }
147    
148    @Override
149    public void draw(Canvas canvas) {
150        super.draw(canvas);
151        
152        if (!mThumbVisible) {
153            // No need to draw the rest
154            return;
155        }
156
157        final int y = mThumbY;
158        final int viewWidth = getWidth();
159        final FastScrollView.ScrollFade scrollFade = mScrollFade;
160
161        int alpha = -1;
162        if (scrollFade.mStarted) {
163            alpha = scrollFade.getAlpha();
164            if (alpha < ScrollFade.ALPHA_MAX / 2) {
165                mCurrentThumb.setAlpha(alpha * 2);
166            }
167            int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
168            mCurrentThumb.setBounds(left, 0, viewWidth, mThumbH);
169            mChangedBounds = true;
170        }
171
172        canvas.translate(0, y);
173        mCurrentThumb.draw(canvas);
174        canvas.translate(0, -y);
175
176        // If user is dragging the scroll bar, draw the alphabet overlay
177        if (mDragging && mDrawOverlay) {
178            mOverlayDrawable.draw(canvas);
179            final Paint paint = mPaint;
180            float descent = paint.descent();
181            final RectF rectF = mOverlayPos;
182            canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2,
183                    (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint);
184        } else if (alpha == 0) {
185            scrollFade.mStarted = false;
186            removeThumb();
187        } else {
188            invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);            
189        }
190    }
191
192    @Override
193    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
194        super.onSizeChanged(w, h, oldw, oldh);
195        if (mCurrentThumb != null) {
196            mCurrentThumb.setBounds(w - mThumbW, 0, w, mThumbH);
197        }
198        final RectF pos = mOverlayPos;
199        pos.left = (w - mOverlaySize) / 2;
200        pos.right = pos.left + mOverlaySize;
201        pos.top = h / 10; // 10% from top
202        pos.bottom = pos.top + mOverlaySize;
203        mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
204                (int) pos.right, (int) pos.bottom);
205    }
206    
207    public void onScrollStateChanged(AbsListView view, int scrollState) {
208    }
209    
210    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 
211            int totalItemCount) {
212        
213        if (totalItemCount - visibleItemCount > 0 && !mDragging) {
214            mThumbY = ((getHeight() - mThumbH) * firstVisibleItem) / (totalItemCount - visibleItemCount);
215            if (mChangedBounds) {
216                final int viewWidth = getWidth();
217                mCurrentThumb.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
218                mChangedBounds = false;
219            }
220        }
221        mScrollCompleted = true;
222        if (firstVisibleItem == mVisibleItem) {
223            return;
224        }
225        mVisibleItem = firstVisibleItem;
226        if (!mThumbVisible || mScrollFade.mStarted) {
227            mThumbVisible = true;
228            mCurrentThumb.setAlpha(ScrollFade.ALPHA_MAX);
229        }
230        mHandler.removeCallbacks(mScrollFade);
231        mScrollFade.mStarted = false;
232        if (!mDragging) {
233            mHandler.postDelayed(mScrollFade, 1500);
234        }
235    }
236
237    
238    private void getSections() {
239        Adapter adapter = mList.getAdapter();
240        if (adapter instanceof HeaderViewListAdapter) {
241            mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
242            adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
243        }
244        if (adapter instanceof SectionIndexer) {
245            mListAdapter = (BaseAdapter) adapter;
246            mSections = ((SectionIndexer) mListAdapter).getSections();
247        }
248    }
249    
250    public void onChildViewAdded(View parent, View child) {
251        if (child instanceof ListView) {
252            mList = (ListView)child;
253            
254            mList.setOnScrollListener(this);
255            getSections();
256        }
257    }
258
259    public void onChildViewRemoved(View parent, View child) {
260        if (child == mList) {
261            mList = null;
262            mListAdapter = null;
263            mSections = null;
264        }
265    }
266    
267    @Override
268    public boolean onInterceptTouchEvent(MotionEvent ev) {
269        if (mThumbVisible && ev.getAction() == MotionEvent.ACTION_DOWN) {
270            if (ev.getX() > getWidth() - mThumbW && ev.getY() >= mThumbY &&
271                    ev.getY() <= mThumbY + mThumbH) {
272                mDragging = true;
273                return true;
274            }            
275        }
276        return false;
277    }
278
279    private void scrollTo(float position) {
280        int count = mList.getCount();
281        mScrollCompleted = false;
282        final Object[] sections = mSections;
283        int sectionIndex;
284        if (sections != null && sections.length > 1) {
285            final int nSections = sections.length;
286            int section = (int) (position * nSections);
287            if (section >= nSections) {
288                section = nSections - 1;
289            }
290            sectionIndex = section;
291            final SectionIndexer baseAdapter = (SectionIndexer) mListAdapter;
292            int index = baseAdapter.getPositionForSection(section);
293            
294            // Given the expected section and index, the following code will
295            // try to account for missing sections (no names starting with..)
296            // It will compute the scroll space of surrounding empty sections
297            // and interpolate the currently visible letter's range across the
298            // available space, so that there is always some list movement while
299            // the user moves the thumb.
300            int nextIndex = count;
301            int prevIndex = index;
302            int prevSection = section;
303            int nextSection = section + 1;
304            // Assume the next section is unique
305            if (section < nSections - 1) {
306                nextIndex = baseAdapter.getPositionForSection(section + 1);
307            }
308            
309            // Find the previous index if we're slicing the previous section
310            if (nextIndex == index) {
311                // Non-existent letter
312                while (section > 0) {
313                    section--;
314                     prevIndex = baseAdapter.getPositionForSection(section);
315                     if (prevIndex != index) {
316                         prevSection = section;
317                         sectionIndex = section;
318                         break;
319                     }
320                }
321            }
322            // Find the next index, in case the assumed next index is not
323            // unique. For instance, if there is no P, then request for P's 
324            // position actually returns Q's. So we need to look ahead to make
325            // sure that there is really a Q at Q's position. If not, move 
326            // further down...
327            int nextNextSection = nextSection + 1;
328            while (nextNextSection < nSections &&
329                    baseAdapter.getPositionForSection(nextNextSection) == nextIndex) {
330                nextNextSection++;
331                nextSection++;
332            }
333            // Compute the beginning and ending scroll range percentage of the
334            // currently visible letter. This could be equal to or greater than
335            // (1 / nSections). 
336            float fPrev = (float) prevSection / nSections;
337            float fNext = (float) nextSection / nSections;
338            index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) 
339                    / (fNext - fPrev));
340            // Don't overflow
341            if (index > count - 1) index = count - 1;
342            
343            mList.setSelectionFromTop(index + mListOffset, 0);
344        } else {
345            int index = (int) (position * count);
346            mList.setSelectionFromTop(index + mListOffset, 0);
347            sectionIndex = -1;
348        }
349
350        if (sectionIndex >= 0) {
351            String text = mSectionText = sections[sectionIndex].toString();
352            mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
353                    sectionIndex < sections.length;
354        } else {
355            mDrawOverlay = false;
356        }
357    }
358
359    private void cancelFling() {
360        // Cancel the list fling
361        MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
362        mList.onTouchEvent(cancelFling);
363        cancelFling.recycle();
364    }
365    
366    @Override
367    public boolean onTouchEvent(MotionEvent me) {
368        if (me.getAction() == MotionEvent.ACTION_DOWN) {
369            if (me.getX() > getWidth() - mThumbW
370                    && me.getY() >= mThumbY 
371                    && me.getY() <= mThumbY + mThumbH) {
372                
373                mDragging = true;
374                if (mListAdapter == null && mList != null) {
375                    getSections();
376                }
377
378                cancelFling();
379                return true;
380            }
381        } else if (me.getAction() == MotionEvent.ACTION_UP) {
382            if (mDragging) {
383                mDragging = false;
384                final Handler handler = mHandler;
385                handler.removeCallbacks(mScrollFade);
386                handler.postDelayed(mScrollFade, 1000);
387                return true;
388            }
389        } else if (me.getAction() == MotionEvent.ACTION_MOVE) {
390            if (mDragging) {
391                final int viewHeight = getHeight();
392                mThumbY = (int) me.getY() - mThumbH + 10;
393                if (mThumbY < 0) {
394                    mThumbY = 0;
395                } else if (mThumbY + mThumbH > viewHeight) {
396                    mThumbY = viewHeight - mThumbH;
397                }
398                // If the previous scrollTo is still pending
399                if (mScrollCompleted) {
400                    scrollTo((float) mThumbY / (viewHeight - mThumbH));
401                }
402                return true;
403            }
404        }
405        
406        return super.onTouchEvent(me);
407    }
408    
409    public class ScrollFade implements Runnable {
410        
411        long mStartTime;
412        long mFadeDuration;
413        boolean mStarted;
414        static final int ALPHA_MAX = 255;
415        static final long FADE_DURATION = 200;
416        
417        void startFade() {
418            mFadeDuration = FADE_DURATION;
419            mStartTime = SystemClock.uptimeMillis();
420            mStarted = true;
421        }
422        
423        int getAlpha() {
424            if (!mStarted) {
425                return ALPHA_MAX;
426            }
427            int alpha;
428            long now = SystemClock.uptimeMillis();
429            if (now > mStartTime + mFadeDuration) {
430                alpha = 0;
431            } else {
432                alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); 
433            }
434            return alpha;
435        }
436        
437        public void run() {
438            if (!mStarted) {
439                startFade();
440                invalidate();
441            }
442            
443            if (getAlpha() > 0) {
444                final int y = mThumbY;
445                final int viewWidth = getWidth();
446                invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
447            } else {
448                mStarted = false;
449                removeThumb();
450            }
451        }
452    }
453}