android 仿微信联系人 首字母分组快速索引

微信风格通讯录模板:带字母索引的联系人列表+可点击详情页 直接可用的微信H5通讯录界面方案,严格遵循WeUI设计规范,适配iOS和安卓主流机型。首页index.html提供带右侧字母快速导航栏的联系人列表,支持按首字母分组、顶部搜索框触发筛选;点击任一联系人跳转details.html,展示头像、姓名、部门、职位、手机号、邮箱等标准信息字段,并预留扩展区域。资源包内置weui.css官方样式库、zepto.min.js轻量交互脚本、全套WeUI导航图标(含搜索栏、cell、toast、dialog等共20+个png图标)、示例图片(vcode.jpg、pic.gi 阅读详情

总结是一种习惯,不能停,一停人就懒了,都快一个月没有写了!该提提神了!

进入正题:android 仿微信联系人 首字母快速索引,先用下美团的索引效果图:

            

 
1、自定义View字母索引栏(右边那一列):
public class QuickIndexBar extends View {
    private final String[] LETTERS = new String[]{
            "↑", "#", "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 Paint mPaint;
    //单元格高
    private float cellHeight;
    //单元格宽
    private float cellWidth;


    private OnLetterUpdateListener mOnLetterUpdateListener;

    /**
     * 设置字母监听
     *
     * @param onLetterUpdateListener
     */
    public void setOnLetterUpdateListener(OnLetterUpdateListener onLetterUpdateListener) {
        mOnLetterUpdateListener = onLetterUpdateListener;
    }

    public QuickIndexBar(Context context) {
        this(context, null);
    }

    public QuickIndexBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QuickIndexBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //去锯齿
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.DKGRAY);
        //粗体
        mPaint.setTypeface(Typeface.DEFAULT_BOLD);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < LETTERS.length; i++) {
            String text = LETTERS[i];
            //计算xy坐标
            //获取文本的宽度mPaint.measureText(text)
            int x = (int) (cellWidth / 2.0f - mPaint.measureText(text) / 2.0f);
            //获取文本的高度
            Rect rect = new Rect();//文本所在的矩形,创建空矩形
            mPaint.getTextBounds(text, 0, text.length(), rect);//填充文本
            int textHeight = rect.height();//获取文本高度
            int y = (int) (cellHeight / 2.0f + textHeight / 2.0f + cellHeight * i);
           <span style="color:#ff0000;"> //当字母被选中,使用不同颜色画出来,根据需求改变画笔的属性就可以了
            if (i == touchIndex){
                mPaint.setColor(Color.GREEN);
                mPaint.setFakeBoldText(true);
            }</span>
             canvas.drawText(LETTERS[i], x, y, mPaint);
           <span style="color:#ff0000;"> mPaint.reset();</span>
        }
    }

    /**
     * This is called during layout when the size of this view has changed. If
     * you were just added to the view hierarchy, you're called with the old
     * values of 0.
     *
     * @param w    Current width of this view.
     * @param h    Current height of this view.
     * @param oldw Old width of this view.
     * @param oldh Old height of this view.
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //测量出view的宽
        cellWidth = getMeasuredWidth();
        //测量view高度除以格子总数
        cellHeight = getMeasuredHeight() / LETTERS.length;
    }
   <span style="color:#ff0000;"> /**</span>
<span style="color:#ff0000;">    * 这段代码实现外部列表滑动时,监听可视首项改变,用来改变画笔颜色进行突出</span>
<span style="color:#ff0000;">    */</span>
    <span style="color:#ff0000;">//被选中项
    int touchIndex = -1;

    public int getTouchIndex() {
        return touchIndex;
    }
    //设置被选中项
    public void setTouchIndex(int touchIndex) {
        this.touchIndex = touchIndex;
        invalidate();
    }</span>

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int index;
        switch (MotionEventCompat.getActionMasked(event)) {
            case MotionEvent.ACTION_DOWN:
                //所在view中点击的Y坐标(屏幕中的绝对坐标),换算成位置
                index = (int) (event.getY() / cellHeight);
                if (index >= 0 && index < LETTERS.length) {
                    if (index != touchIndex) {
                        if (mOnLetterUpdateListener != null) {
                            mOnLetterUpdateListener.onLetterUpdateListener(LETTERS[index]);
                            touchIndex = index;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                index = (int) (event.getY() / cellHeight);
                if (index >= 0 && index < LETTERS.length) {
                    <span style="color:#ff0000;">if (index != touchIndex) {
                        if (mOnLetterUpdateListener != null) {
                            mOnLetterUpdateListener.onLetterUpdateListener(LETTERS[index]);
                            touchIndex = index;
                        }
                    }</span>
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        //重绘,被选中字母颜色变化
        invalidate();
        //拦截事件并处理
        return true;
    }

    <span style="color:#cc0000;">/**
     * 对外暴露字母监听
     */
public interface OnLetterUpdateListener {
    void onLetterUpdateListener(String letter);
}
}</span>
这边注释得很详细,需要注意的地方,我都进行标红了!

2、布局:
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <android.support.v7.widget.RecyclerView
       android:id="@+id/rv_persons"
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>
  
    <com.example.views.QuickIndexBar
        android:id="@+id/qib_bar"
        android:layout_width="40dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:background="@color/gray_half"
        />
</RelativeLayout>
这边本人使用了 RecyclerView 替换 listview
不过并不建议这么做,因为在点击索引栏定位时:
rv_persons.getLayoutManager().scrollToPosition(i);
recycleView 索引时是从下往上滚,只要符合要求的项为可视项就不在进行滚动,所以当你点击的字母栏字母,满足
item 在 目前可视项下方,recycleView 从下往上滚就会出现在当前界面最后一项,也就是底部项!
所以还是使用listview,这边最好的选择是使用 ExpandableListView ,以方便后期的扩展,虽然数据进行多次分组会很麻烦!


3、字符串转换成拼音工具类:

这边需要用到 pinyin4j-2.5.0.jar:下载地址:jar包下载地址

import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;

/**
 * Created by f on 2015-12-01.
 * 根据汉子文本获取拼音首字母
 */
public class PinYinUtil {
    /**
     * 将汉语拼音转化成字母
     *
     * @param str 字符串
     * @return 字母串
     */
    public static String getPinyin(String str) {
        //输出格式
        HanyuPinyinOutputFormat mFormat = new HanyuPinyinOutputFormat();
        //输出为大写字母
        mFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        //去掉音标
        mFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        StringBuilder sb = new StringBuilder();
        char[] chars = str.toCharArray();
        for (char c : chars) {
            //如果是空格跳过
            if (Character.isWhitespace(c)) {
                continue;
            }
            //判断是否为汉字
            if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
                String s = "";
                try {
                    //通过char得到拼音集合,单->dan或者是shan
                    s = PinyinHelper.toHanyuPinyinStringArray(c, mFormat)[0];
                    sb.append(s);
                } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
                    badHanyuPinyinOutputFormatCombination.printStackTrace();
                    sb.append(s);
                }
            } else if (Character.toString(c).matches("^[a-zA-Z]*")) {//判断是否是英语,是保留
                sb.append(c);
            } else {    //不是汉字不是英语的,保留下来的,使用“#”代替,归为一类
                sb.append("#");
            }
        }
        return sb.toString();
    }

}
这边根据自己分组需求,进行整改!

4、Java bean类:

import com.example.utils.PinYinUtil;
import java.io.Serializable;

/**
 * 
 * 联系人界面bean,实现Comparable<Persons>接口对每个类的对象进行整体排序
 */
public class Persons <span style="color:#cc0000;">implements Comparable<Persons>{</span>
   
    private String name;
    private String pinyin;
    private int imageId;

    public Persons() {
    }

    public Persons(int imageId, String name, String status) {
        this.imageId = imageId;
        this.name = name;
        this.status = status;
        this.pinyin = PinYinUtil.getPinyin(name);
    }

   
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }

    public String getPinyin() {
        return pinyin;
    }

    public void setPinyin(String pinyin) {
        this.pinyin = pinyin;
    }


    /**
     * 重写父类方法,对比pinyin进行自然排序
     *
     * @param another Persons
     * @return 该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
     */
    @Override
    public int compareTo(Persons another) {
        return this.pinyin.compareTo(another.getPinyin());
    }

    @Override
    public String toString() {
        return "Persons{" +
                "name='" + name + '\'' +
                ", pinyin='" + pinyin + '\'' +
                ", imageId=" + imageId +
                '}';
    }
}
5、例子名单:
public static final String[] NAMES = new String[]{"宋江", "卢俊义", "吴用",
            "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智深",
            "武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "穆弘",
            "雷横", "李俊", "阮小二", "张横", "阮小五", " 张顺", "阮小七", "杨雄", "石秀", "解珍",
            " 解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪",
            "魏定国", "萧让", "裴宣", "欧鹏", "邓飞", " 燕顺", "杨林", "凌振", "蒋敬", "吕方",
            "郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "项充",
            "李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿",
            "陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永", "施恩",
            "周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "李立",
            "李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", " 王定六", "郁保四", "白胜",
            "时迁", "段景柱", "&张三", "11级李四", "12级小明"};

