IndexableListView 实现

简单分析一下listview右侧快速拖动条的实现.

1 IndexScroller

首先实现一个IndexScroller类,这个类用于控制右侧的快速拖动条.

1.1 构造方法:

public IndexScroller(Context context, ListView lv) {
    mDensity = context.getResources().getDisplayMetrics().density;
    mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
    mListView = lv;
    setAdapter(mListView.getAdapter());
    
    mIndexbarWidth = 20 * mDensity;
    mIndexbarMargin = 10 * mDensity;
    mPreviewPadding = 5 * mDensity;
}

构造方法主要是设置高宽,绑定listview(需要实现SectionIndexer接口)和adapter.

1.2 拦截触摸事件:

public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        // If down event occurs inside index bar region, start indexing
        if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) {
            setState(STATE_SHOWN);
            
            // It demonstrates that the motion event started from index bar
            mIsIndexing = true;
            // Determine which section the point is in, and move the list to that section
            mCurrentSection = getSectionByPoint(ev.getY());
            mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
            return true;
        }
        break;
    case MotionEvent.ACTION_MOVE:
        if (mIsIndexing) {
            // If this event moves inside index bar
            if (contains(ev.getX(), ev.getY())) {
                // Determine which section the point is in, and move the list to that section
                mCurrentSection = getSectionByPoint(ev.getY());
                mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
            }
            return true;
        }
        break;
    case MotionEvent.ACTION_UP:
        if (mIsIndexing) {
            mIsIndexing = false;
            mCurrentSection = -1;
        }
        if (mState == STATE_SHOWN)
            setState(STATE_HIDING);
        break;
    }
    return false;
}

当检测到按下和移动时,根据位置获取当前按下的Section,然后调用SectionIndexer的方法获取对应的Listv的位置,然后设置listeview 的setSelection.

当松开时,隐藏listview.

1.3 绘制

绘制主要使用Paint在Canvas上绘制右侧图形,其实本身并不复杂,不再详述.

public void draw(Canvas canvas) {
    if (mState == STATE_HIDDEN)
        return;
    
    // mAlphaRate determines the rate of opacity
    Paint indexbarPaint = new Paint();
    indexbarPaint.setColor(Color.BLACK);
    indexbarPaint.setAlpha((int) (64 * mAlphaRate));
    indexbarPaint.setAntiAlias(true);
    canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity, indexbarPaint);
    
    if (mSections != null && mSections.length > 0) {
        // Preview is shown when mCurrentSection is set
        if (mCurrentSection >= 0) {
            Paint previewPaint = new Paint();
            previewPaint.setColor(Color.BLACK);
            previewPaint.setAlpha(96);
            previewPaint.setAntiAlias(true);
            previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0));
            
            Paint previewTextPaint = new Paint();
            previewTextPaint.setColor(Color.WHITE);
            previewTextPaint.setAntiAlias(true);
            previewTextPaint.setTextSize(50 * mScaledDensity);
            
            float previewTextWidth = previewTextPaint.measureText(mSections[mCurrentSection]);
            float previewSize = 2 * mPreviewPadding + previewTextPaint.descent() - previewTextPaint.ascent();
            RectF previewRect = new RectF((mListViewWidth - previewSize) / 2
                    , (mListViewHeight - previewSize) / 2
                    , (mListViewWidth - previewSize) / 2 + previewSize
                    , (mListViewHeight - previewSize) / 2 + previewSize);
            
            canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint);
            canvas.drawText(mSections[mCurrentSection], previewRect.left + (previewSize - previewTextWidth) / 2 - 1
                    , previewRect.top + mPreviewPadding - previewTextPaint.ascent() + 1, previewTextPaint);
        }
        
        Paint indexPaint = new Paint();
        indexPaint.setColor(Color.WHITE);
        indexPaint.setAlpha((int) (255 * mAlphaRate));
        indexPaint.setAntiAlias(true);
        indexPaint.setTextSize(12 * mScaledDensity);
        
        float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length;
        float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint.ascent())) / 2;
        for (int i = 0; i < mSections.length; i++) {
            float paddingLeft = (mIndexbarWidth - indexPaint.measureText(mSections[i])) / 2;
            if (mCurrentSection==i) {
                Paint selectPaint = new Paint();
                selectPaint.setColor(Color.GREEN);
                selectPaint.setAlpha((int) (255 * mAlphaRate));
                selectPaint.setAntiAlias(true);
                selectPaint.setTextSize(12 * mScaledDensity);
                canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
                        , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - selectPaint.ascent(), selectPaint);
            }else {
                canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
                        , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - indexPaint.ascent(), indexPaint);
            }
            
        }
    }
}

