AndroidUI效果--开源项目IndexableListView(字母索引)
开发通讯录相关的应用的时候可能会需要这种效果,索引这种效果根据人性化和美观,我下了一个关于字母索引的Demo,里面很好实现了这种效果,不过这只是个Demo,在实际的项目当中,可能会增加分组效果,我们可能在这个基础上进行扩展。
下面我把源代码贴一下,我下下来的项目是完全没有注释的,我稍微研究了一下代码,并加上了注释,希望对大伙有帮助。
布局
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <com.woozzu.android.widget.IndexableListView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:id="@+id/listview" />
- </LinearLayout>
Activity
- package com.woozzu.android.indexablelistview;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- import android.app.Activity;
- import android.content.Context;
- import android.os.Bundle;
- import android.widget.ArrayAdapter;
- import android.widget.SectionIndexer;
- import com.woozzu.android.util.StringMatcher;
- import com.woozzu.android.widget.IndexableListView;
- public class IndexableListViewActivity extends Activity {
- private ArrayList<String> mItems;
- private IndexableListView mListView;
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.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("做作");
- mItems.add("wokao");
- Collections.sort(mItems); // 排序
- ContentAdapter adapter = new ContentAdapter(this,
- android.R.layout.simple_list_item_1, mItems);
- mListView = (IndexableListView) findViewById(R.id.listview);
- mListView.setAdapter(adapter);
- mListView.setFastScrollEnabled(true); // 设置快速滑动
- }
- private class ContentAdapter extends ArrayAdapter<String> implements
- SectionIndexer {
- private String mSections = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";
- public ContentAdapter(Context context, int textViewResourceId,
- List<String> objects) {
- super(context, textViewResourceId, objects);
- }
- @Override
- public int getPositionForSection(int section) {
- // If there is no item for current section, previous section will be
- // selected
- // 如果当前部分没有item,则之前的部分将被选择
- for (int i = section; i >= 0; i--) {
- for (int j = 0; j < getCount(); j++) {
- System.out.println(getCount());
- if (i == 0) { // #
- // For numeric section 数字
- for (int k = 0; k <= 9; k++) {// 1...9
- // 字符串第一个字符与1~9之间的数字进行匹配
- if (StringMatcher.match(
- String.valueOf(getItem(j).charAt(0)),
- String.valueOf(k)))
- return j;
- }
- } else { // A~Z
- if (StringMatcher.match(
- String.valueOf(getItem(j).charAt(0)),
- String.valueOf(mSections.charAt(i))))
- return j;
- }
- }
- }
- return 0;
- }
- @Override
- public int getSectionForPosition(int position) {
- return 0;
- }
- @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));
- return sections;
- }
- }
- }
字符串匹配工具类
- /*
- * Copyright 2011 woozzu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.wwj.indexableListView.util;
- public class StringMatcher {
- // 这些变量是韩文,小巫也不知道是什么意思,有谁懂的马上联系我啊
- private final static char KOREAN_UNICODE_START = '가'; // 韩文字符编码开始?
- private final static char KOREAN_UNICODE_END = '힣'; // 韩文字符编码结束?
- private final static char KOREAN_UNIT = '까' - '가'; // 不知道是啥?
- // 韩文的一些字符初始化
- private final static char[] KOREAN_INITIAL = { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ',
- 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ',
- 'ㅎ' };
- /**
- * 字符匹配
- * @param value 需要keyword匹配的字符串
- * @param keyword #ABCDEFGHIJKLMNOPQRSTUVWXYZ中的一个
- * @return
- */
- public static boolean match(String value, String keyword) {
- if (value == null || keyword == null)
- return false;
- if (keyword.length() > value.length())
- return false;
- int i = 0, j = 0;
- do {
- // 如果是韩文字符并且在韩文初始数组里面
- if (isKorean(value.charAt(i)) && isInitialSound(keyword.charAt(j))) {
- if (keyword.charAt(j) == getInitialSound(value.charAt(i))) {
- i++;
- j++;
- } else if (j > 0)
- break;
- else
- i++;
- } else {
- // 逐个字符匹配
- if (keyword.charAt(j) == value.charAt(i)) {
- i++;
- j++;
- } else if (j > 0)
- break;
- else
- i++;
- }
- } while (i < value.length() && j < keyword.length());
- // 如果最后j等于keyword的长度说明匹配成功
- return (j == keyword.length()) ? true : false;
- }
- // 判断字符是否在韩文字符编码范围内
- private static boolean isKorean(char c) {
- if (c >= KOREAN_UNICODE_START && c <= KOREAN_UNICODE_END)
- return true;
- return false;
- }
- // 判断是否在韩文字符里面
- private static boolean isInitialSound(char c) {
- for (char i : KOREAN_INITIAL) {
- if (c == i)
- return true;
- }
- return false;
- }
- // 获得韩文初始化字符数组里面的一个字符
- private static char getInitialSound(char c) {
- return KOREAN_INITIAL[(c - KOREAN_UNICODE_START) / KOREAN_UNIT];
- }
- }
自定义索引列表
- /*
- * Copyright 2011 woozzu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.wwj.indexableListView.widget;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.util.AttributeSet;
- import android.view.GestureDetector;
- import android.view.MotionEvent;
- import android.widget.ListAdapter;
- import android.widget.ListView;
- /**
- * 自定义索引列表
- *
- * @author by 佚名
- *
- */
- 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) {
- // 创建一个GestureDetector(手势探测器)
- 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
- // 显示索引条
- mScroller.show();
- return super.onFling(e1, e2, velocityX, velocityY);
- }
- });
- }
- mGestureDetector.onTouchEvent(ev);
- return super.onTouchEvent(ev);
- }
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return true;
- }
- @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);
- }
- }
索引条
- /*
- * Copyright 2011 woozzu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.wwj.indexableListView.widget;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.graphics.RectF;
- import android.os.Handler;
- import android.os.Message;
- import android.os.SystemClock;
- import android.view.MotionEvent;
- import android.widget.Adapter;
- import android.widget.ListView;
- import android.widget.SectionIndexer;
- /**
- * 右侧的索引条
- *
- * @author by 佚名
- *
- */
- 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; // ListView宽度
- private int mListViewHeight; // ListView高度
- private int mCurrentSection = -1; // 当前部分
- private boolean mIsIndexing = false; // 是否正在索引
- private ListView mListView = null;
- private SectionIndexer mIndexer = null;
- private String[] mSections = null;
- private RectF mIndexbarRect;
- // 4种状态(已隐藏、正在显示、已显示、正在隐藏)
- 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;
- 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
- // 隐藏3秒钟
- mAlphaRate = 1;
- fade(3000);
- break;
- }
- }
- private 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;
- }
- }
- };
- }