6、适配器:

/**
 *
 * 联系人列表适配器
 */
public class PersonRVAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<Persons> mPersonsList;
    private static final int TYPE_HEADER = 0;
    private static final int TYPE_ITEM = 1;
    private static final int TYPE_FOOTER = 2;

    public PersonRVAdapter(List<Persons> personsList) {
        if (personsList == null) {
            throw new IllegalArgumentException(
                    "mRecentList must not be null");
        }
        mPersonsList = personsList;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_HEADER;
        } else if (position + 1 == getItemCount()) {
            return TYPE_FOOTER;
        } else {
            return TYPE_ITEM;
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {//头部项
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.contacts_rv_header, parent, false);
            HeaderViewHolder itemViewHolder = new HeaderViewHolder(itemView);
            return itemViewHolder;
        } else if (viewType == TYPE_ITEM) {//正常项
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.contacts_rv_item, parent, false);
            ItemViewHolder itemViewHolder = new ItemViewHolder(itemView);
            itemViewHolder.setIsRecyclable(true);
            return itemViewHolder;
        } else if (viewType == TYPE_FOOTER) {
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.contacts_rv_footer, parent, false);
            FooterViewHolder itemViewHolder = new FooterViewHolder(itemView);
            return itemViewHolder;
        }
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder != null) {
            if (holder instanceof HeaderViewHolder) {
                ((HeaderViewHolder) holder).iv_header_image.setBackgroundResource(R.drawable.person);
                ((HeaderViewHolder) holder).tv_header_what.setText("0.0");
            } else if (holder instanceof ItemViewHolder) {
                Persons persons = mPersonsList.get(position);
                String str = null;
                //当前联系人姓名首字母
                char c = persons.getPinyin().charAt(0);
                String currentLetter = c + "";
                if (position == 2) {
                    str =currentLetter;
                } else {
                    String lastLetter = mPersonsList.get(position - 1).getPinyin().charAt(0) + "";
                    if (!TextUtils.equals(currentLetter, lastLetter)) {
                        str = currentLetter;
                    }
                }
                //根据str是否为空与是否为字母决定是否显示字母栏
                if ((c >= 'A' && str != null && c <= 'Z') || position == 2) {
                    ((ItemViewHolder) holder).tv_letter.setVisibility(View.VISIBLE);
                    ((ItemViewHolder) holder).tv_letter.setText(str);
                } else {
                    ((ItemViewHolder) holder).tv_letter.setVisibility(View.GONE);
                }
                ((ItemViewHolder) holder).iv_person_header.setBackgroundResource(R.mipmap.ic_launcher);
                ((ItemViewHolder) holder).tv_person_name.setText(persons.getName());
            } else if (holder instanceof FooterViewHolder) {

            }
        }
    }


    @Override
    public int getItemCount() {
        return mPersonsList.size();
    }

    /**
     * ItemViewHolder
     */
    public static final class ItemViewHolder extends RecyclerView.ViewHolder {
        //首字母
        public TextView tv_letter;
        //头像
        public ImageView iv_person_header;
        //from who
        public TextView tv_person_name;

        public ItemViewHolder(View itemView) {
            super(itemView);
            iv_person_header = (ImageView) itemView.findViewById(R.id.iv_person_header);
            tv_person_name = (TextView) itemView.findViewById(R.id.tv_person_name);
            tv_letter = (TextView) itemView.findViewById(R.id.tv_letter);

        }
    }

    /**
     * HeaderViewHolder
     */
    public static final class HeaderViewHolder extends RecyclerView.ViewHolder {
        //功能logo
        public ImageView iv_header_image;
        //功能名
        public TextView tv_header_what;

        public HeaderViewHolder(View itemView) {
            super(itemView);
            iv_header_image = (ImageView) itemView.findViewById(R.id.iv_header_image);
            tv_header_what = (TextView) itemView.findViewById(R.id.tv_header_what);
        }
    }
    /**
     * FooterViewHolder
     */
    public static final class FooterViewHolder extends RecyclerView.ViewHolder {
        //功能logo
        public ImageView iv_footer_image;
        //功能名
        public TextView tv_footer_what;
        //字母栏
        public TextView tv_letter_footer;
        public FooterViewHolder(View itemView) {
            super(itemView);
            iv_footer_image = (ImageView) itemView.findViewById(R.id.iv_footer_image);
            tv_footer_what = (TextView) itemView.findViewById(R.id.tv_footer_what);
            tv_letter_footer= (TextView) itemView.findViewById(R.id.tv_letter_footer);

        }
    }

}