至此,IndexScroller的工作已经完成.

2 IndexableListView

我们注意到上面的IndexScroller其实只是一个普通类,所以里面的draw 拦截按键等方法并不会被真正被调用,所以我们需要在IndexableListView中调用这些方法.

IndexableListView是一个继承自ListView的类,首先通过setFastScrollEnabled方法来创建IndexScroller对象.

public void setFastScrollEnabled(boolean enabled) {
    mIsFastScrollEnabled = enabled;
    if (mIsFastScrollEnabled) {
        if (mScroller == null)
            mScroller = new IndexScroller(getContext(), this);
    } else {
        if (mScroller != null) {
            mScroller.hide();
            mScroller = null;
        }
    }
}

重写draw方法,调用IndexScroller的draw方法:

public void draw(Canvas canvas) {
    super.draw(canvas);
    // Overlay index bar
    if (mScroller != null)
        mScroller.draw(canvas);
}

重写onInterceptTouchEvent方法,判断如果触摸位置在IndexScroller范围内,则拦截事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(mScroller.contains(ev.getX(), ev.getY()))
        return true;
    return super.onInterceptTouchEvent(ev);
}

重写onTouchEvent方法:

public boolean onTouchEvent(MotionEvent ev) {
    // Intercept ListView's touch event
    if (mScroller != null && mScroller.onTouchEvent(ev))
        return true;
    
    if (mGestureDetector == null) {
        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2,
                    float velocityX, float velocityY) {
                // If fling happens, index bar shows
                if (mScroller != null)
                    mScroller.show();
                return super.onFling(e1, e2, velocityX, velocityY);
            }
            
        });
    }
    mGestureDetector.onTouchEvent(ev);
    
    return super.onTouchEvent(ev);
}

主要是两个作用,一个是当IndexScroller存在时拦截按键事件处理,另一个就是在mScroller 不可见时,将mScroller 置为可见.

下面附上两个类的全部代码:

IndexableListView主要实现代码在以下两个类:

public class IndexableListView extends ListView {

    private boolean mIsFastScrollEnabled = false;
    private IndexScroller mScroller = null;
    private GestureDetector mGestureDetector = null;

    public IndexableListView(Context context) {
        super(context);
    }

    public IndexableListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public IndexableListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean isFastScrollEnabled() {
        return mIsFastScrollEnabled;
    }

    @Override
    public void setFastScrollEnabled(boolean enabled) {
        mIsFastScrollEnabled = enabled;
        if (mIsFastScrollEnabled) {
            if (mScroller == null)
                mScroller = new IndexScroller(getContext(), this);
        } else {
            if (mScroller != null) {
                mScroller.hide();
                mScroller = null;
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        
        // Overlay index bar
        if (mScroller != null)
            mScroller.draw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Intercept ListView's touch event
        if (mScroller != null && mScroller.onTouchEvent(ev))
            return true;
        
        if (mGestureDetector == null) {
            mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {

                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2,
                        float velocityX, float velocityY) {
                    // If fling happens, index bar shows
                    if (mScroller != null)
                        mScroller.show();
                    return super.onFling(e1, e2, velocityX, velocityY);
                }
                
            });
        }
        mGestureDetector.onTouchEvent(ev);
        
        return super.onTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(mScroller.contains(ev.getX(), ev.getY()))
            return true;
        
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);
        if (mScroller != null)
            mScroller.setAdapter(adapter);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mScroller != null)
            mScroller.onSizeChanged(w, h, oldw, oldh);
    }
}

