IndexableListView

// 索引条核心类

public class IndexScroller {

    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;

    private float mIndexbarWidth; // 索引条宽度,高度肯定是显示所有字符的, 所以高度是看内容的
    private float mIndexbarMargin; // 索引条距离屏幕右侧边缘的距离 , 索引条中的文字和边框的padding 也取这个值
    private float mPreviewPadding; // 预览框中的文字距离边框的长度
    private float mDensity; // 屏幕当前密度/标准密度(160)
    private float mScaledDensity; // 用在字体上的, 和上面一样, 当前密度/160
    private float mAlphaRate; // 透明度, 用于显示和隐藏索引条(包括文本) 0-1
    private int mState = STATE_HIDDEN; // 索引条的状态
    private int mListViewWidth; // listview 的宽
    private int mListViewHeight; // listview的高
    private int mCurrentSection = -1; // 当前被选中的索引项位置
    private boolean mIsIndexing = false;
    private ListView mListView = null; // 指向封装在内部的listview
    private SectionIndexer mIndexer = null; // 外部接口, 提供给调用时自定义一些方法来实现功能
    private String[] mSections = null; // 索引列表中的所有文本
    private RectF mIndexbarRect; // 索引条区域

    /***
     * 所有变量的初始化, 有三个地方, 1 构造函数中直接的定义索引条的宽和padding等 2
     * setAdapter(mListView.getAdapter());
     * 通过adapter把SectionIndexer对象和mSections传递进来, 3 onSizeChanged , 当父控件ListView
     * onSizeChanged 被调用的时候, 调用本类中的onSizeChanged 将listview的高和宽传递进来
     * 
     */

    /***
     * 构造方法初始化变量
     */

    public IndexScroller(Context ctx, ListView lv) {
        // 通过上下文获取当前屏幕密度比值
        mDensity = ctx.getResources().getDisplayMetrics().density;
        // 比值,字体对应的密度比值
        mScaledDensity = ctx.getResources().getDisplayMetrics().scaledDensity;

        // 因为IndexScroller 只是作为listView的核心部分, 具体的变量和方法, 其实是在ListView中定义的
        // 所以必须传递Listview进来进行初始化和绑定
        mListView = lv;

        // 绑定SectionIndexer接口的实现方法
        setAdapter(mListView.getAdapter());

        // 在标准屏幕下(160px) , 该控件宽20, 乘以密度比值得到实际需要的宽度px
        mIndexbarWidth = 20 * mDensity; // 实际大小, 索引条宽度
        mIndexbarMargin = 10 * mDensity; // 实际大小, 索引条距离屏幕右侧边缘的距离 ,
        mPreviewPadding = 5 * mDensity; // 实际大小, 预览框中的文字距离边框的长度
    }

    /**
     * 
     * 调用类中创建的Adapter绑定ListView , 且该 adapter中实现SectionIndexer接口方法
     * 于是在ListView.setAdapter方法被调用的时候, 调用核心类scroller类中此处的setAdapter
     * 使用adapter这种方式就能很好的实现方法和变量的传递,得到需要调用的SectionIndexer对象中的方法和mSections
     * 
     * @param adapter
     */
    public void setAdapter(Adapter adapter) {
        if (adapter instanceof SectionIndexer) {
            mIndexer = (SectionIndexer) adapter;
            mSections = (String[]) mIndexer.getSections();
        }
    }