7、联系人界面:
/**
 * 联系人界面
 */

public class ContactsFragment extends Fragment {


    private OnFragmentInteractionListener mListener;
    //索引view
    private QuickIndexBar qib_bar;
    //联系人列表
    private List<Persons> mPersonsList;
    //索引弹窗
    private PopupWindow mPopupWindow;
    //handler 处理者
    private Handler mHandler = new Handler();
    //弹窗异步关闭
    private Thread mThread = new Thread() {
        @Override
        public void run() {
           <span style="color:#ff0000;"> if (mPopupWindow != null)
                mPopupWindow.dismiss();
            mPopupWindow = null;</span>
        }
    };
    //联系人列表
    private RecyclerView rv_persons;
    //recycleView布局类型
    private LinearLayoutManager mLayoutManager;
    //recycleView适配器
    private PersonRVAdapter mRVAdapter;
    //当前可见首项
    private int currentFirstIndex = -1;
    //下拉刷新
    private SwipeRefreshLayout srl_contacts;

    public static ContactsFragment newInstance(String param1, String param2) {
        ContactsFragment fragment = new ContactsFragment();
//        Bundle args = new Bundle();
//        args.putString(ARG_PARAM1, param1);
//        args.putString(ARG_PARAM2, param2);
//        fragment.setArguments(args);
        return fragment;
    }