还有一个IndexScroller:

public class IndexScroller {
    
    private float mIndexbarWidth;
    private float mIndexbarMargin;
    private float mPreviewPadding;
    private float mDensity;
    private float mScaledDensity;
    private float mAlphaRate;
    private int mState = STATE_HIDDEN;
    private int mListViewWidth;
    private int mListViewHeight;
    private int mCurrentSection = -1;
    private boolean mIsIndexing = false;
    private ListView mListView = null;
    private SectionIndexer mIndexer = null;
    private String[] mSections = null;
    private RectF mIndexbarRect;
    
    private static final int STATE_HIDDEN = 0;
    private static final int STATE_SHOWING = 1;
    private static final int STATE_SHOWN = 2;
    private static final int STATE_HIDING = 3;
    
    public IndexScroller(Context context, ListView lv) {
        mDensity = context.getResources().getDisplayMetrics().density;
        mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
        mListView = lv;
        setAdapter(mListView.getAdapter());
        
        mIndexbarWidth = 20 * mDensity;
        mIndexbarMargin = 10 * mDensity;
        mPreviewPadding = 5 * mDensity;
    }

    public void draw(Canvas canvas) {
        if (mState == STATE_HIDDEN)
            return;
        
        // mAlphaRate determines the rate of opacity
        Paint indexbarPaint = new Paint();
        indexbarPaint.setColor(Color.BLACK);
        indexbarPaint.setAlpha((int) (64 * mAlphaRate));
        indexbarPaint.setAntiAlias(true);
        canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity, indexbarPaint);
        
        if (mSections != null && mSections.length > 0) {
            // Preview is shown when mCurrentSection is set
            if (mCurrentSection >= 0) {
                Paint previewPaint = new Paint();
                previewPaint.setColor(Color.BLACK);
                previewPaint.setAlpha(96);
                previewPaint.setAntiAlias(true);
                previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0));
                
                Paint previewTextPaint = new Paint();
                previewTextPaint.setColor(Color.WHITE);
                previewTextPaint.setAntiAlias(true);
                previewTextPaint.setTextSize(50 * mScaledDensity);
                
                float previewTextWidth = previewTextPaint.measureText(mSections[mCurrentSection]);
                float previewSize = 2 * mPreviewPadding + previewTextPaint.descent() - previewTextPaint.ascent();
                RectF previewRect = new RectF((mListViewWidth - previewSize) / 2
                        , (mListViewHeight - previewSize) / 2
                        , (mListViewWidth - previewSize) / 2 + previewSize
                        , (mListViewHeight - previewSize) / 2 + previewSize);
                
                canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint);
                canvas.drawText(mSections[mCurrentSection], previewRect.left + (previewSize - previewTextWidth) / 2 - 1
                        , previewRect.top + mPreviewPadding - previewTextPaint.ascent() + 1, previewTextPaint);
            }
            
            Paint indexPaint = new Paint();
            indexPaint.setColor(Color.WHITE);
            indexPaint.setAlpha((int) (255 * mAlphaRate));
            indexPaint.setAntiAlias(true);
            indexPaint.setTextSize(12 * mScaledDensity);
            
            float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length;
            float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint.ascent())) / 2;
            for (int i = 0; i < mSections.length; i++) {
                float paddingLeft = (mIndexbarWidth - indexPaint.measureText(mSections[i])) / 2;
                if (mCurrentSection==i) {
                    Paint selectPaint = new Paint();
                    selectPaint.setColor(Color.GREEN);
                    selectPaint.setAlpha((int) (255 * mAlphaRate));
                    selectPaint.setAntiAlias(true);
                    selectPaint.setTextSize(12 * mScaledDensity);
                    canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
                            , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - selectPaint.ascent(), selectPaint);
                }else {
                    canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
                            , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - indexPaint.ascent(), indexPaint);
                }
                
            }
        }
    }
    
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // If down event occurs inside index bar region, start indexing
            if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) {
                setState(STATE_SHOWN);
                
                // It demonstrates that the motion event started from index bar
                mIsIndexing = true;
                // Determine which section the point is in, and move the list to that section
                mCurrentSection = getSectionByPoint(ev.getY());
                mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
                return true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (mIsIndexing) {
                // If this event moves inside index bar
                if (contains(ev.getX(), ev.getY())) {
                    // Determine which section the point is in, and move the list to that section
                    mCurrentSection = getSectionByPoint(ev.getY());
                    mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
                }
                return true;
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mIsIndexing) {
                mIsIndexing = false;
                mCurrentSection = -1;
            }
            if (mState == STATE_SHOWN)
                setState(STATE_HIDING);
            break;
        }
        return false;
    }
    
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        mListViewWidth = w;
        mListViewHeight = h;
        mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth
                , mIndexbarMargin
                , w - mIndexbarMargin
                , h - mIndexbarMargin);
    }
    
    public void show() {
        if (mState == STATE_HIDDEN)
            setState(STATE_SHOWING);
        else if (mState == STATE_HIDING)
            setState(STATE_HIDING);
    }
    
    public void hide() {
        if (mState == STATE_SHOWN)
            setState(STATE_HIDING);
    }
    
    public void setAdapter(Adapter adapter) {
        if (adapter instanceof SectionIndexer) {
            mIndexer = (SectionIndexer) adapter;
            mSections = (String[]) mIndexer.getSections();
        }
    }
    
    private void setState(int state) {
        if (state < STATE_HIDDEN || state > STATE_HIDING)
            return;
        
        mState = state;
        switch (mState) {
        case STATE_HIDDEN:
            // Cancel any fade effect
            mHandler.removeMessages(0);
            break;
        case STATE_SHOWING:
            // Start to fade in
            mAlphaRate = 0;
            fade(0);
            break;
        case STATE_SHOWN:
            // Cancel any fade effect
            mHandler.removeMessages(0);
            break;
        case STATE_HIDING:
            // Start to fade out after three seconds
            mAlphaRate = 1;
            fade(3000);
            break;
        }
    }
    
    public boolean contains(float x, float y) {
        // Determine if the point is in index bar region, which includes the right margin of the bar
        return (x >= mIndexbarRect.left && y >= mIndexbarRect.top && y <= mIndexbarRect.top + mIndexbarRect.height());
    }
    
    private int getSectionByPoint(float y) {
        if (mSections == null || mSections.length == 0)
            return 0;
        if (y < mIndexbarRect.top + mIndexbarMargin)
            return 0;
        if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin)
            return mSections.length - 1;
        return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length));
    }
    
    private void fade(long delay) {
        mHandler.removeMessages(0);
        mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
    }
    
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            
            switch (mState) {
            case STATE_SHOWING:
                // Fade in effect
                mAlphaRate += (1 - mAlphaRate) * 0.2;
                if (mAlphaRate > 0.9) {
                    mAlphaRate = 1;
                    setState(STATE_SHOWN);
                }
                
                mListView.invalidate();
                fade(10);
                break;
            case STATE_SHOWN:
                // If no action, hide automatically
                setState(STATE_HIDING);
                break;
            case STATE_HIDING:
                // Fade out effect
                mAlphaRate -= mAlphaRate * 0.2;
                if (mAlphaRate < 0.1) {
                    mAlphaRate = 0;
                    setState(STATE_HIDDEN);
                }
                
                mListView.invalidate();
                fade(10);
                break;
            }
        }
    };
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容