    /***
     * 保持最新的高和宽 初始化 mIndexbarRect , mListViewWidth ,mListViewHeight
     * 这个方法必须保证在draw方法之前调用, 因为draw方法中首先就需要这个mIndexbarRect来绘制索引框
     */

    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        mListViewWidth = w;
        mListViewHeight = h;
        mIndexbarRect = new RectF(w - mIndexbarWidth - mIndexbarMargin,
                mIndexbarMargin, w - mIndexbarMargin, h - mIndexbarMargin);

    }

    /***
     * 绘制索引条和预览文本
     */
    public void draw(Canvas canvas) {
        // 1 绘制索引条, 包括索引条背景和文本
        // 只在某些状态才绘制
        if (mState == STATE_HIDDEN) {
            return;
        }

        // 设置画笔属性准备绘制索引条背景
        Paint indexbarPaint = new Paint();
        indexbarPaint.setColor(Color.BLACK);
        indexbarPaint.setAlpha((int) (64 * mAlphaRate)); // 半透明

        // 绘制索引条背景 (第二,第三个参数指定圆角半径)
        canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity,
                indexbarPaint);

        // 绘制好索引条背景后, 先绘制预览正方形, 再绘制索引条中的文本
        if (mSections != null && mSections.length > 0) {
            // 绘制预览框和文本
            // 如果显示索引条的同时, 选中了索引条中的某项则显示预览
            if (mCurrentSection >= 0) {
                Paint previewPaint = new Paint();
                previewPaint.setColor(Color.BLACK);
                previewPaint.setAlpha(96);

                Paint previewTextPaint = new Paint();
                previewTextPaint.setColor(Color.WHITE);
                previewTextPaint.setTextSize(50 * mScaledDensity); // 按密度比设置字体像素大小

                // 测量设置的单个字符的宽度
                float previewTextWidth = previewTextPaint
                        .measureText(mSections[mCurrentSection]);

                // 计算预览框高度 = 上下边距 + 文本基线以下长度 - 文本基线以上的长度(负数)

                // 文本基线以上是ascent() , 值为负数
                // 文本基线以下是descent(), 值为正数
                float previewSize = 2 * mPreviewPadding
                        + previewTextPaint.descent()
                        - previewTextPaint.ascent();

                // 定义预览背景正方形区域
                // 屏幕的宽度也就是listview的宽度 , 预览框是正方形 , 预览框宽度 = previewSize 计算高度

                // left = (listview的宽度 - 预览框的宽度) / 2
                // top = (屏幕高度 - 预览框的高度) / 2
                // right = (listview的宽度 - 预览框的宽度) / 2 + 预览框的宽度
                // bottom = (屏幕高度 - 预览框的宽度) / 2 + 预览框的高度

                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);

                // 绘制文本 , drawText(String text, float x, float y, Paint paint)
                // y -- 这个参数是文本基线的y轴坐标 = 边框top + 边距 + 基线以上部分(也就是减去assent 负数)
                canvas.drawText(mSections[mCurrentSection], previewRect.left
                        + (previewSize - previewTextWidth) / 2, previewRect.top
                        + mPreviewPadding - previewTextPaint.ascent(),
                        previewTextPaint);

            }
        }

        // 绘制索引条里面的文本

        Paint indexPaint = new Paint();
        indexPaint.setColor(Color.WHITE);
        indexPaint.setAlpha((int) (255 * mAlphaRate));
        indexPaint.setTextSize(12 * mScaledDensity); // 设置好了字符画笔就能获得字体的宽高

        // 每个索引项的可用高度
        float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin)
                / mSections.length;
        // paddingTop = (索引项的可用高度 - 文本的高度) /2
        float paddingTop = (sectionHeight - (indexPaint.descent() - indexbarPaint
                .ascent())) / 2;

        for (int i = 0; i < mSections.length; i++) {
            // indexPaint.measureText 获取字符的宽 , 然后用索引条的宽减去字符宽再除以2得到两侧边距
            float paddingLeft = (mIndexbarWidth - indexPaint
                    .measureText(mSections[i])) / 2;
            canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft,
                    mIndexbarRect.top + mIndexbarMargin + paddingTop
                            + sectionHeight * i - indexPaint.ascent(),
                    indexPaint);
        }
    }

    // 实现索引条的显示和状态切换
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(android.os.Message msg) {
            super.handleMessage(msg);

            switch (mState) {
            case STATE_SHOWING:
                mAlphaRate += (1 - mAlphaRate) * 0.2;
                if (mAlphaRate > 0.9) {
                    mAlphaRate = 1;
                    setState(STATE_SHOWN);
                }

                mListView.invalidate(); // 刷新父控件, 导致本控件draw方法不断重绘
                fade(10); // 消息处理中, 调用fade方法发送消息, 形成循环, 进行渐变处理
                break;
            case STATE_SHOWN:
                setState(STATE_HIDING);
                break;

            case STATE_HIDING:
                mAlphaRate -= mAlphaRate * 0.2;
                if (mAlphaRate < 0.1) {
                    mAlphaRate = 0;
                    setState(STATE_HIDDEN);
                }

                mListView.invalidate();
                fade(10);
                break;
            }
        };
    };

    private void fade(long delay) {
        mHandler.removeMessages(0); // 清除未执行完的消息
        mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
    }

    protected void setState(int state) {
        if (state < STATE_HIDDEN || state > STATE_HIDING)
            return;

        mState = state;

        switch (mState) {
        case STATE_HIDDEN: // 已经隐藏, 取消所有显示效果
            mHandler.removeMessages(0);
            break;
        case STATE_SHOWING: // 逐渐显示, 通过fade方法向handler发送消息
            mAlphaRate = 0;
            fade(0);
            break;
        case STATE_SHOWN: // 长期显示
            mHandler.removeMessages(0);
            break;
        case STATE_HIDING: // 逐渐隐藏
            mAlphaRate = 1;
            fade(3000);
            break;
        }

    }

    /**
     * 事件
     */

    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN: // 手指按下事件
            if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) { // 如果当前索引条已经显示,
                                                                            // 且手指是在索引条范围内
                setState(STATE_SHOWN); // 那么设置状态为STATE_SHOWN , 就不会自动隐藏索引条

                // mIsIndexing 赋值为真,表示当前控件是索引条 ,
                mIsIndexing = true;
                // 根据选中的索引项, 让listView选中 (显示)对应的item
                mCurrentSection = getSectionByPoint(ev.getY());
                mListView.setSelection(mIndexer
                        .getPositionForSection(mCurrentSection));
                return true;
            }
            break;
        case MotionEvent.ACTION_MOVE: // 手指在索引条上按下且滑动

            if (mIsIndexing) {
                if (contains(ev.getX(), ev.getY())) {
                    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;
    }

    /***
     * 根据事件Y坐标, 判断当前选中的文本
     * 
     * @param y
     * @return
     */
    private int getSectionByPoint(float y) {
        // 如果索引项为空则直接返回0
        if (mSections == null || mSections.length < 1) {
            return 0;
        }

        // 如果y坐标在索引条顶部范围外则返回0
        if (y < mIndexbarRect.top + mIndexbarMargin) {
            return 0;
        }

        // 如果Y 坐标超出索引条底部范围, 则返回最后一个下标
        if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin) {
            return mSections.length - 1;
        }

        // 计算位置得出下标且返回
        return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect
                .height() - 2 * mIndexbarMargin) / mSections.length));
    }

    /**
     * 判断手指按下的区域是否是索引条内
     * 
     * @param x
     * @param y
     * @return
     */
    public boolean contains(float x, float y) {
        // 这里对x 没有很严格的限制 , 因为索引条本身就太窄了, 而且距离屏幕右边的距离就很小, 所以不用考虑屏幕右边一点间距
        return x > mIndexbarRect.left && y > mIndexbarRect.top
                && y < mIndexbarRect.top + mIndexbarRect.height();
    }

    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);
        }
    }

}