    public ContactsFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    //    findAllPersons();

        if (getArguments() != null) {
//            mParam1 = getArguments().getString(ARG_PARAM1);
//            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

    /**
     * 真机测试,有联系人的情况下:
     * 填充数据
     */
//    private void findAllPersons() {
//        Cursor cursor = getContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
//        mPersonsList = new ArrayList<>();
//        if (cursor.moveToFirst()) {
//            do {
//                //获取联系人name
//                String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
//                Persons person = new Persons(name, R.mipmap.ic_launcher,"",0);
//                mPersonsList.add(person);
//            }while (cursor.moveToNext());
//        } else {
//            getDatas();
//        }
//        //根据name的首字母排序
//        Collections.sort(mPersonsList);
//    }

    /**
     * 非真机测试或者无数据情况下,读取备用数据
     */
//    private void getDatas() {
//        if (ContactNames.NAMES.length != 0) {
//            for (String name : ContactNames.NAMES) {
//                Persons person = new Persons(name, R.mipmap.ic_launcher,"","0");
//                mPersonsList.add(person);
//            }
//        }
//    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.main_fragment_contacts, container, false);
        srl_contacts= (SwipeRefreshLayout) view.findViewById(R.id.srl_contacts);
        srl_contacts.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                srl_contacts.setRefreshing(false);
            }
        });
        //联系人列表
        rv_persons = (RecyclerView) view.findViewById(R.id.rv_persons);
        mLayoutManager = new LinearLayoutManager(getContext());

