Android自定义A_Z字母排序ListView,悬停Listview
近期项目有个功能,要开发一个通讯录,什么样的通讯录呢:
1.带字母分组的悬停
2.右边带字母索引的侧边栏
3.与顶部搜索框进行动态变化
4.点击Item可以直接拨电话、复制、发邮件
想想一个通讯录要求咋这么多呢,不过码农还是得干啊,先来一张成品图
先来分析一波UI设计的剧本:
1.顶部一个搜索框,点击后,当listview滑动且输入框无内容时,它的提示图片和文字要显示出来
2.侧边字母表要与listview相呼应,点击某个字母,这个字母要放大加圈显示,且listivew要定位到对应的分组
3.listview在输入框下方,需要做分组悬停,且与侧边字母表对应
1.那就先做吧。先从顶部EditText做起,定义一个类EditTextWithImg
public class EditTextWithImg extends AppCompatEditText { private Bitmap searchBitmap; private float txtSize; private int txtColor; private String txt; private int height;//控件本身的高度 private int width;//控件本身的宽度 private float searchLeft;//搜索图片 距离控件左边的距离 private float searchTop ;//搜索图片距离控件顶部的距离 private Paint txtPaint; private boolean isInput;//是否是手动开始输入 private boolean isNeedDraw ;//避免重复绘制 private InputListener inputListener; public void setInputListener(InputListener inputListener) { this.inputListener = inputListener; } /** * 当listview进行滑动时,要判断下输入框有无内容,没有就恢复初始状态 */ public void setInput() { if (TextUtils.isEmpty(getText().toString().trim()) && isNeedDraw) { setInputMode(false); if (inputListener != null) { inputListener.inputOver(); } } } /** * 设置edittext是否获取焦点 * @param isInput */ public void setInputMode(Boolean isInput){ this.isInput = isInput; isNeedDraw = isInput; setFocusable(isInput); setFocusableInTouchMode(isInput); invalidate(); } public EditTextWithImg(Context context, AttributeSet attrs) { super(context, attrs); initResource(attrs); initPaint(); } public EditTextWithImg(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initResource(attrs); initPaint(); } private void initResource(AttributeSet attrs) { //自定义的一些属性 提示文字大小,颜色,内容 TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.etimg); float density = getResources().getDisplayMetrics().density;//像素密度 txtSize = typedArray.getDimension(R.styleable.etimg_edit_txtsize,11*density+0.5f); txtColor = typedArray.getColor(R.styleable.etimg_edit_txtcolor,0x000000); txt = typedArray.getString(R.styleable.etimg_edit_txt); typedArray.recycle(); } private void initPaint() { txtPaint = new Paint(); txtPaint.setTextSize(txtSize); txtPaint.setColor(txtColor); txtPaint.setAntiAlias(true); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (searchBitmap == null) searchBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.img_search); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); height = getHeight(); width = getWidth(); searchLeft = width / 2 - searchBitmap.getWidth() - dpToPx(1); searchTop = height / 2 - searchBitmap.getHeight() / 2 + dpToPx(1); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //当没有点击输入框且输入框无内容时,绘制初始状态 if (!isInput && TextUtils.isEmpty(getText().toString().trim())) { canvas.drawText(txt, width / 2, height / 2 + txtSize / 2, txtPaint); if (searchBitmap != null) { canvas.drawBitmap(searchBitmap, searchLeft, searchTop, null); } } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (searchBitmap != null && !searchBitmap.isRecycled()) searchBitmap.recycle(); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //当手开始点击时,获取焦点,清空初始状态 setInputMode(true); if (inputListener != null) { inputListener.inputBegin(); } break; } return super.onTouchEvent(event); } private int dpToPx(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } public interface InputListener{ void inputBegin(); void inputOver(); } }
具体都写了注释,大概流程是
1.先绘制初始时的状态,即显示提示语(搜索)和搜索图标;
2.当手开始点击进行输入时,让控件获取焦点,去除提示语和搜索图标
3.当Listview进行滑动时,判断下控件里有没有内容,没有的话就恢复初始状态
2.再来定义一个类绘制侧边字母表SideLetterBar
public class SideLetterBar extends View { private static final String[] index = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z","#"}; private static LinkedHashMap<String,Integer> indexMap = new LinkedHashMap<>(); static { indexMap.put("A",0);indexMap.put("B",1); indexMap.put("C",2);indexMap.put("D",3); indexMap.put("E",4);indexMap.put("F",5); indexMap.put("G",6);indexMap.put("H",7); indexMap.put("I",8);indexMap.put("J",9); indexMap.put("K",10);indexMap.put("L",11); indexMap.put("M",12);indexMap.put("N",13); indexMap.put("O",14);indexMap.put("P",15); indexMap.put("Q",16);indexMap.put("R",17); indexMap.put("S",18);indexMap.put("T",19); indexMap.put("U",20);indexMap.put("V",21); indexMap.put("W",22);indexMap.put("X",23); indexMap.put("Y",24);indexMap.put("Z",25); indexMap.put("#",26); } private int choose = -1;//当前选中的字母的位置 private Paint paint = new Paint(); private Paint paintCircle = new Paint(); private OnLetterChangedListener onLetterChangedListener; private TextView overlay; private float circleradius ; private int circleColor ; private float textSize; private float textSize_Big; private int textColor; private int textChooseColor; private int height;//控件高度 private int width;//控件宽度 private int singleHeight;//每个字母所占的高度 public SideLetterBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public SideLetterBar(Context context, AttributeSet attrs) { super(context, attrs); } public SideLetterBar(Context context) { super(context); } /** * 设置悬浮的textview * @param overlay */ public void setOverlay(TextView overlay){ this.overlay = overlay; } /** * listview拖动时候 顶部第一个item定位到对应的字母 * @param letter */ public void drawA_ZCircle(String letter){ if (TextUtils.isEmpty(letter)) return; choose = indexMap.get(letter); invalidate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); height = getHeight(); width = getWidth(); singleHeight = height / index.length; circleradius = getResources().getDimension(R.dimen.side_letter_bar_clrcle_size); circleColor = getResources().getColor(R.color.letter_clrclr); textSize = getResources().getDimension(R.dimen.side_letter_bar_letter_size); textSize_Big = getResources().getDimension(R.dimen.side_letter_bar_letter_bigsize); textColor = getResources().getColor(R.color.letter_text); textChooseColor = getResources().getColor(R.color.white); } @SuppressWarnings("deprecation") @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < index.length; i++) { paint.setAntiAlias(true); if (i == choose) { //对选中字母进行单独绘制 paint.setColor(textChooseColor); paint.setTextSize(textSize_Big); paintCircle.setColor(circleColor); paintCircle.setAntiAlias(true); paintCircle.setStyle(Paint.Style.FILL); canvas.drawCircle(width / 2, singleHeight * (choose + 1) - singleHeight / 3, circleradius, paintCircle); } else { paint.setTextSize(textSize); paint.setColor(textColor); } float xPos = width / 2 - paint.measureText(index[i]) / 2; float yPos = singleHeight * i + singleHeight; canvas.drawText(index[i], xPos, yPos, paint); paint.reset(); } } /** * 因为在Listview上方,所以当点击事件来的时候,直接return ,不进行向下分发 * @param event * @return */ @Override public boolean dispatchTouchEvent(MotionEvent event) { final int action = event.getAction(); final float y = event.getY(); final int c = (int) (y / singleHeight); final int oldChoose = choose; switch (action) { case MotionEvent.ACTION_DOWN: if (oldChoose != c && onLetterChangedListener != null) { if (c >= 0 && c < index.length) { changeLetterBar(c); } } break; case MotionEvent.ACTION_MOVE: if (oldChoose != c && onLetterChangedListener != null) { if (c >= 0 && c < index.length) { changeLetterBar(c); } } break; case MotionEvent.ACTION_UP: choose = c; invalidate(); if (overlay != null){ overlay.setVisibility(GONE); } break; } return true; } private void changeLetterBar(int letterPosition) { onLetterChangedListener.onLetterChanged(index[letterPosition]); choose = letterPosition; invalidate(); //设置悬浮view if (overlay != null){ overlay.setVisibility(VISIBLE); overlay.setText(index[letterPosition]); } } public void setOnLetterChangedListener(OnLetterChangedListener onLetterChangedListener) { this.onLetterChangedListener = onLetterChangedListener; } public interface OnLetterChangedListener { void onLetterChanged(String letter); } }
主要流程就是
1.初始没有字母被选中,就在onDraw里把每个字母绘制在侧边栏
2.当手点击某一个字母时,因为字母表在相对布局中是处于listview立体空间中的上方,所以点击事件在dispatchTouchEvent里进行处理,主要是return true不让事件进行分发,然后确定好选中字母的位置,重新在onDraw里进行绘制
3.当listview进行拖动时候,会通过drawA_ZCircle方法通知该view,在indexMap里找到listview的顶部第一个item的首字母的位置,然后再重新绘制
3.接下里就是重写Listview了
public class SortListView extends ListView implements AbsListView.OnScrollListener { private final String TAG = "SortListView"; private TextView tv_hoverTitle;//顶部悬停view private EditTextWithImg editText;//顶部搜索view private SideLetterBar side_letterbar;//侧边字母表 private String lastPy; private boolean isScroll;//是否滑动listview private boolean isChackLetterBar;//是否手动选中侧边字母表 public SortListView(Context context) { super(context); initListener(); } public SortListView(Context context, AttributeSet attrs) { super(context, attrs); initListener(); } public SortListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initListener(); } private void initListener(){ setOnScrollListener(this); } public void setTv_hoverTitle(TextView tv_hoverTitle) { this.tv_hoverTitle = tv_hoverTitle; } public void setEditText(EditTextWithImg editText) { this.editText = editText; } public void setSide_letterbar(final SideLetterBar side_letterbar) { this.side_letterbar = side_letterbar; side_letterbar.setOnLetterChangedListener(new SideLetterBar.OnLetterChangedListener() { @Override public void onLetterChanged(String letter) { isChackLetterBar = true; lastPy = letter; //设置顶部悬停view tv_hoverTitle.setVisibility(View.VISIBLE); tv_hoverTitle.setText(letter); //设置listview位置 PeopleAdapter peopleAdapter = (PeopleAdapter) getAdapter(); int position = peopleAdapter.getLetterPosition(letter); setSelection(position); //然后绘制侧边字母表选中状态 side_letterbar.drawA_ZCircle(letter); } }); } @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN : //当手开始触摸屏幕时候才进行联动 避免刚初始化就绘制报错 isScroll = true; break; case MotionEvent.ACTION_MOVE : //当手开始滑动屏幕时解除手动选中字母表行为 isChackLetterBar = false; break; } return super.onTouchEvent(ev); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //当滑动时,如果edittext没有输入内容 就清空焦点,恢复初始状态 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL && editText != null && TextUtils.isEmpty(editText.getText().toString().trim())) { editText.setInput(); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (!isScroll) return; if (isChackLetterBar) return; if (side_letterbar == null) return; if (tv_hoverTitle == null) return; if (getChildAt(0) == null) return; People p = (People) getAdapter().getItem(firstVisibleItem); if (p == null) return; String py = p.getPinyin(); int top = Math.abs(getChildAt(0).getTop());//选取当前屏幕可见的第一个item距离顶部距离 if (top == 0) {//一开始就设置显示,防止闪烁情况 tv_hoverTitle.setText(py.toUpperCase()); tv_hoverTitle.setVisibility(View.VISIBLE); } else { //页面第一次进来后再次滑动,后面的itemview gettop值总不为0 if (!py.equals(lastPy)) { tv_hoverTitle.setText(py.toUpperCase()); tv_hoverTitle.setVisibility(View.VISIBLE); } } //滑动时判断当前屏幕第一个item是什么字母开头的,然后绘制侧边字母表选中状态 if (!py.equals(lastPy) ) {//避免重复绘制 if (TextUtils.equals("#", py)) { side_letterbar.drawA_ZCircle(py); } else { side_letterbar.drawA_ZCircle(py.toUpperCase()); } } lastPy = py; } }
listview是需要跟其它两个view进行互动的,主要流程就是
1.在onScrollStateChanged里进行判断,如果开始滑动了,就通知下Edittext状态是否需要变化
2.在onScroll里获取第一个可见的item的对象和view,判断侧边字母表需要标注哪个字母,listview顶部悬停分组该是哪个字母
3.在侧边字母表的的拖动监听里,设置相应的悬停字母,listview的position
这三个写完就结束了,如何进行使用可以到我的Github下载Demo
没有梯子的可以在这个地址下载