场景:因部分用户跳转通讯录后习惯使用右侧的的字母导航,然后我们发现部分机型的右侧导航使用之后有时候无效,有时候会导致app当前界面崩溃,在这种需求驱动下,我整理了同事写的代码,然后做了细致划分,Tired …
注:只要你做Android一年以上,那么根据文中步骤操作的话,实现这个功能没什么难度 ~
Learning is the first driving force
目录
- 提前掌握
- 准备工作
- 实际应用
提前掌握
注:整个功能Demo基本是围绕以下几个功能综合而成的,代码略微较长,为了方便查看,我已将它打包上传
Demo下载地址:点我,点我,快点我 > <
- Android基础功能 - 跳转通讯录且回传用户信息
- Android6.0特性 - 动态权限的多样性处理方式
- Android进阶之路 - 封装完善的BaseAdapter之BaseQuickAdapter
- Android进阶之路 - 五分钟内掌握EventBus的使用方式
准备工作
二级目录
- 权限部分
- 通讯录部分
- EventBus信息传递部分
- Adapter部分
权限部分
-
争对动态权限的问题,可以查看 Android6.0特性 - 动态权限的多样性处理方式
-
涉及依赖 - build.gradle(app)
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.0.2'
implementation 'com.github.tbruyelle:rxpermissions:0.10.2'
通讯录部分
- 添加权限 - manifests
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
- Abbreviated
package nkwl.com.contentdemo;
/**
* @author paul
* @date
* desc:
*/
public interface Abbreviated {
String getInitial();
}
- 用于接收联系人信息的Model - ShareContactsBean
/**
* Copyright 2017 ChenHao Dendi
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 nkwl.com.contentdemo;
public class ShareContactsBean implements Abbreviated, Comparable<ShareContactsBean> {
private final String mName;
private final String mPhone;
private final String mAbbreviation;
private final String mInitial;
ShareContactsBean(String name, String phone) {
this.mName = name;
this.mPhone = phone;
this.mAbbreviation = ContactsUtils.getAbbreviation(name);
this.mInitial = mAbbreviation.substring(0, 1);
}
@Override
public String getInitial() {
return mInitial;
}
public String getName() {
return mName;
}
public String getPhone() {
return mPhone;
}
@Override
public int compareTo(ShareContactsBean r) {
if (mAbbreviation.equals(r.mAbbreviation)) {
return 0;
}
boolean flag;
if ((flag = mAbbreviation.startsWith("#")) ^ r.mAbbreviation.startsWith("#")) {
return flag ? 1 : -1;
}
return getInitial().compareTo(r.getInitial());
}
}
- 获取联系人的工具类 - ContactsManager
package nkwl.com.contentdemo;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import io.reactivex.annotations.NonNull;
public class ContactsManager {
@NonNull
public static ArrayList<ShareContactsBean> getPhoneContacts(Context context) {
ArrayList<ShareContactsBean> result = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
Cursor phoneCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, null, null, null);
if (phoneCursor != null) {
while (phoneCursor.moveToNext()) {
String phoneNumber = phoneCursor.getString(0).replace(" ", "").replace("-", "");
String contactName = phoneCursor.getString(1);
result.add(new ShareContactsBean(contactName, phoneNumber));
}
phoneCursor.close();
}
Collections.sort(result, new Comparator<ShareContactsBean>() {
@Override
public int compare(ShareContactsBean l, ShareContactsBean r) {
return l.compareTo(r);
}
});
return result;
}
}
EventBus信息传递部分
- build.fradle(app)
implementation 'org.greenrobot:eventbus:3.0.0'
- Eventbus大多都会有一个实体类用于数据传递 - EventBusBean
package nkwl.com.contentdemo;
/**
* Created by paul on 2017/8/10.
* EventBus发送的事件实体
*/
public class EventBusBean<T> {
private int code;
private T data;
public EventBusBean(int code) {
this.code = code;
}
public EventBusBean(int code, T data) {
this.code = code;
this.data = data;
}
public int getCode() {
return code;
}
public T getData() {
return data;
}
public void setCode(int code) {
this.code = code;
}
public void setData(T data) {
this.data = data;
}
}
- EventBus封装的工具类 - EventBusUtil
package nkwl.com.contentdemo;
import org.greenrobot.eventbus.EventBus;
public class EventBusUtil {
public static void register(Object subscriber) {
EventBus.getDefault().register(subscriber);
}
public static void unregister(Object subscriber) {
EventBus.getDefault().unregister(subscriber);
}
public static void postEvent(EventBusBean eventBusBean) {
EventBus.getDefault().post(eventBusBean);
}
//event3.0粘性事件
public static void postStickyEvent(EventBusBean eventBusBean) {
EventBus.getDefault().postSticky(eventBusBean);
}
}
Adapter部分
- build.gradle
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
implementation 'com.android.support:recyclerview-v7:28.0.0'
- 通讯录右侧字母导航的自定义控件 - AzSidebar
package nkwl.com.contentdemo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
/**
* @author paul
* @date desc:字母索引
*/
public class AzSidebar extends View {
/**
* 索引字母颜色
*/
private static final int LETTER_COLOR = 0xFF757575;
/**
* 索引字母背景圆圈颜色
*/
private static final int BG_COLOR = 0xFF4081D6;
/**
* 索引字母数组
*/
public String[] indexs = {"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 int mWidth;
private int mHeight;
/**
* 单元格的高度
*/
private float mCellHeight;
/**
* 顶部间距
*/
private float mMarginTop;
/**
* 手指按下的字母索引
*/
private int touchIndex = -1;
private Paint mPaint;
private Paint bgPaint;
/**
* 手指是否触目屏幕
*/
/*private boolean isTouched = false;*/
/**
* 手指抬起是否显示背景
*/
private boolean isDisplay = true;
public AzSidebar(Context context) {
super(context);
init();
}
public AzSidebar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(LETTER_COLOR);
mPaint.setTextSize(dp2px(getContext(), 11));
mPaint.setAntiAlias(true);
bgPaint = new Paint();
bgPaint.setColor(BG_COLOR);
bgPaint.setAntiAlias(true);
}
public void setIndexs(String[] indexs) {
this.indexs = indexs;
mMarginTop = (mHeight - mCellHeight * indexs.length) / 2;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
//字母的坐标点:(x,y)
if (indexs.length <= 0) {
return;
}
drawLetter(canvas);
}
/**
* 画字母
*/
private void drawLetter(Canvas canvas) {
for (int i = 0; i < indexs.length; i++) {
String letter = indexs[i];
float textX = mWidth / 2 - getTextWidth(letter) / 2;
float textY = mCellHeight / 2 + getTextHeight(letter) / 2 + mCellHeight * i + mMarginTop;
if (touchIndex == i && isDisplay) {
//绘制文字圆形背景
float circleX = mWidth / 2 + dp2px(getContext(), 0.5f);
float circleY = mCellHeight / 2 + mCellHeight * i + mMarginTop;
canvas.drawCircle(circleX, circleY, dp2px(getContext(), 8), bgPaint);
mPaint.setColor(Color.WHITE);
} else {
mPaint.setColor(LETTER_COLOR);
}
canvas.drawText(letter, textX, textY, mPaint);
}
}
/**
* dp 转化为 px
*
* @param context context
* @param dpValue dpValue
* @return int
*/
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 获取字符的宽度
*
* @param text 需要测量的字母
* @return 对应字母的高度
*/
public float getTextWidth(String text) {
Rect bounds = new Rect();
mPaint.getTextBounds(text, 0, text.length(), bounds);
return bounds.width();
}
/**
* 获取字符的高度
*
* @param text 需要测量的字母
* @return 对应字母的高度
*/
public float getTextHeight(String text) {
Rect bounds = new Rect();
mPaint.getTextBounds(text, 0, text.length(), bounds);
return bounds.height();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
//26个字母加上"#"
mCellHeight = (mHeight * 1f / 27);
mMarginTop = (mHeight - mCellHeight * indexs.length) / 2;
}
public void setDisplay(boolean d) {
this.isDisplay = d;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 按下字母的下标
int letterIndex = (int) ((event.getY() - mMarginTop) / mCellHeight);
if (letterIndex != touchIndex) {
if (letterIndex > indexs.length - 1) {
touchIndex = indexs.length - 1;
} else if (letterIndex < 0) {
touchIndex = 0;
} else {
touchIndex = letterIndex;
}
}
// 判断是否越界
if (letterIndex >= 0 && letterIndex < indexs.length) {
// 显示按下的字母
if (textView != null) {
textView.setVisibility(View.VISIBLE);
textView.setText(indexs[letterIndex]);
}
//通过回调方法通知列表定位
if (mOnIndexChangedListener != null) {
mOnIndexChangedListener.onIndexChanged(indexs[letterIndex]);
}
}
//isTouched = true;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (textView != null) {
textView.setVisibility(View.GONE);
}
//isTouched = false;
break;
default:
}
invalidate();
return true;
}
public void setTouchIndex(String word) {
for (int i = 0; i < indexs.length; i++) {
if (indexs[i].equals(word)) {
touchIndex = i;
invalidate();
return;
}
}
}
public interface OnIndexChangedListener {
/**
* 按下字母改变了
*
* @param index 按下的字母
*/
void onIndexChanged(String index);
}
private OnIndexChangedListener mOnIndexChangedListener;
private TextView textView;
public void setOnIndexChangedListener(OnIndexChangedListener onIndexChangedListener) {
this.mOnIndexChangedListener = onIndexChangedListener;
}
/**
* 设置显示按下首字母的TextView
*/
public void setSelectedIndexTextView(TextView textView) {
this.textView = textView;
}
}
- 通讯录adaptere的Item布局 - item_contacts
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff">
<View
android:id="@+id/item_top_line"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#E4E4E4"
android:layout_marginLeft="15dp"
android:layout_below="@+id/tv_letter"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_letter"
android:layout_width="match_parent"
android:layout_height="28dp"
android:background="#F0F0F0"
android:gravity="center_vertical"
android:paddingLeft="15dp"
android:textColor="#757575"
android:visibility="gone"
tools:text="A"
tools:visibility="visible"/>
<TextView
android:id="@+id/tv_contacts_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_letter"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:ellipsize="end"
android:maxLength="10"
android:singleLine="true"
tools:text="boy"/>
<TextView
android:id="@+id/tv_contacts_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_contacts_name"
android:layout_marginLeft="15dp"
android:layout_marginTop="6dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="6dp"
android:gravity="center_vertical"
android:textColor="#999999"
android:textSize="14sp"
tools:text="13764959025"/>
</RelativeLayout>
- 通讯录的适配器 - ContactsAdapter
package nkwl.com.contentdemo;
import android.support.annotation.Nullable;
import android.view.View;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import java.util.List;
/**
* @date 2018/12/3
* desc:
*/
public class ContactsAdapter extends BaseQuickAdapter<ShareContactsBean, BaseViewHolder> {
private List<ShareContactsBean> data;
public ContactsAdapter(int layoutResId, @Nullable List<ShareContactsBean> data) {
super(layoutResId, data);
this.data = data;
}
@Override
protected void convert(BaseViewHolder helper, ShareContactsBean item) {
boolean isVisible = helper.getLayoutPosition() == getPositionForLetterName(item.getInitial());
helper.getView(R.id.tv_letter).setVisibility(isVisible ? View.VISIBLE : View.GONE);
helper.getView(R.id.item_top_line).setVisibility(isVisible ? View.GONE : View.VISIBLE);
helper.setText(R.id.tv_contacts_name, item.getName())
.setText(R.id.tv_contacts_phone, item.getPhone())
.setText(R.id.tv_letter, item.getInitial());
}
public int getPositionForLetterName(String firstLetterChar) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getInitial().equals(firstLetterChar)) {
return i;
}
}
return -1;
}
}
实际应用
- 一级页面UI - activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_phone"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:text="获取联系人信息" />
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:text="" />
<TextView
android:id="@+id/tv_num"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:text="" />
</LinearLayout>
- 一级页面,主要包含权限请求、EventBus回调处理 - ManiActivity
package nkwl.com.contentdemo;
import android.Manifest;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.tbruyelle.rxpermissions2.RxPermissions;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public class MainActivity extends AppCompatActivity {
private TextView mName;
private TextView mPhone;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EventBusUtil.register(this);
mName = findViewById(R.id.tv_name);
mPhone = findViewById(R.id.tv_num);
findViewById(R.id.tv_phone).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getPhoneData();
}
});
}
//动态权限
private void getPhoneData() {
RxPermissions rxPermissions = new RxPermissions(this);
//申请一个权限,这个权限组就已经授权
rxPermissions.request(Manifest.permission.READ_CONTACTS, Manifest.permission.READ_PHONE_STATE).subscribe(new Observer<Boolean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(Boolean aBoolean) {
//有权限的状态
if (aBoolean) {
Intent intent = new Intent(MainActivity.this, ContactsActivity.class);
startActivity(intent);
}
//无权限的状态
else {
Toast.makeText(MainActivity.this, "没有授权", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
//EventBus回调监听
@Subscribe(threadMode = ThreadMode.MAIN)
public void receiceEvent(EventBusBean eventBusBean) {
if (eventBusBean != null) {
switch (eventBusBean.getCode()) {
case 178:
ShareContactsBean shareContactsBean = (ShareContactsBean) eventBusBean.getData();
mName.setText(shareContactsBean.getName());
mPhone.setText(shareContactsBean.getPhone());
break;
default:
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
EventBusUtil.unregister(this);
}
}
- 通讯录UI - activity_contacts
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_contacts"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<nkwl.com.contentdemo.AzSidebar
android:id="@+id/sidebar"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"/>
</RelativeLayout>
</LinearLayout>
- 通讯录(关键点) - ContactsActivity
package nkwl.com.contentdemo;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import com.chad.library.adapter.base.BaseQuickAdapter;
import java.util.ArrayList;
import java.util.List;
public class ContactsActivity extends Activity {
private List<ShareContactsBean> constactsList;
private List<String> letterList;
private ContactsAdapter contactsAdapter;
private LinearLayoutManager layoutManager;
private RecyclerView rvContacts;
private AzSidebar sideBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contacts);
rvContacts = findViewById(R.id.rv_contacts);
sideBar = findViewById(R.id.sidebar);
constactsList = new ArrayList<>();
letterList = new ArrayList<>();
initView();
fetchData();
setListener();
}
private void initView() {
contactsAdapter = new ContactsAdapter(R.layout.item_contacts, constactsList);
layoutManager = new LinearLayoutManager(this);
rvContacts.setLayoutManager(layoutManager);
rvContacts.setAdapter(contactsAdapter);
}
private void fetchData() {
letterList.clear();
constactsList.clear();
ArrayList<ShareContactsBean> phoneContacts = ContactsManager.getPhoneContacts(this);
constactsList.addAll(phoneContacts);
for (int i = 0; i < constactsList.size(); i++) {
ShareContactsBean shareContactsBean = constactsList.get(i);
String initial = shareContactsBean.getInitial();
if (!letterList.contains(initial)) {
letterList.add(initial);
}
}
sideBar.setIndexs(letterList.toArray(new String[letterList.size()]));
contactsAdapter.notifyDataSetChanged();
}
private void setListener() {
contactsAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
List data = adapter.getData();
ShareContactsBean shareContactsBean = (ShareContactsBean) data.get(position);
EventBusUtil.postEvent(new EventBusBean<>(178, shareContactsBean));
finish();
}
});
sideBar.setOnIndexChangedListener(new AzSidebar.OnIndexChangedListener() {
@Override
public void onIndexChanged(String index) {
int positionForLetterName = contactsAdapter.getPositionForLetterName(index);
layoutManager.scrollToPositionWithOffset(positionForLetterName, 0);
}
});
rvContacts.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (constactsList.size() > 0) {
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
sideBar.setTouchIndex(constactsList.get(firstVisibleItemPosition).getInitial());
}
}
});
}
}