//        mLayoutManager.setStackFromEnd(true);
        // mLayoutManager.setReverseLayout(true);
        rv_persons.setLayoutManager(mLayoutManager);
        rv_persons.setItemAnimator(new DefaultItemAnimator());
        //  rv_persons.setHasFixedSize(false);
        mRVAdapter = new PersonRVAdapter(mPersonsList);
        qib_bar = (QuickIndexBar) view.findViewById(R.id.qib_bar);
        // 字母按键回调
        qib_bar.setOnLetterUpdateListener(new QuickIndexBar.OnLetterUpdateListener() {
            View view = LayoutInflater.from(getContext()).inflate(R.layout.pop_contacts_layout, null);
            TextView tv_content = (TextView) view.findViewById(R.id.tv_content);

            @Override
            public void onLetterUpdateListener(String letter) {
                <span style="color:#ff0000;">if (mPopupWindow != null) {
                    tv_content.setText(letter);
                } else {
                    mPopupWindow = new PopupWindow(view, 80, 80, false);
                    mPopupWindow.showAtLocation(getActivity().getWindow().getDecorView(), Gravity.CENTER, 0, 0);
                }
                tv_content.setText(letter);</span>

                mHandler.removeCallbacks(mThread);
                mHandler.postDelayed(mThread, 1000);

                if (TextUtils.equals(letter, "↑") || TextUtils.equals(letter, "#")) {
                    rv_persons.getLayoutManager().scrollToPosition(0);
                } else if (TextUtils.equals(letter, "☆")) {
                    rv_persons.getLayoutManager().scrollToPosition(mPersonsList.size()-1);
                } else {
                    //根据字母地位recycleView
                    for (int i = 0; i < mPersonsList.size(); i++) {
                        Persons person = mPersonsList.get(i);
                        String where = person.getPinyin().charAt(0) + "";
                        if (TextUtils.equals(letter, where)) {
                            //判断是向上滑动还是向下滑动
                            int currentLastPosition = mLayoutManager.findLastVisibleItemPosition();
                            int currentFirstPosition = mLayoutManager.findFirstVisibleItemPosition();
                            if (i > currentLastPosition) {//向下滑动
                                int total = currentLastPosition - currentFirstPosition;
                                rv_persons.getLayoutManager().scrollToPosition(i + total - 1);
                            } else {//向上滑动
                                rv_persons.getLayoutManager().scrollToPosition(i);
                            }
                            break;
                        }
                    }
                }
            }
        });
        rv_persons.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                <span style="color:#cc0000;">if (mPersonsList != null) {
                    char c = mPersonsList.get(mLayoutManager.findFirstVisibleItemPosition()).getPinyin().charAt(0);
                    currentFirstIndex = switchPinyin(c);
                    //     if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    qib_bar.setTouchIndex(currentFirstIndex);
                    //   }</span>
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

            }
        });
        rv_persons.setAdapter(mRVAdapter);
        return view;
    }


    public void onButtonPressed(Uri uri) {
        if (mListener != null) {
            mListener.onFragmentInteraction(uri);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            mListener = (OnFragmentInteractionListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }


    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    /**
     * 首字母转化成对应position
     *
     * @param c 字母
     * @return
     */
    private int switchPinyin(char c) {
        int switchNew = -1;
        if (c >= 'A' && c <= 'Z') {
            switchNew = 2;
            for (char cc = 'A'; cc <= 'Z'; cc++) {
                if (c == cc) {
                    break;
                }
                switchNew++;
            }
        } else {
            switchNew = 1;
        }
        return switchNew;
    }

}


好了,差不多就这样,大家如果在参考我的方法出错了,可以留言一下,方便大家一起进步!

最后在贴个他人使用expandableListView实现的连接:点击打开链接,

如果你希望一级列表展开且不能关闭,可以加入一下代码:

for (int i = 0; i < mGroupList.size(); i++) {
            mExpandableListView.expandGroup(i);
        }
        mExpandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
               <span style="white-space:pre">	</span>   //group 点击事件进行拦截,不进行任何处理,就不会关闭了
<pre style="font-family: 宋体; font-size: 10.5pt; background-color: rgb(255, 255, 255);"><span style="white-space:pre">		</span>  v.setClickable(<span style="color:#000080;"><strong>false</strong></span>);
return true; } });

 






