Android自定义A_Z字母排序ListView,悬停Listview

近期项目有个功能,要开发一个通讯录,什么样的通讯录呢:

1.带字母分组的悬停

2.右边带字母索引的侧边栏

3.与顶部搜索框进行动态变化

4.点击Item可以直接拨电话、复制、发邮件

想想一个通讯录要求咋这么多呢,不过码农还是得干啊,先来一张成品图


Android自定义A_Z字母排序ListView,悬停Listview


Android自定义A_Z字母排序ListView,悬停Listview


先来分析一波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

没有梯子的可以在这个地址下载