//ListView , IndexScoller是绘制在这个Listview里面的

public class IndexableListView extends ListView {

    private boolean mIsFastScrollEnabled = false;
    private IndexScroller mIndexScroller = null;
    private GestureDetector mGestureDector = null;

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

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

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

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

    @Override
    public void setFastScrollEnabled(boolean enabled) {
        mIsFastScrollEnabled = true;
        if (mIsFastScrollEnabled) {
            // 设置允许快速滑动 , 初始化mIndexScroller
            if (mIndexScroller == null) {
                mIndexScroller = new IndexScroller(getContext(), this);
            }
        } else {
            // 设置不允许快速滑动, 则判断是否索引条存在则隐藏且销毁索引条
            if (mIndexScroller != null) {
                mIndexScroller.hide();
                mIndexScroller = null;
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas); // 调用父类draw方法绘制listview基础的部分
        // 调用scroller的draw方法绘制, 为了安全加个判断
        if (mIndexScroller != null) {
            mIndexScroller.draw(canvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 如果事件是在索引条的范围内则 直接调用scroller的onTouchEvent
        if (mIndexScroller != null && mIndexScroller.onTouchEvent(ev)) {
            return true;
        }

        // 针对快速滑动事件
        if (mGestureDector == null) {
            mGestureDector = new GestureDetector(getContext(),
                    new GestureDetector.SimpleOnGestureListener() {

                        @Override
                        public boolean onFling(MotionEvent e1, MotionEvent e2,
                                float velocityX, float velocityY) {
                            if (mIndexScroller != null) {
                                mIndexScroller.show();
                            }

                            return super.onFling(e1, e2, velocityX, velocityY);
                        }
                    });
        }
        mGestureDector.onTouchEvent(ev);
        return super.onTouchEvent(ev);
    }

    /***
     * 如果touchEvent是索引条的, 那么就阻断Listview的事件处理 让索引条处理onTouch
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mIndexScroller.contains(ev.getX(), ev.getY())) {
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /***
     * 作为ListView而言adapter需要完成的工作是列表项的具体显示, 如果是自定义的adapter类的话也就是里面的getView方法 ,
     * 也可以使用系统自带的布局文件 但是在这里的adapter不仅需要完成列表项的显示, 更重要的是实现 SectionIndexer 接口方法,
     * 提供了从section获取Position 等方法 因此这里在setAdapter 的时候, 需要把adapter传递给scroller核心类 ,
     * 然后在scroller中把这个SectionIndexer实现对象取出,
     * 于是scroller中就可以通过SectionIndexer读取到所有的section , 为listview设置选中的section 等操作
     * 
     */
    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);
        if (mIndexScroller != null) {
            mIndexScroller.setAdapter(adapter);
        }
    }

    /***
     * onSizeChanged 需要让scroller时时更新该listview的高和宽,且时时更新索引条的矩形区域
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mIndexScroller.onSizeChanged(w, h, oldw, oldh);
    }
}

// Activity , 包含了ListView需要的继承自arrayAdapter的内部类,且实现了SectionIndex接口

public class MainActivity extends Activity {

    private ArrayList<String> mItems;
    IndexableListView mListview;

    // private IndexableListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mItems = new ArrayList<String>();
        mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
        mItems.add("Steve Jobs");
        mItems.add("Inheritance (The Inheritance Cycle)");
        mItems.add("11/22/63: A Novel");
        mItems.add("The Hunger Games");
        mItems.add("The LEGO Ideas Book");
        mItems.add("Explosive Eighteen: A Stephanie Plum Novel");
        mItems.add("Catching Fire (The Second Book of the Hunger Games)");
        mItems.add("Elder Scrolls V: Skyrim: Prima Official Game Guide");
        mItems.add("Death Comes to Pemberley");
        mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
        mItems.add("Steve Jobs");
        mItems.add("Inheritance (The Inheritance Cycle)");
        mItems.add("11/22/63: A Novel");
        mItems.add("The Hunger Games");
        mItems.add("The LEGO Ideas Book");
        mItems.add("Explosive Eighteen: A Stephanie Plum Novel");
        mItems.add("Catching Fire (The Second Book of the Hunger Games)");
        mItems.add("Elder Scrolls V: Skyrim: Prima Official Game Guide");
        mItems.add("Death Comes to Pemberley");

        mItems.add("Shanghai");
        mItems.add("Beijing");
        mItems.add("Tianjing");
        mItems.add("Guangzhou");
        mItems.add("Suzhou");
        mItems.add("Nanjing");
        mItems.add("Hainan");
        mItems.add("Xizang");

        Collections.sort(mItems);

        mListview = (IndexableListView) findViewById(R.id.id_indexListView);

        ContentAdapter adapter = new ContentAdapter(this,
                android.R.layout.simple_list_item_1, mItems);

        mListview.setAdapter(adapter);

        mListview.setFastScrollEnabled(true);

    }

    class ContentAdapter extends ArrayAdapter<String> implements SectionIndexer {

        // 索引关键字字符串
        private String mSections = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";

        public ContentAdapter(Context context, int resource,
                List<String> objects) {
            super(context, resource, objects);
        }

        /***
         * 返回索引项数组 这里每个索引项都只有一个字符, 所以索引项数组的是单个字符串数组
         */
        @Override
        public Object[] getSections() {
            String[] sections = new String[mSections.length()];
            for (int i = 0; i < mSections.length(); i++) {
                sections[i] = String.valueOf(mSections.charAt(i)); // 每个索引项section都是一个单个的字符串
            }
            return sections;
        }

        /***
         * 根据给出的索引项, 查找匹配的列表项, 并返回第一个匹配的列表项位置
         */

        @Override
        public int getPositionForSection(int sectionIndex) {

            // i -- 索引项下标, 如果当前索引项没有匹配的列表项, 那么则查找前一个索引项

            for (int i = sectionIndex; i >= 0; i--) {
                // 遍历ArrayList中所有item , j 是列表项的下标
                for (int j = 0; j < getCount(); j++) {
                    if (i == 0) { // 查找数字开头的
                        // 从0到9 , 只要找到第一个匹配的就返回该列表项
                        for (int k = 0; k <= 9; k++) {
                            if (StringMatcher.match(getItem(j),
                                    String.valueOf(k))) {
                                return j;
                            }
                        }

                    } else {
                        // 匹配索引项字母开头的
                        String key = String.valueOf(mSections.charAt(i));
                        if (StringMatcher.match(getItem(j), key)) {
                            return j;
                        }
                    }
                }
            }
            return 0;
        }

        @Override
        public int getSectionForPosition(int position) {
            return 0;
        }

    }
}