Android原生联系人列表组件:微信式字母索引+悬停分组标题,零第三方依赖 一套开箱即用的Android联系人界面实现,完全基于原生View和RecyclerView构建,不依赖任何第三方UI库。支持按中文姓名首字母自动分组排序(含#号归类未命名联系人),右侧可滑动字母索引条,点击即跳转对应分组;滚动过程中顶部动态悬停显示当前分组标识(如‘A’‘王’‘#’),交互体验贴近微信通讯录。组件已适配真实通讯录数据读取逻辑,兼容主流Android API级别,可直接作为独立联系人选择器嵌入现有项目。工程结构清晰,核心代码集中在app模块,包含完整Gradle配置、适配各密度的资源图(1.p 阅读详情

相关推荐

微信H5通讯录开发实战:基于WeUI的组件选型与字母索引联动

在移动端H5开发中,通讯录是极具代表性的交互场景,不仅涉及列表渲染、搜索过滤,还包含字母索引、触摸联动等高频需求。WeUI作为微信官方维护的样式库,以轻量、原生视觉和灵活定制能力,成为构建此类页面的理想选择。通过理解WeUI的class体系与组件设计原理,开发者可以用较低成本实现分组列表、右侧字母索引和搜索栏等核心模块。在实际工程中,触摸事件处理、滚动定位偏移、粘性布局兼容以及长按菜单禁用等细节,直接决定了用户体验的流畅度。这类技术方案的落地,尤其适用于企业微信工具、内部OA系统或社交类H5页面,既能保持与

weixin_33290540的博客 239

安卓右侧字母索引,带滑动气泡,仿微信

效果: 特点: 写一个自定义view就行 暴露出来手指离开屏幕的接口 关键在于文字位置的绘制,和父view 的clipchilden的属性 在layout中: <RelativeLayout android:layout_width="25dp" android:layout_height="match_parent" android:layout_marginTop="100dp" android:l

weixin_43225603的博客 564

基于weui从零构建通讯录索引列表与搜索联动实战

在移动端页面开发中,通讯录场景常涉及列表分组、字母索引、快速检索等典型交互。weui作为微信官方设计规范下的轻量级CSS方案,提供了贴近微信原生视觉的基础组件,但缺少IndexBar等完整业务组件,需要开发者自行实现分组定位与搜索联动逻辑。本文从技术原理出发,剖析通讯录页面的核心架构,讲解如何利用weui的cell、search-bar等基础样式,配合原生JavaScript实现按拼音首字母分组的索引列表、滑动高亮与滚动关联,以及处理中文输入法组合态、防抖过滤等实践细节,并针对Android WebView

weixin_33824385的博客 283

Android自定义字母索引

Android字母索引这个功能还是很常见的,例如:电话联系人,城市选择等一些功能都会用到。轮子已造好直接使用,[Github](https://github.com/lzjin/SideBarView.git)地址。 ####效果图: ![gif.gif](https://upload-images.jianshu.io/upload_images/9249152-8997e2ffc6500...

落雨敏 4913

Android-仿微信通讯录的实现联系人按字母分组显示右侧添加快速索引条

在APP中,只要有联系人,都肯定有通讯录页面,而且所有的通讯录以页面都很相似,一个列表,列表中的人员按字母排列,而且按字母分组显示,且右侧有快速索引。

android仿微信通讯录搜索示例(匹配拼音,字母,索引位置)

前言: 仿微信通讯录搜索功能,通过汉字或拼音首字母找到匹配的联系人并显示匹配的位置 一:先看效果图 字母索引 搜索匹配 二:功能分析 1:汉字转拼音 通讯录汉字转拼音(首个字符当考虑姓氏多音字), 现在转换拼音常见的有pinyin4j和tinypinyin, pinyin4j的功能强大,包含声调多音字,tinypinyin执行快占用内存少, 如果只是简单匹配通讯录,建议使用tinypinyin,用法也很简单这里不详细介绍 拼音类 public class CNPinyin implements Serializable, Comparable<CNPi

【亲测免费】 打造高效通讯录:Android仿微信通讯录界面功能实现

打造高效通讯录:Android仿微信通讯录界面功能实现 去发现同类优质开源项目:https://gitcode.com/ 项目介绍 在移动应用开发中,通讯录功能是许多社交类应用的核心组件之一。为了帮助开发者快速实现类似微信的通讯录界面,我们推出了“Android仿微信通讯录界面功能实现”资源包。该资源包不仅详细展示了如何在Android平台上构建一个具有分组显示、按字母索引导航的好友列表界面,还涵...

gitblog_06590的博客 522

Android自定义View实现通讯录字母索引(仿微信通讯录)

一、效果:我们看到很多软件的通讯录在右侧都有一个字母索引功能,像微信,小米通讯录,QQ,还有美团选择地区等等。这里我截了一张美团选择城市的图片来看看; 我们今天就来实现图片中右侧模块的索引功能,包括触摸显示以选中的索引字母。这里我的UI界面主要是参照微信的界面来实现,所以各位也可以对照微信来看看效果,什么都不说了,只有效果图最具有说服力! 二、分析: 我们看到这样的效果我们心理都回去琢磨,他是如何实现的; 首先,它肯定是通过自定义 View 来实现的,因为 Android 没有提供类似这样的控件、那么接下来就是如何自定义我们的 View ,我们知道自定义 View 最最主要的两个方法就是

从0到1掌握Qwen2.5-VL-72B-Instruct-quantized.w8a8:开发者必备API手册

Qwen2.5-VL-72B-Instruct-quantized.w8a8是一款基于Qwen/Qwen2.5-VL-72B-Instruct模型进行INT8量化优化的视觉-文本多模态模型,由Neural Magic开发。它能高效处理图像与文本输入并生成精准文本输出,在保持出色性能的同时显著降低计算资源需求,是开发者构建视觉问答、图像理解等应用的理想选择。 ## 模型核心特性解析 🚀 ###

gitblog_00106的博客 490

基于NE555的触摸延时开关电路设计与实现

本文详细介绍了基于NE555芯片的触摸延时开关电路设计与实现方法。通过分析NE555的单稳态工作模式,结合定时电阻、电解电容等关键元件选型,提供了一套完整的电路搭建方案。文章还包含抗干扰设计、调试技巧及常见故障处理,帮助电子爱好者快速掌握触摸延时灯的制作要点。

weixin_29268267的博客 788

Jenkins+docker 微服务实现自动化部署安装和部署过程

本文详细介绍了如何使用Docker部署Jenkins自动化服务器。主要内容包括:1)拉取Jenkins官方或BlueOcean镜像;2)创建容器并配置端口映射、数据卷挂载;3)设置防火墙规则;4)配置清华大学镜像加速;5)安装必要插件;6)SSH密钥配置;7)构建触发器设置;8)编写自动化构建脚本。通过Docker部署Jenkins可以简化安装流程,确保环境一致性,并利用容器特性实现快速启动和管理。文章提供了完整的配置步骤和注意事项,帮助用户快速搭建CI/CD环境。

