Android MVVM框架搭建(七)Permission、AlertDialog、拍照和相册选取
- 前言
- 正文
- 一、数据库升级
- 二、数据操作
- 二、自定义Dialog
- ① DialogViewHelper
- ② AlertController
- ③ AlertDialog
- ④ 样式
- ⑤ 布局
- 三、权限请求
- ① 权限配置
- ② 权限工具类
- 四、DataBinding
- 五、工具类
- 六、核心环节
- ① 显示修改用户信息弹窗
- ② 相册选取
- ③ 相机拍照
- ③ 修改昵称和简介
- 七、源码
前言
在上一篇博客中完成了新闻详情数据的查看以及用户的注册登录,这篇文章中将对用户的信息进行增加和修改。会使用到文件读写、相机权限、自定义Dialog、相册选取和相机拍照。
正文
下面先进行数据库的升级,因为我们要更换用户的头像,因此首先用户表里面是需要一个头像的字段的,之前对数据库进行升级的时候都是直接添加一个表,那么这一次升级我们往表里面增加一个字段。
一、数据库升级
一般来说再设计数据库的时候就要想到一些因素,像增加表字段这种事情一般是出现在业务需求有改动的情况下,因此我们在设计表的时候可以想清楚有没有可能进行扩展,会怎样扩展。下面我们要往数据表User中增加一个avatar的字段,表示头像。
同时,增加get和set方法。
代码语言:javascript复制 public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
然后进行数据库的升级,打开AppDatabase,增加如下代码:
代码语言:javascript复制 /**
* 版本升级迁移到5 在用户表中新增一个avatar字段
*/
static final Migration MIGRATION_4_5 = new Migration(4, 5) {
@Override
public void migrate(@NonNull @NotNull SupportSQLiteDatabase database) {
//User表中新增avatar字段
database.execSQL("ALTER TABLE `user` ADD COLUMN avatar TEXT");
}
};
这表示我在User表中增加一个avatar字段,然后我们添加迁移
再把数据库版本改成5。
这样,数据库的升级迁移就完成了。
二、数据操作
UserRepository中的代码也需要更新,在里面增加如下代码:
代码语言:javascript复制 private static volatile UserRepository mInstance;
public static UserRepository getInstance() {
if (mInstance == null) {
synchronized (UserRepository.class) {
if (mInstance == null) {
mInstance = new UserRepository();
}
}
}
return mInstance;
}
这一篇文章中将会涉及到HomeActivity中的页面数据交互,因此,我们需要一个HomeViewModel,在viewmodels包下创建它,里面的代码如下:
代码语言:javascript复制public class HomeViewModel extends BaseViewModel {
public LiveData<User> user;
public String defaultName = "初学者-Study";
public String defaultIntroduction = "Android | Java";
public void getUser() {
user = UserRepository.getInstance().getUser();
}
public void updateUser(User user) {
UserRepository.getInstance().updateUser(user);
failed = UserRepository.getInstance().failed;
getUser();
}
}
这里我放置了两个默认值,因为在注册的时候,昵称和简介是可以不用填写的,所以在显示的时候如果没有填就显示这个默认值,如果是Kotlin的话就直接使用缺省值就好了,这两个默认值会在xml中用到的。同时这个HomeViewModel里面有一个获取用户信息和修改用户信息的方法,当我们登录成功进入的HomeActivity时是获取,当修改用户信息的时候是更新,这很好理解。这一步说清楚之后下面就要做新的操作了。
二、自定义Dialog
下面要定义一个dialog,用于App中使用,在view包下新建一个dialog包,包下新建一个DialogViewHelper类,里面的代码如下:
① DialogViewHelper
代码语言:javascript复制public class DialogViewHelper {
private View mContentView;
private SparseArray<WeakReference<View>> mViews;
public DialogViewHelper(Context context, int layoutResId) {
this();
mContentView = LayoutInflater.from(context).inflate(layoutResId, null);
}
public DialogViewHelper() {
mViews = new SparseArray<>();
}
public <T extends View> void setText(int viewId, CharSequence text) {
TextView tv = getView(viewId);
if (tv != null) {
tv.setText(text);
}
}
public <T extends View> T getView(int viewId) {
WeakReference<View> weakReference = mViews.get(viewId);
View view = null;
if (weakReference != null) {
view = weakReference.get();
}
if (view == null) {
view = mContentView.findViewById(viewId);
if (view != null) {
mViews.put(viewId, new WeakReference<>(view));
}
}
return (T) view;
}
public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
View view = getView(viewId);
if (view != null) {
view.setOnClickListener(onClickListener);
}
}
public void setIcon(int viewId, int resId) {
ImageView iv = getView(viewId);
if (iv != null) {
iv.setImageResource(resId);
}
}
public void setContentView(View contentView) {
mContentView = contentView;
}
public View getContentView() {
return mContentView;
}
}
② AlertController
同样在dialog包下新建一个AlertController类,代码如下:
代码语言:javascript复制public class AlertController {
private AlertDialog mAlertDialog;
private Window mWindow;
private DialogViewHelper mViewHelper;
public AlertController(AlertDialog alertDialog, Window window) {
mAlertDialog = alertDialog;
mWindow = window;
}
public void setDialogViewHelper(DialogViewHelper dialogViewHelper) {
mViewHelper = dialogViewHelper;
}
public void setText(int viewId, CharSequence text) {
mViewHelper.setText(viewId, text);
}
public void setIcon(int viewId, int resId) {
mViewHelper.setIcon(viewId, resId);
}
public <T extends View> T getView(int viewId) {
return mViewHelper.getView(viewId);
}
public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
mViewHelper.setOnClickListener(viewId, onClickListener);
}
public AlertDialog getDialog() {
return mAlertDialog;
}
public Window getWindow() {
return mWindow;
}
//-------------------------------------------------------------------------------------------------
public static class AlertParams {
public Context mContext;
//对话框主题背景
public int mThemeResId;
public boolean mCancelable;
public DialogInterface.OnCancelListener mOnCancelListener;
public DialogInterface.OnDismissListener mOnDismissListener;
public DialogInterface.OnKeyListener mOnKeyListener;
//文本颜色
public SparseArray<Integer> mTextColorArray = new SparseArray<>();
//存放文本的更改
public SparseArray<CharSequence> mTextArray = new SparseArray<>();
//存放点击事件
public SparseArray<View.OnClickListener> mClickArray = new SparseArray<>();
//存放长按点击事件
public SparseArray<View.OnLongClickListener> mLondClickArray = new SparseArray<>();
//存放对话框图标
public SparseArray<Integer> mIconArray = new SparseArray<>();
//存放对话框图片
public SparseArray<Bitmap> mBitmapArray = new SparseArray<>();
//对话框布局资源id
public int mLayoutResId;
//对话框的view
public View mView;
//对话框宽度
public int mWidth;
//对话框高度
public int mHeight;
//对话框垂直外边距
public int mHeightMargin;
//对话框横向外边距
public int mWidthMargin;
//动画
public int mAnimation;
//对话框显示位置
public int mGravity = Gravity.CENTER;
public AlertParams(Context context, int themeResId) {
mContext = context;
mThemeResId = themeResId;
}
public void apply(AlertController alert) {
//设置对话框布局
DialogViewHelper dialogViewHelper = null;
if (mLayoutResId != 0) {
dialogViewHelper = new DialogViewHelper(mContext, mLayoutResId);
}
if (mView != null) {
dialogViewHelper = new DialogViewHelper();
dialogViewHelper.setContentView(mView);
}
if (dialogViewHelper == null) {
throw new IllegalArgumentException("please set layout");
}
//将对话框布局设置到对话框
alert.getDialog().setContentView(dialogViewHelper.getContentView());
//设置DialogViewHelper辅助类
alert.setDialogViewHelper(dialogViewHelper);
//设置文本
for (int i = 0; i < mTextArray.size(); i ) {
alert.setText(mTextArray.keyAt(i), mTextArray.valueAt(i));
}
//设置图标
for (int i = 0; i < mIconArray.size(); i ) {
alert.setIcon(mIconArray.keyAt(i), mIconArray.valueAt(i));
}
//设置点击
for (int i = 0; i < mClickArray.size(); i ) {
alert.setOnClickListener(mClickArray.keyAt(i), mClickArray.valueAt(i));
}
//配置自定义效果,底部弹出,宽高,动画,全屏
Window window = alert.getWindow();
window.setGravity(mGravity);//显示位置
if (mAnimation != 0) {
window.setWindowAnimations(mAnimation);//设置动画
}
//设置宽高
WindowManager.LayoutParams params = window.getAttributes();
params.width = mWidth;
params.height = mHeight;
params.verticalMargin = mHeightMargin;
params.horizontalMargin = mWidthMargin;
window.setAttributes(params);
}
}
}
下面自定义Dialog
③ AlertDialog
在dialog包下新建一个AlertDialog,里面的代码如下:
代码语言:javascript复制public class AlertDialog extends Dialog {
private AlertController mAlert;
public AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, themeResId);
mAlert = new AlertController(this, getWindow());
}
public void setText(int viewId, CharSequence text) {
mAlert.setText(viewId, text);
}
public <T extends View> T getView(int viewId) {
return mAlert.getView(viewId);
}
public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
mAlert.setOnClickListener(viewId, onClickListener);
}
//----------------------------------------------------------------------------------------------
public static class Builder {
private final AlertController.AlertParams P;
public Builder(Context context) {
this(context, R.style.dialog);
}
public Builder(Context context, int themeResId) {
P = new AlertController.AlertParams(context, themeResId);
}
/**
* 设置对话框布局
*
* @param view
* @return
*/
public Builder setContentView(View view) {
P.mView = view;
P.mLayoutResId = 0;
return this;
}
/**
* @param layoutId
* @return
*/
public Builder setContentView(int layoutId) {
P.mView = null;
P.mLayoutResId = layoutId;
return this;
}
/**
* 设置文本
*
* @param viewId
* @param text
* @return
*/
public Builder setText(int viewId, CharSequence text) {
P.mTextArray.put(viewId, text);
return this;
}
/**
* 设置文本颜色
*
* @param viewId
* @param color
* @return
*/
public Builder setTextColor(int viewId, int color) {
P.mTextColorArray.put(viewId, color);
return this;
}
/**
* 设置图标
*
* @param iconId
* @return
*/
public Builder setIcon(int iconId, int resId) {
P.mIconArray.put(iconId, resId);
return this;
}
/**
* 设置图片
*
* @param viewId
* @return
*/
public Builder setBitmap(int viewId, Bitmap bitmap) {
P.mBitmapArray.put(viewId, bitmap);
return this;
}
/**
* 设置对话框宽度占满屏幕
*
* @return
*/
public Builder fullWidth() {
P.mWidth = ViewGroup.LayoutParams.MATCH_PARENT;
return this;
}
/**
* 对话框底部弹出
*
* @param isAnimation
* @return
*/
public Builder fromBottom(boolean isAnimation) {
if (isAnimation) {
P.mAnimation = R.style.dialog_from_bottom_anim;
}
P.mGravity = Gravity.BOTTOM;
return this;
}
/**
* 对话框右部弹出
*
* @param isAnimation
* @return
*/
public Builder fromRight(boolean isAnimation) {
if (isAnimation) {
P.mAnimation = R.style.dialog_scale_anim;
}
P.mGravity = Gravity.RIGHT;
return this;
}
/**
* 设置对话框宽高
*
* @param width
* @param heigth
* @return
*/
public Builder setWidthAndHeight(int width, int heigth) {
P.mWidth = width;
P.mHeight = heigth;
return this;
}
/**
* 设置对话框宽高
*
* @param width
* @param heigth
* @return
*/
public Builder setWidthAndHeightMargin(int width, int heigth, int heightMargin, int widthMargin) {
P.mWidth = width;
P.mHeight = heigth;
P.mHeightMargin = heightMargin;
P.mWidthMargin = widthMargin;
return this;
}
/**
* 添加默认动画
*
* @return
*/
public Builder addDefaultAnimation() {
P.mAnimation = R.style.dialog_scale_anim;
return this;
}
/**
* 设置动画
*
* @param styleAnimation
* @return
*/
public Builder setAnimation(int styleAnimation) {
P.mAnimation = styleAnimation;
return this;
}
/**
* 设置点击事件
*
* @param viewId
* @param onClickListener
* @return
*/
public Builder setOnClickListener(int viewId, View.OnClickListener onClickListener) {
P.mClickArray.put(viewId, onClickListener);
return this;
}
public Builder setOnLongClickListener(int viewId, View.OnLongClickListener onLongClickListener) {
P.mLondClickArray.put(viewId, onLongClickListener);
return this;
}
/**
* Sets whether the dialog is cancelable or not. Default is true.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
P.mOnCancelListener = onCancelListener;
return this;
}
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
public Builder setOnKeyListener(OnKeyListener onKeyListener) {
P.mOnKeyListener = onKeyListener;
return this;
}
public AlertDialog create() {
// Context has already been wrapped with the appropriate theme.
final AlertDialog dialog = new AlertDialog(P.mContext, P.mThemeResId);
P.apply(dialog.mAlert);
dialog.setCancelable(P.mCancelable);
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
public AlertDialog show() {
final AlertDialog dialog = create();
dialog.show();
return dialog;
}
}
}
④ 样式
在设置弹窗的样式和弹窗出现的方式,在themes.xml下新增如下代码:
代码语言:javascript复制 <style name="loading_dialog" parent="android:style/Theme.Dialog">
"android:windowFrame">@null
"android:windowNoTitle">true
"android:windowBackground">@drawable/shape_bg_white_radius_6
"android:windowIsFloating">true
"android:windowContentOverlay">@null
style>
<style name="dialog" parent="@android:style/Theme.Dialog">
"android:windowFrame">@null
"android:windowIsFloating">true
"android:windowIsTranslucent">true
"android:windowBackground">@android:color/transparent
"android:backgroundDimEnabled">true
"android:windowNoTitle">true
style>
<style name="dialog_from_bottom_anim">
"android:windowEnterAnimation">@anim/dialog_from_bottom_anim_in
"android:windowExitAnimation">@anim/dialog_from_bottom_anim_out
style>
<style name="dialog_from_top_anim">
"android:windowEnterAnimation">@anim/dialog_from_top_anim_in
"android:windowExitAnimation">@anim/dialog_from_top_anim_out
style>
<style name="dialog_scale_anim">
"android:windowEnterAnimation">@anim/dialog_scale_anim_in
"android:windowExitAnimation">@anim/dialog_scale_anim_out
style>
这里还用到动画样式文件,在res文件夹下新建一个anim文件夹,里面定义了7个xml文件,如下所示: 新建dialog_from_bottom_anim_in.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="400"
android:fromXDelta="0"
android:fromYDelta="1000"
android:toXDelta="0"
android:toYDelta="0" />
set>
dialog_from_bottom_anim_out.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="400"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="1000" />
set>
dialog_from_top_anim_in.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromYDelta="-100%"
android:toYDelta="0" />
set>
dialog_from_top_anim_out.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromYDelta="0"
android:toYDelta="-100%" />
set>
dialog_scale_anim_in.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android" >
<scale
android:duration="135"
android:fromXScale="0.8"
android:fromYScale="0.8"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.05"
android:toYScale="1.05" />
<scale
android:duration="105"
android:fromXScale="1.05"
android:fromYScale="1.05"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="135"
android:toXScale="0.95"
android:toYScale="0.95" />
<scale
android:duration="60"
android:fromXScale="0.95"
android:fromYScale="0.95"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="240"
android:toXScale="1.0"
android:toYScale="1.0" />
<alpha
android:duration="90"
android:fromAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />
set>
dialog_scale_anim_out.xml,代码如下:
代码语言:javascript复制<set xmlns:android="http://schemas.android.com/apk/res/android" >
<scale
android:duration="150"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.6"
android:toYScale="0.6" />
<alpha
android:duration="150"
android:fromAlpha="1.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="0.0" />
set>
loading_animation.xml,代码如下:
代码语言:javascript复制<set android:shareInterpolator="false" xmlns:android="http://schemas.android.com/apk/res/android">
<rotate
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="0"
android:toDegrees=" 360"
android:duration="1500"
android:startOffset="-1"
android:repeatMode="restart"
android:repeatCount="-1"/>
set>
这里还有一个shape_bg_white_radius_6.xml样式,在drawable中创建,里面的代码如下:
代码语言:javascript复制<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="24dp"/>
<solid android:color="@color/white"/>
shape>
同样再创建一个shape_bg_white_radius_12.xml,代码如下:
代码语言:javascript复制<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp"/>
<solid android:color="@color/white"/>
shape>
还有一个shape_bg_white_radius_24.xml,代码如下:
代码语言:javascript复制<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="24dp"/>
<solid android:color="@color/white"/>
shape>
⑤ 布局
在本文章将会创建三个弹窗布局,一个用于表示加载状态,一个用于表示修改用户信息,最后一个用于输入信息。
在layout下新建一个dialog_edit.xml,代码如下:
代码语言:javascript复制<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@drawable/shape_bg_white_radius_12">
<TextView
android:id="@ id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="标题"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@ id/et_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@ id/tv_title"
android:layout_margin="12dp"
android:maxLength="18"
android:singleLine="true"
android:textSize="@dimen/sp_14" />
<View
android:id="@ id/v_line"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@ id/et_content"
android:background="@color/line" />
<TextView
android:id="@ id/tv_cancel"
android:layout_width="150dp"
android:layout_height="50dp"
android:textSize="@dimen/sp_14"
android:layout_below="@ id/v_line"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:text="取消" />
<View
android:layout_width="1dp"
android:layout_height="50dp"
android:layout_below="@ id/v_line"
android:layout_centerHorizontal="true"
android:background="@color/line" />
<TextView
android:id="@ id/tv_sure"
android:layout_width="150dp"
android:layout_height="50dp"
android:textColor="@color/purple_500"
android:layout_below="@ id/v_line"
android:textSize="@dimen/sp_14"
android:layout_toEndOf="@ id/tv_cancel"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:text="确定" />
RelativeLayout>
layout>
在layout下新建一个dialog_loading.xml,代码如下:
代码语言:javascript复制<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@ id/dialog_view"
android:orientation="vertical"
android:layout_width="120dp"
android:layout_height="120dp"
android:gravity="center"
android:padding="10dp">
<ImageView
android:id="@ id/iv_loading"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@mipmap/ic_loading" />
<TextView
android:id="@ id/tv_loading_tx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:maxLines="1"
android:text="Loading..."
android:textColor="@color/purple_500"
android:textSize="14sp" />
LinearLayout>
这里有一个图标
放在mipmap下。
最后在layout下新建一个dialog_modify_user_info.xml,里面的代码如下:
代码语言:javascript复制<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="330dp"
android:layout_height="wrap_content"
android:background="@drawable/shape_bg_white_radius_24"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="修改用户信息"
android:textColor="@color/purple_500"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/line" />
<TextView
android:id="@ id/tv_modify_avatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:padding="12dp"
android:text="修改头像"
android:textColor="@color/black"
android:textSize="16sp" />
<LinearLayout
android:id="@ id/lay_modify_avatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@ id/tv_album_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/line"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:padding="12dp"
android:text="相册选择"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:id="@ id/tv_camera_photo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/line"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:padding="12dp"
android:text="相机拍照"
android:textColor="@color/black"
android:textSize="16sp" />
LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/line" />
<TextView
android:id="@ id/tv_modify_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:padding="12dp"
android:text="修改昵称"
android:textColor="@color/black"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/line" />
<TextView
android:id="@ id/tv_modify_Introduction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:padding="12dp"
android:text="修改简介"
android:textColor="@color/black"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/line" />
<TextView
android:id="@ id/tv_close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="关闭"
android:textColor="@color/purple_500"
android:textSize="16sp" />
LinearLayout>
layout>
这里的准备工作就都做好了,后面会用到,先不着急,然后在BaseActivity中增加一个加载弹窗,
代码语言:javascript复制 private LoadingDialog loadingDialog;
/**
* 显示加载弹窗
*/
protected void showLoading() {
loadingDialog = new LoadingDialog(this);
loadingDialog.show();
}
/**
* 显示加载弹窗
*
* @param isClose true 则点击其他区域弹窗关闭, false 不关闭。
*/
protected void showLoading(boolean isClose) {
loadingDialog = new LoadingDialog(this, isClose);
}
/**
* 隐藏加载弹窗
*/
protected void dismissLoading() {
if (loadingDialog != null) {
loadingDialog.dismiss();
}
}
这样在Activity中就可以直接使用,显示加载弹窗,隐藏加载弹窗。
三、权限请求
权限在Android上是一个麻烦但是又不得不做的事情,如果你要是还是Android6.0以下的手机就可以不用管这些,但是很可惜现在都是Android10,11了,因此我们还需要做兼容。
① 权限配置
因为要用到文件读写和相机,所以就需要在AndroidManifest.xml中增加如下代码:
代码语言:javascript复制 <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
这还没有完的,在Android10.0上要访问文件,需要在application比前中添加
代码语言:javascript复制 android:requestLegacyExternalStorage="true"
如下图所示:
同事我们还需要兼容Android7.0,在xml文件夹下新建一个file_paths.xml,里面的代码如下:
代码语言:javascript复制<resources>
<paths>
<external-files-path
name="images"
path="Pictures" />
paths>
resources>
然后我们在AndroidManifest.xml中配置它,代码如下:
代码语言:javascript复制 <provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
provider>
添加位置如下:
② 权限工具类
我这里可以自己写一个工具类,当然也可以用第三方框架,在utils包下新建一个PermissionUtils类,里面的代码如下:
代码语言:javascript复制public class PermissionUtils {
private static PermissionUtils mInstance;
public static final String READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE;
public static final String WRITE_EXTERNAL_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE;
public static final String CAMERA = Manifest.permission.CAMERA;
public static final int REQUEST_STORAGE_CODE = 1001;
public static final int REQUEST_CAMERA_CODE = 1002;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_CODE = 1000;
public static PermissionUtils getInstance() {
if (mInstance == null) {
synchronized (PermissionUtils.class) {
if (mInstance == null) {
mInstance = new PermissionUtils();
}
}
}
return mInstance;
}
/**
* 检查是有拥有某权限
*
* @param permission 权限名称
* @return true 有 false 没有
*/
public static boolean hasPermission(Activity activity, String permission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
}
}
/**
* 通过权限名称获取请求码
*
* @param permissionName 权限名称
* @return requestCode 权限请求码
*/
private static int getPermissionRequestCode(String permissionName) {
int requestCode;
switch (permissionName) {
case READ_EXTERNAL_STORAGE:
case WRITE_EXTERNAL_STORAGE:
requestCode = REQUEST_STORAGE_CODE;
break;
case CAMERA:
requestCode = REQUEST_CAMERA_CODE;
break;
default:
requestCode = 1000;
break;
}
return requestCode;
}
/**
* 请求权限
*
* @param permission 权限名称
*/
public static void requestPermission(Activity activity, String permission) {
int requestCode = getPermissionRequestCode(permission);
//请求此权限
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
}
}
然后因为权限请求是Activity有关系,那么我们可以在BaseActivity中再封装一层,
代码语言:javascript复制 /**
* 打开相册请求码
*/
protected static final int SELECT_PHOTO_CODE = 2000;
/**
* 打开相机请求码
*/
protected static final int TAKE_PHOTO_CODE = 2001;
添加两个请求吗,因为打开相机和相册都需要跳转到系统的页面,还需要获取返回的数据,这里我就提前定义好,然后在onCreate中对PermissionUtils进行初始化。
在BaseActivity中添加如下代码:
代码语言:javascript复制 /**
* 当前是否在Android11.0及以上
*/
protected boolean isAndroid11() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
}
/**
* 当前是否在Android10.0及以上
*/
protected boolean isAndroid10() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
/**
* 当前是否在Android7.0及以上
*/
protected boolean isAndroid7() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
/**
* 当前是否在Android6.0及以上
*/
protected boolean isAndroid6() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
protected boolean isStorageManager() {
return Environment.isExternalStorageManager();
}
protected boolean hasPermission(String permissionName) {
return PermissionUtils.hasPermission(this, permissionName);
}
protected void requestPermission(String permissionName) {
PermissionUtils.requestPermission(this, permissionName);
}
/**
* 请求外部存储管理 Android11版本时获取文件读写权限时调用
*/
protected void requestManageExternalStorage() {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" getPackageName()));
startActivityForResult(intent, PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE_CODE);
}
定义了一些需要用到的方法。下面进行DataBinding使用,弹窗中怎么获取DataBinding。
四、DataBinding
首先在activity_home.xml中添加 , 代码如下:
代码语言:javascript复制 <data>
<variable
name="homeViewModel"
type="com.llw.mvvm.viewmodels.HomeViewModel" />
data>
然后修改主页面的头像数据DataBinding,代码如下:
代码语言:javascript复制 <com.llw.mvvm.view.CustomImageView
android:id="@ id/iv_avatar"
localUrl="@{homeViewModel.user.avatar}"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="0.5dp"
android:scaleType="centerCrop"
android:src="@drawable/logo"
app:shapeAppearanceOverlay="@style/circleImageStyle"
app:strokeColor="@color/white"
app:strokeWidth="1dp" />
这里的localUrl需要我们再去CustomImageView类中定义,在CustomImageView中添加如下代码:
代码语言:javascript复制 private static final RequestOptions OPTIONS_LOCAL = new RequestOptions()
.placeholder(R.drawable.logo)//图片加载出来前,显示的图片
.fallback(R.drawable.logo) //url为空的时候,显示的图片
.error(R.mipmap.ic_loading_failed)//图片加载失败后,显示的图片
.diskCacheStrategy(DiskCacheStrategy.NONE)//不做磁盘缓存
.skipMemoryCache(true);
@BindingAdapter(value = {"localUrl"}, requireAll = false)
public static void setLocalUrl(ImageView imageView, String url) {
Glide.with(BaseApplication.getContext()).load(url).apply(OPTIONS_LOCAL).into(imageView);
}
然后就是在nav_header.xml中绑定DataBinding,里面的代码如下:
代码语言:javascript复制<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="homeViewModel"
type="com.llw.mvvm.viewmodels.HomeViewModel" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:id="@ id/lay_user_info"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@color/purple_500">
<com.llw.mvvm.view.CustomImageView
android:id="@ id/iv_avatar"
localUrl="@{homeViewModel.user.avatar}"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerVertical="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:padding="1dp"
android:scaleType="centerCrop"
android:src="@drawable/logo"
app:shapeAppearanceOverlay="@style/circleImageStyle"
app:strokeColor="@color/white"
app:strokeWidth="2dp" />
<TextView
android:id="@ id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@ id/iv_avatar"
android:layout_marginTop="16dp"
android:layout_toEndOf="@ id/iv_avatar"
android:text="@{homeViewModel.user.nickname ?? homeViewModel.defaultName}"
android:textColor="#FFF"
android:textSize="16sp" />
<TextView
android:id="@ id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@ id/tv_name"
android:layout_marginTop="8dp"
android:layout_toEndOf="@ id/iv_avatar"
android:text="@{homeViewModel.user.introduction ?? homeViewModel.defaultIntroduction}"
android:textColor="#FFF"
android:textSize="14sp" />
RelativeLayout>
LinearLayout>
layout>
这里面的这一行代码需要说一下
代码语言:javascript复制homeViewModel.user.nickname ?? homeViewModel.defaultName
这一行代码就等同于
代码语言:javascript复制homeViewModel.user.nickname != null ? homeViewModel.user.nickname : homeViewModel.defaultName
这个defaultName是我前面设置的默认值,因为注册时可能不会填写昵称和简介。这里要让这个默认值起作用,在保存用户信息的使用。这里需要修改注册页面中的默认值,从之前的空字符串改成null,这样在xml中的判断值才会有作用,同时及时你的值为null,在xml中也不会报错,这是DataBinding做了处理,类似于Kotlin中的空安全。
这里的DataBinding主要实现两个功能,第一个是HomeActivity的标题栏头像能够根据用户修改图片变化而变化,没有修改则使用默认的头像,第二个就是NavigationView中的head_layout也是通过用户手动去修改昵称、简介、头像时发生变化。
五、工具类
很快就要进入主要内容了,在代码中我们经常会用到一些工具类,比如dp转px,时间处理、Bitmp处理,相机图片处理,鉴于在后面我将会用到这些工具类,现在就给贴出来。这里的工具类都放在utils包下面,新建SizeUtils类,代码如下:
代码语言:javascript复制public final class SizeUtils {
private SizeUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* Value of dp to value of px.
*
* @param dpValue The value of dp.
* @return value of px
*/
public static int dp2px(Context context, final float dpValue) {
final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale 0.5f);
}
/**
* Value of px to value of dp.
*
* @param pxValue The value of px.
* @return value of dp
*/
public static int px2dp(Context context, final float pxValue) {
final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density;
return (int) (pxValue / scale 0.5f);
}
/**
* Value of sp to value of px.
*
* @param spValue The value of sp.
* @return value of px
*/
public static int sp2px(Context context, final float spValue) {
final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale 0.5f);
}
/**
* Value of px to value of sp.
*
* @param pxValue The value of px.
* @return value of sp
*/
public static int px2sp(Context context, final float pxValue) {
final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale 0.5f);
}
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters unit and value
* are as in {@link TypedValue#TYPE_DIMENSION}.
*
* @param value The value to apply the unit to.
* @param unit The unit to convert from.
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
public static float applyDimension(Context context, final float value, final int unit) {
DisplayMetrics metrics = context.getApplicationContext().getResources().getDisplayMetrics();
switch (unit) {
case TypedValue.COMPLEX_UNIT_PX:
return value;
case TypedValue.COMPLEX_UNIT_DIP:
return value * metrics.density;
case TypedValue.COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case TypedValue.COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f / 72);
case TypedValue.COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case TypedValue.COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f / 25.4f);
}
return 0;
}
/**
* Force get the size of view.
* e.g.
* * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
* Override
* public void onGetSize(final View view) {
* view.getWidth();
* }
* });
*
*
* @param view The view.
* @param listener The get size listener.
*/
public static void forceGetViewSize(final View view, final onGetSizeListener listener) {
view.post(new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onGetSize(view);
}
}
});
}
/**
* Return the width of view.
*
* @param view The view.
* @return the width of view
*/
public static int getMeasuredWidth(final View view) {
return measureView(view)[0];
}
/**
* Return the height of view.
*
* @param view The view.
* @return the height of view
*/
public static int getMeasuredHeight(final View view) {
return measureView(view)[1];
}
/**
* Measure the view.
*
* @param view The view.
* @return arr[0]: view's width, arr[1]: view's height
*/
public static int[] measureView(final View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
}
int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width);
int lpHeight = lp.height;
int heightSpec;
if (lpHeight > 0) {
heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
} else {
heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
view.measure(widthSpec, heightSpec);
return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()};
}
public interface onGetSizeListener {
void onGetSize(View view);
}
}
EasyDate类,代码如下:
代码语言:javascript复制public final class EasyDate {
public static final String STANDARD_TIME = "yyyy-MM-dd HH:mm:ss";
public static final String FULL_TIME = "yyyy-MM-dd HH:mm:ss.SSS";
public static final String YEAR_MONTH_DAY = "yyyy-MM-dd";
public static final String YEAR_MONTH_DAY_CN = "yyyy年MM月dd号";
public static final String HOUR_MINUTE_SECOND = "HH:mm:ss";
public static final String HOUR_MINUTE_SECOND_CN = "HH时mm分ss秒";
public static final String YEAR = "yyyy";
public static final String MONTH = "MM";
public static final String DAY = "dd";
public static final String HOUR = "HH";
public static final String MINUTE = "mm";
public static final String SECOND = "ss";
public static final String MILLISECOND = "SSS";
public static final String YESTERDAY = "昨天";
public static final String TODAY = "今天";
public static final String TOMORROW = "明天";
public static final String SUNDAY = "星期日";
public static final String MONDAY = "星期一";
public static final String TUESDAY = "星期二";
public static final String WEDNESDAY = "星期三";
public static final String THURSDAY = "星期四";
public static final String FRIDAY = "星期五";
public static final String SATURDAY = "星期六";
public static final String[] weekDays = {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};
/**
* 获取标准时间
*
* @return 例如 2021-07-01 10:35:53
*/
public static String getDateTime() {
return new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(new Date());
}
/**
* 获取完整时间
*
* @return 例如 2021-07-01 10:37:00.748
*/
public static String getFullDateTime() {
return new SimpleDateFormat(FULL_TIME, Locale.CHINESE).format(new Date());
}
/**
* 获取年月日(今天)
*
* @return 例如 2021-07-01
*/
public static String getTheYearMonthAndDay() {
return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.CHINESE).format(new Date());
}
/**
* 获取年月日
*
* @return 例如 2021年07月01号
*/
public static String getTheYearMonthAndDayCn() {
return new SimpleDateFormat(YEAR_MONTH_DAY_CN, Locale.CHINESE).format(new Date());
}
/**
* 获取年月日
* @param delimiter 分隔符
* @return 例如 2021年07月01号
*/
public static String getTheYearMonthAndDayDelimiter(CharSequence delimiter) {
return new SimpleDateFormat(YEAR delimiter MONTH delimiter DAY, Locale.CHINESE).format(new Date());
}
/**
* 获取时分秒
*
* @return 例如 10:38:25
*/
public static String getHoursMinutesAndSeconds() {
return new SimpleDateFormat(HOUR_MINUTE_SECOND, Locale.CHINESE).format(new Date());
}
/**
* 获取时分秒
*
* @return 例如 10时38分50秒
*/
public static String getHoursMinutesAndSecondsCn() {
return new SimpleDateFormat(HOUR_MINUTE_SECOND_CN, Locale.CHINESE).format(new Date());
}
/**
* 获取时分秒
* @param delimiter 分隔符
* @return 例如 2021/07/01
*/
public static String getHoursMinutesAndSecondsDelimiter(CharSequence delimiter) {
return new SimpleDateFormat(HOUR delimiter MINUTE delimiter SECOND, Locale.CHINESE).format(new Date());
}
/**
* 获取年
*
* @return 例如 2021
*/
public static String getYear() {
return new SimpleDateFormat(YEAR, Locale.CHINESE).format(new Date());
}
/**
* 获取月
*
* @return 例如 07
*/
public static String getMonth() {
return new SimpleDateFormat(MONTH, Locale.CHINESE).format(new Date());
}
/**
* 获取天
*
* @return 例如 01
*/
public static String getDay() {
return new SimpleDateFormat(DAY, Locale.CHINESE).format(new Date());
}
/**
* 获取小时
*
* @return 例如 10
*/
public static String getHour() {
return new SimpleDateFormat(HOUR, Locale.CHINESE).format(new Date());
}
/**
* 获取分钟
*
* @return 例如 40
*/
public static String getMinute() {
return new SimpleDateFormat(MINUTE, Locale.CHINESE).format(new Date());
}
/**
* 获取秒
*
* @return 例如 58
*/
public static String getSecond() {
return new SimpleDateFormat(SECOND, Locale.CHINESE).format(new Date());
}
/**
* 获取毫秒
*
* @return 例如 666
*/
public static String getMilliSecond() {
return new SimpleDateFormat(MILLISECOND, Locale.CHINESE).format(new Date());
}
/**
* 获取时间戳
*
* @return 例如 1625107306051
*/
public static long getTimestamp() {
return System.currentTimeMillis();
}
/**
* 将时间转换为时间戳
*
* @param time 例如 2021-07-01 10:44:11
* @return 1625107451000
*/
public static long dateToStamp(String time) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE);
Date date = null;
try {
date = simpleDateFormat.parse(time);
} catch (ParseException e) {
e.printStackTrace();
}
return Objects.requireNonNull(date).getTime();
}
/**
* 将时间戳转换为时间
*
* @param timeMillis 例如 1625107637084
* @return 例如 2021-07-01 10:47:17
*/
public static String stampToDate(long timeMillis) {
return new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(new Date(timeMillis));
}
/**
* 获取今天是星期几
*
* @return 例如 星期四
*/
public static String getTodayOfWeek() {
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
int index = cal.get(Calendar.DAY_OF_WEEK) - 1;
if (index < 0) {
index = 0;
}
return weekDays[index];
}
/**
* 根据输入的日期时间计算是星期几
*
* @param dateTime 例如 2021-06-20
* @return 例如 星期日
*/
public static String getWeek(String dateTime) {
Calendar cal = Calendar.getInstance();
if ("".equals(dateTime)) {
cal.setTime(new Date(System.currentTimeMillis()));
} else {
SimpleDateFormat sdf = new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault());
Date date;
try {
date = sdf.parse(dateTime);
} catch (ParseException e) {
date = null;
e.printStackTrace();
}
if (date != null) {
cal.setTime(new Date(date.getTime()));
}
}
return weekDays[cal.get(Calendar.DAY_OF_WEEK) - 1];
}
/**
* 获取输入日期的昨天
*
* @param date 例如 2021-07-01
* @return 例如 2021-06-30
*/
public static String getYesterday(Date date) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.DATE, -1);
date = calendar.getTime();
return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date);
}
/**
* 获取输入日期的明天
*
* @param date 例如 2021-07-01
* @return 例如 2021-07-02
*/
public static String getTomorrow(Date date) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
date = calendar.getTime();
return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date);
}
/**
* 根据年月日计算是星期几并与当前日期判断 非昨天、今天、明天 则以星期显示
*
* @param dateTime 例如 2021-07-03
* @return 例如 星期六
*/
public static String getDayInfo(String dateTime) {
String dayInfo;
String yesterday = getYesterday(new Date());
String today = getTheYearMonthAndDay();
String tomorrow = getTomorrow(new Date());
if (dateTime.equals(yesterday)) {
dayInfo = YESTERDAY;
} else if (dateTime.equals(today)) {
dayInfo = TODAY;
} else if (dateTime.equals(tomorrow)) {
dayInfo = TOMORROW;
} else {
dayInfo = getWeek(dateTime);
}
return dayInfo;
}
/**
* 获取本月天数
*
* @return 例如 31
*/
public static int getCurrentMonthDays() {
Calendar calendar = Calendar.getInstance();
//把日期设置为当月第一天
calendar.set(Calendar.DATE, 1);
//日期回滚一天,也就是最后一天
calendar.roll(Calendar.DATE, -1);
return calendar.get(Calendar.DATE);
}
/**
* 获得指定月的天数
*
* @param year 例如 2021
* @param month 例如 7
* @return 例如 31
*/
public static int getMonthDays(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
//把日期设置为当月第一天
calendar.set(Calendar.DATE, 1);
//日期回滚一天,也就是最后一天
calendar.roll(Calendar.DATE, -1);
return calendar.get(Calendar.DATE);
}
}
CameraUtils类,代码如下:
代码语言:javascript复制public class CameraUtils {
/**
* 相机Intent
* @param context
* @param outputImagePath
* @return
*/
public static Intent getTakePhotoIntent(Context context, File outputImagePath) {
// 激活相机
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 判断存储卡是否可以用,可用进行存储
if (hasSdcard()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// 从文件中创建uri
Uri uri = Uri.fromFile(outputImagePath);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
} else {
//兼容android7.0 使用共享文件的形式
ContentValues contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, outputImagePath.getAbsolutePath());
Uri uri = context.getApplicationContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}
}
return intent;
}
/**
* 相册Intent
* @return
*/
public static Intent getSelectPhotoIntent() {
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
return intent;
}
/**
* 判断sdcard是否被挂载
*/
public static boolean hasSdcard() {
return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED);
}
/**
* 4.4及以上系统处理图片的方法
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public static String getImageOnKitKatPath(Intent data, Context context) {
String imagePath = null;
Uri uri = data.getData();
Log.d("uri=intent.getData :", "" uri);
if (DocumentsContract.isDocumentUri(context, uri)) {
//数据表里指定的行
String docId = DocumentsContract.getDocumentId(uri);
Log.d("getDocumentId(uri) :", "" docId);
Log.d("uri.getAuthority() :", "" uri.getAuthority());
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1];
String selection = MediaStore.Images.Media._ID "=" id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, context);
} else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null, context);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
imagePath = getImagePath(uri, null, context);
}
return imagePath;
}
/**
* 通过uri和selection来获取真实的图片路径,从相册获取图片时要用
*/
public static String getImagePath(Uri uri, String selection, Context context) {
String path = null;
Cursor cursor = context.getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
/**
* 更改图片显示角度
* @param filepath
* @param orc_bitmap
* @param iv
*/
public static void ImgUpdateDirection(String filepath, Bitmap orc_bitmap, ImageView iv) {
//图片旋转的角度
int digree = 0;
//根据图片的filepath获取到一个ExifInterface的对象
ExifInterface exif = null;
try {
exif = new ExifInterface(filepath);
if (exif != null) {
// 读取图片中相机方向信息
int ori = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
// 计算旋转角度
switch (ori) {
case ExifInterface.ORIENTATION_ROTATE_90:
digree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
digree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
digree = 270;
break;
default:
digree = 0;
break;
}
}
//如果图片不为0
if (digree != 0) {
// 旋转图片
Matrix m = new Matrix();
m.postRotate(digree);
orc_bitmap = Bitmap.createBitmap(orc_bitmap, 0, 0, orc_bitmap.getWidth(),
orc_bitmap.getHeight(), m, true);
}
if (orc_bitmap != null) {
iv.setImageBitmap(orc_bitmap);
}
} catch (IOException e) {
e.printStackTrace();
exif = null;
}
}
/**
* 4.4以下系统处理图片的方法
*/
public static String getImageBeforeKitKatPath(Intent data, Context context) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null, context);
return imagePath;
}
/**
* 比例压缩
* @param image
* @return
*/
public static Bitmap compression(Bitmap image) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
if (outputStream.toByteArray().length / 1024 > 1024) {
//重置outputStream即清空outputStream
outputStream.reset();
//这里压缩50%,把压缩后的数据存放到baos中
image.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
//开始读入图片,此时把options.inJustDecodeBounds 设回true了
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
options.inJustDecodeBounds = false;
int outWidth = options.outWidth;
int outHeight = options.outHeight;
//现在主流手机比较多是800*480分辨率,所以高和宽我们设置为
float height = 800f;//这里设置高度为800f
float width = 480f;//这里设置宽度为480f
//缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
int zoomRatio = 1;//be=1表示不缩放
if (outWidth > outHeight && outWidth > width) {//如果宽度大的话根据宽度固定大小缩放
zoomRatio = (int) (options.outWidth / width);
} else if (outWidth < outHeight && outHeight > height) {//如果高度高的话根据宽度固定大小缩放
zoomRatio = (int) (options.outHeight / height);
}
if (zoomRatio <= 0) {
zoomRatio = 1;
}
options.inSampleSize = zoomRatio;//设置缩放比例
options.inPreferredConfig = Bitmap.Config.RGB_565;//降低图片从ARGB888到RGB565
//重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
//压缩好比例大小后再进行质量压缩
bitmap = BitmapFactory.decodeStream(inputStream, null, options);
return bitmap;
}
}
BitmapUtils类,代码如下(本文中没有用到,因为我没有服务器,但是如果你需要上传到服务器的话,常规做法是将图片转成Base64,发送给服务器):
代码语言:javascript复制public class BitmapUtils {
/**
* bitmap转为base64
*
* @param bitmap
* @return
*/
public static String bitmapToBase64(Bitmap bitmap) {
String result = null;
ByteArrayOutputStream baos = null;
try {
if (bitmap != null) {
baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
baos.flush();
baos.close();
byte[] bitmapBytes = baos.toByteArray();
result = Base64.encodeToString(bitmapBytes, Base64.DEFAULT);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.flush();
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* base64转为bitmap
*
* @param base64Data
* @return
*/
public static Bitmap base64ToBitmap(String base64Data) {
byte[] bytes = Base64.decode(base64Data, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
/**
* url转bitmap
* @param url
* @return
*/
public static Bitmap urlToBitmap(final String url){
final Bitmap[] bitmap = {null};
new Thread(() -> {
URL imageurl = null;
try {
imageurl = new URL(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
try {
HttpURLConnection conn = (HttpURLConnection)imageurl.openConnection();
conn.setDoInput(true);
conn.connect();
InputStream is = conn.getInputStream();
bitmap[0] = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
return bitmap[0];
}
}
六、核心环节
下面的代码都写在HomeActivity中,首先声明一些变量
代码语言:javascript复制 //可输入弹窗
private AlertDialog editDialog = null;
//修改用户信息弹窗
private AlertDialog modifyUserInfoDialog = null;
//是否显示修改头像的两种方式
private boolean isShow = false;
//用于保存拍照图片的uri
private Uri mCameraUri;
// 用于保存图片的文件路径,Android 10以下使用图片路径访问图片
private String mCameraImagePath;
首先我们在onCreate方法中,增加一行显示加载弹窗的代码,这个方法是写在BaseActivity中,而当前的HomeActivity是要继承自BaseActivity的。
代码语言:javascript复制 //显示加载弹窗
showLoading();
添加的位置
然后就是在initView方法中增加代码:
代码语言:javascript复制 //获取NavigationView的headerLayout视图
View headerView = binding.navView.getHeaderView(0);
headerView.setOnClickListener(v -> showModifyUserInfoDialog());
//获取headerLayout视图的Binding
NavHeaderBinding headerBinding = DataBindingUtil.bind(headerView);
//获取本地用户信息
homeViewModel.getUser();
//用户信息发生改变时给对应的xml设置数据源也就是之前写好的ViewModel。
homeViewModel.user.observe(this, user -> {
localUser = user;
binding.setHomeViewModel(homeViewModel);
if (headerBinding != null) {
headerBinding.setHomeViewModel(homeViewModel);
}
//隐藏加载弹窗
dismissLoading();
});
添加位置如下图
这里的代码很关键,首先是在HomeActivity中要获取到本地的User数据,这是通过HomeViewModel中的UserRepository去获取的,然后是获取之后通知xml去加载数据,这就是DataBinding的魅力,数据改变之后我们就隐藏掉加载弹窗,所以这一步很关键。
① 显示修改用户信息弹窗
如果不出意外的话,你是没有写showModifyUserInfoDialog方法的,因此这里肯定是红色的,那么你可以手动创建,也可以通过快捷键Alt Enter的方式快速创建方法,里面的代码如下:
代码语言:javascript复制 /**
* 显示修改用户弹窗
*/
private void showModifyUserInfoDialog() {
DialogModifyUserInfoBinding binding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_modify_user_info, null, false);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.addDefaultAnimation()
.setCancelable(true)
.setContentView(binding.getRoot())
.setWidthAndHeight(SizeUtils.dp2px(this, 300), LinearLayout.LayoutParams.WRAP_CONTENT)
.setOnClickListener(R.id.tv_modify_avatar, v -> {
//修改头像,点击显示修改头像的方式,再次点击隐藏修改方式
binding.layModifyAvatar.setVisibility(isShow ? View.GONE : View.VISIBLE);
isShow = !isShow;
}).setOnClickListener(R.id.tv_album_selection, v -> albumSelection())//相册选择
.setOnClickListener(R.id.tv_camera_photo, v -> cameraPhoto())//相机拍照
.setOnClickListener(R.id.tv_modify_nickname, v -> showEditDialog(0))//修改昵称
.setOnClickListener(R.id.tv_modify_Introduction, v -> showEditDialog(1))//修改简介
.setOnClickListener(R.id.tv_close, v -> modifyUserInfoDialog.dismiss())//关闭弹窗
.setOnDismissListener(dialog -> isShow = false);
modifyUserInfoDialog = builder.create();
modifyUserInfoDialog.show();
}
这里的方法是显示修改用户信息弹窗,当我们点击NavigationView的headerLayout时就会显示这个弹窗,那么这个弹窗里面做了什么呢?
首先是获取DataBinding,这里只是为了方便不写findViewById,不获取也没有关系就直接用布局,然后是在点击tv_modify_avatar的时候控制修改头像的布局的显示和隐藏,这里要是还想优化的话,可以增加一个动画效果,例如向下展开显示,向上收缩隐藏。我这里就不搞这些花里胡哨的东西了。然后就是这里有四个方法的调用,实际上是三个方法,有一个是复用的,只不过是传入的类型不同。
② 相册选取
这里我们从上往下来写这些方法,首先是albumSelection方法,我们切换头像有两种方式,这里是通过相册去选取。
代码语言:javascript复制 /**
* 相册选择
*/
private void albumSelection() {
modifyUserInfoDialog.dismiss();
if (isAndroid11()) {
//请求打开外部存储管理
requestManageExternalStorage();
} else {
if (!isAndroid6()) {
//打开相册
openAlbum();
return;
}
if (!hasPermission(PermissionUtils.READ_EXTERNAL_STORAGE)) {
requestPermission(PermissionUtils.READ_EXTERNAL_STORAGE);
return;
}
//打开相册
openAlbum();
}
}
这里我们首先是关闭之前的弹窗,然后检查用户是否在Android11,是的话请求打开外部存储管理的开关,不是再判断是不是Android6.0及以上版本,不是就不用请求动态权限,直接调用openAlbum打开相册,是就检查有没有获取读取存储文件的权限,没有获取就去请求这个权限,如果已经获取了就打开相册,我们先看打开外部存储管理的返回,
代码语言:javascript复制 /**
* 页面返回结果
*/
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
showMsg("未知原因");
return;
}
switch (requestCode) {
case PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE_CODE:
//从外部存储管理页面返回
if (!isStorageManager()) {
showMsg("未打开外部存储管理开关,无法打开相册,抱歉");
return;
}
if (!hasPermission(PermissionUtils.READ_EXTERNAL_STORAGE)) {
requestPermission(PermissionUtils.READ_EXTERNAL_STORAGE);
return;
}
//打开相册
openAlbum();
break;
}
}
这里我们对返回的结果要做处理,如果打开了则再检查是否有这个存储权限,请注意这里我没有去检查是不是Android6.0及以上版本,因为如果我有这个返回的话,那么毋庸置疑,肯定在Android6.0以上,就没有必要再去多此一举了,如果没有打开开关的话这里就会提示你。
下面我们再去看权限请求的回调,
代码语言:javascript复制 /**
* 权限请求结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull @NotNull String[] permissions, @NonNull @NotNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PermissionUtils.REQUEST_STORAGE_CODE:
//文件读写权限
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
showMsg("您拒绝了读写文件权限,无法打开相册,抱歉。");
return;
}
openAlbum();
break;
default:
break;
}
}
这里我们同样要对权限通过和不通过做处理,这一步弄清楚之后,就是真的要去打开相册了,调用openAlbum方法,方法代码如下:
代码语言:javascript复制 /**
* 打开相册
*/
private void openAlbum() {
startActivityForResult(CameraUtils.getSelectPhotoIntent(), SELECT_PHOTO_CODE);
}
一句话就搞定了,不过这里我用的startActivityForResult是已经过时的API了,但是还是可以用的,你也可以用新的API。当我们选择了一个图片之后会返回一个结果,也在onActivityResult回调中,那么我们在这个里面再加一个case。
代码语言:javascript复制 case SELECT_PHOTO_CODE:
//相册中选择图片返回
modifyAvatar(CameraUtils.getImageOnKitKatPath(data, this));
break;
这应该很好理解吧,然后我们保存返回的图片路径,这里又用到一个方法。方法代码如下:
代码语言:javascript复制 /**
* 修改头像
*/
private void modifyAvatar(String imagePath) {
if (!TextUtils.isEmpty(imagePath)) {
//保存到数据表中
modifyContent(2, imagePath);
Log.d(TAG, "modifyAvatar: " imagePath);
} else {
showMsg("图片获取失败");
}
}
这里是修改头像,如果获取到的图片不是空的就调用modifyContent方法去保存,方法代码如下:
代码语言:javascript复制 /**
* 修改内容
*
* @param type 类型 0:昵称 1:简介 2: 头像
* @param content 修改内容
*/
private void modifyContent(int type, String content) {
if (type == 0) {
localUser.setNickname(content);
} else if (type == 1) {
localUser.setIntroduction(content);
} else if (type == 2) {
localUser.setAvatar(content);
}
homeViewModel.updateUser(localUser);
homeViewModel.failed.observe(this, failed -> {
dismissLoading();
if ("200".equals(failed)) {
showMsg("修改成功");
}
});
}
因为要修改的三个数据都是字符串,所以我们可以写一个通用方法,用一个type来区分保存。这样就只用修改一个值了。虽然从代码上看像是俄罗斯套娃,但是逻辑就是这样的。 到这里为止,通过相册选取方式修改头像就写完了,下面来看通过相机拍照修改头像。运行效果如下图所示:
③ 相机拍照
回到我们之前的修改用户信息弹窗,现在第一个方法已经不报错了,下面写第二个方法cameraPhoto,代码如下:
代码语言:javascript复制 /**
* 相册拍照
*/
private void cameraPhoto() {
modifyUserInfoDialog.dismiss();
if (!isAndroid6()) {
//打开相机
openCamera();
return;
}
if (!hasPermission(PermissionUtils.CAMERA)) {
requestPermission(PermissionUtils.CAMERA);
return;
}
//打开相机
openCamera();
}
这里的逻辑我想不用再重复了,一目了然。下面是相机权限的回调,在onRequestPermissionsResult中增加一个case,代码如下:
代码语言:javascript复制 case PermissionUtils.REQUEST_CAMERA_CODE:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
showMsg("您拒绝了相机权限,无法打开相机,抱歉。");
return;
}
openCamera();
break;
如果通过权限就打开相机,打开相机要比相册麻烦一些,openCamera方法代码如下:
代码语言:javascript复制 /**
* 调起相机拍照
*/
private void openCamera() {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 判断是否有相机
if (captureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
Uri photoUri = null;
if (isAndroid10()) {
// 适配android 10 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
photoUri = getContentResolver().insert(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ?
MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
} else {
photoFile = createImageFile();
if (photoFile != null) {
mCameraImagePath = photoFile.getAbsolutePath();
if (isAndroid7()) {
//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri
photoUri = FileProvider.getUriForFile(this, getPackageName() ".fileprovider", photoFile);
} else {
photoUri = Uri.fromFile(photoFile);
}
}
}
mCameraUri = photoUri;
if (photoUri != null) {
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(captureIntent, TAKE_PHOTO_CODE);
}
}
}
下面就是拍照后的返回了,在onActivityResult中增加一个case,
代码语言:javascript复制 case TAKE_PHOTO_CODE:
//相机中拍照返回
modifyAvatar(isAndroid10() ? mCameraUri.toString() : mCameraImagePath);
break;
后面的代码就是复用的,因此我们可以运行一下了。
下面就是修改昵称和简介了
③ 修改昵称和简介
再回到修改用户弹窗哪里,现在只有一个方法了,showEditDialog代码如下:
代码语言:javascript复制 /**
* 显示可输入文字弹窗
* @param type 0 修改昵称 1 修改简介
*/
private void showEditDialog(int type) {
modifyUserInfoDialog.dismiss();
DialogEditBinding binding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_edit, null, false);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.addDefaultAnimation()
.setCancelable(true)
.setText(R.id.tv_title, type == 0 ? "修改昵称" : "修改简介")
.setContentView(binding.getRoot())
.setWidthAndHeight(SizeUtils.dp2px(this, 300), LinearLayout.LayoutParams.WRAP_CONTENT)
.setOnClickListener(R.id.tv_cancel, v -> editDialog.dismiss())
.setOnClickListener(R.id.tv_sure, v -> {
String content = binding.etContent.getText().toString().trim();
if (content.isEmpty()) {
showMsg(type == 0 ? "请输入昵称" : "请输入简介");
return;
}
if (type == 0 && content.length() > 10) {
showMsg("昵称过长,请输入8个以内汉字或字母");
return;
}
editDialog.dismiss();
showLoading();
//保存输入的值
modifyContent(type, content);
});
editDialog = builder.create();
binding.etContent.setHint(type == 0 ? "请输入昵称" : "请输入简介");
editDialog.show();
}
这一步就结束了,是不是很突然呢,后面的代码我们都已经写好了,下面运行一下:
这里其实还有优化空间,看你有没有感觉。好了,本篇文章就到这里,写作不易啊。山高水长,后会有期~
七、源码
GitHub:MVVM-Demo CSDN: MVVMDemo_7.rar