//StringMatcher 比较字符串前缀是否匹配

public class StringMatcher {

    public static boolean match(String value, String key) {

        if (value == null || key == null)
            return false;

        if (value.length() < key.length()) {
            return false;
        }

        int i = 0; // value 位置指针
        int j = 0; // key 位置指针

        // 思路是如果key是value开头的一部分的话, 那么i, j记录的是比对过的位数 , i == j

        do {
            // 逐位比对 , key 一定是位数比较短的字符串

            if (value.charAt(i) == key.charAt(j)) {
                i++;
                j++;
            } else if (j > 0) {
                // 如果value和key 的当前比对字符不相等的话, 但是j>0 , 表示前面比对过前缀, 且前面比对过的的部分是相等的,
                // 只是到了第j位字符不同, 于是这时前缀匹配成功, 应该退出比对.

                break;

            } else {
                // 前缀匹配, 中间不匹配会执行到上面j>0后退出循环
                // 只有一开始就不匹配才会执行到这个地方 ,不匹配于是 i, j 初始就是0, 是相等的, 不能直接退出,
                // 需要给i++ , 表示已经匹配比较过第i位字符 , 因为我们只比较前缀,可以直接退出了.. 如果需要比较中间匹配的话, 那这里就不要break , 
                // 最后返回的时候判断 i>j 则表示, value里面包含key
                i++;
                break;
            }

        } while (i < value.length() && j < key.length());

        return (i == j) ? true : false;
    }
}

//布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.example.hendry_indexable_listview.IndexableListView
        android:id="@+id/id_indexListView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值