qq_33665793的博客 1687

自定义类似微信联系人列表姓名首字母边栏索引列表效果

声明:部分代码和资源并非原创,而是从互联网上下载。资源和代码经过本人的一些优化,逻辑更清晰易读,加强了使用性,嵌入项目时更容易。 效果类似于 先来看一下右边的SideBar字母索引列表效果如何实现: package org.initialltouchlist; import com.example.listsildedel.R; import android.co

時を越える笑顔 2182

CSM中一些常见问题的解决方式

CSM中一些常见问题的解决方式 抖动 摄像机在每帧移动或者旋转时,会产生较严重的抖动问题。

小龙虾的博客 2130

自定义控件:快速索引

快速索引在应用中很常见,在联系人,微信,省市列表,应用管理,文件管理等应用场景都可以看到快速索引的身影,本篇博客将讲解快速索引的自定义,从中你可以学到获取汉字首字母的方法,绘制字母时,纵坐标的计算方法一、静态绘制初始化数据创建自定义控件QuickIndexBar 继承Viewpublic class QuickIndexBar extends View { private Paint

JackChan 1646

【Unity】NGUI下载与导入

Unity3D插件NGUI的下载和导入。

Get your hands dirty 3万+

正弦波、方波、三角波和锯齿波信号发生器——Multisim电路仿真

调节方波信号的占空比,也就是改变输出信号的高低电平时间比例,可以设置电容不同的充放电时间来达到目的,RP2可以使得电容具有不同的充放电时间,其中二极管起到限制电流流向的作用。假设U+>U,那么输出端就达到正向饱和,电容开始充电,当电容电压大于同相输入端电压时,此时电容开始放电,由此往复,从而产生方波信号。3,正反馈支路占优势,电路开始起振并不是稳定的,振荡会不断增大,最终将导致运放饱和,输出的波形是削波失真的正弦波。通常在电路中引入负反馈,例如使用负温度系数的热敏电阻,以自动稳定增益,防止输出波形失真。

XU157303764的博客 5319

【Android】快速实现仿美团选择城市界面,微信通讯录界面

做法:不过既然是RecyclerView里的Item,又有。

2401_84132401的博客 1028

一文整理深度学习【深度学习linux的docker+pytorch+cuda+nvidia-docker+vscode配置】

docker+pytorch+cuda+nvidia-docker+vscode

Edward的博客 1335

Android 微信通讯录界面实现Demo

Android 微信通讯录界面实现Demo 去发现同类优质开源项目:https://gitcode.com/ 欢迎使用“Android仿微信通讯录界面功能实现”资源包。本资源旨在帮助开发者快速实现类似微信的通讯录功能,涵盖移动端和服务端的核心逻辑。通过本示例,您可以学习到如何构建具有分组显示、按字母索引导航的好友列表界面,同时了解移动端与服务端交互的基础知识。 功能特色 移动端实现:详细展示了...

gitblog_06553的博客 439

桌面运维工程师面试题汇总(高频完整版)

岗位,覆盖:电脑硬件、系统安装与故障、网络、办公软件、安全、AD域、打印机、日常排错、服务意识。

老韩的Linux云计算架构师进阶之路 911

【Flutter 问题系列第 85 篇】Xcode 运行 Flutter 提示 The Dart VM Service was not discovered after 60 seconds.

在 Xcode 上运行 Flutter 时提示 `The Dart VM Service was not discovered after 60 seconds. This is taking much longer than expected...`,意思是 `60秒后未发现Dart虚拟机服务。这比预期的要长得多`

“Allen Su”的博客 2235

联系人的侧边字母索引ListView 将手机通讯录姓名通过首字母排序。

1 package com.lixu.letterlistview; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import org.apache.http.NameValuePair; 6 import org.apache.http.message.BasicName...

weixin_30655219的博客 224

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

【Android】快速实现仿美团选择城市界面

做法:不过既然是RecyclerView里的Item,又有。

1002

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

【Android】快速实现仿美团选择城市界面,薪资翻倍

上文提到,重构后,SuspensionDecoration数据源依赖的接口是ISuspensionInterface, 如下: public interface ISuspensionInterface { //是否需要显示悬停title boolean isShowSuspension(); //悬停的title String getSuspensionTag(); } 在BaseIndexBean里实现,默认显示悬停,分组title和IndexBar的Tag是一样的。 public abstract c

m0_66685318的博客 477

快速索引 (对View的自定义,黑马程序员)

* 应用场景: 微信好友列表, 联系人通讯录, 应用管理, 文件管理 * 功能实现:  * 1. A-Z索引的绘制.  * 2. 处理Touch事件.  * 3. 提供使用监听\回调.  * 4. 汉字转换成拼音.  * 5. 进行排序展示. * 6. 进行分组.  * 7. 将自定义控件和ListView合体.  项目结构: MainActivity:

临窗听雨的博客 616

Android之自定义索引

自定义view在我看来,一直都是一个难点,它的逻辑思维很强,有时候你会完全想不到。我自己也是对自定义view很抵触,可奈何,再抵触还是要学,要会的一个知识点。今天我们就在写一回,但其实我觉得好像也不完全算是自定义view。好了,不扯了,入正题。        微信主页面的好友列表大家应该都熟悉,我要说的是列表最右边的那一竖排字母的索引,就是它让我们在寻找好友得时候可以方便一点,并且你的好友都是在

wq_0711的博客 301
上一篇: android 屏幕亮度控制
下一篇: android 广播实现activity之间的通讯
kill_bugs
博客等级 码龄12年 12粉丝 39原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值