前言
在上一篇博客中完成了新闻详情数据的查看以及用户的注册登录,这篇文章中将对用户的信息进行增加和修改。会使用到文件读写、相机权限、自定义Dialog、相册选取和相机拍照。
正文
下面先进行数据库的升级,因为我们要更换用户的头像,因此首先用户表里面是需要一个头像的字段的,之前对数据库进行升级的时候都是直接添加一个表,那么这一次升级我们往表里面增加一个字段。
一、数据库升级
一般来说再设计数据库的时候就要想到一些因素,像增加表字段这种事情一般是出现在业务需求有改动的情况下,因此我们在设计表的时候可以想清楚有没有可能进行扩展,会怎样扩展。下面我们要往数据表User中增加一个avatar的字段,表示头像。
同时,增加get和set方法。
public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; }
然后进行数据库的升级,打开AppDatabase,增加如下代码:
/** * 版本升级迁移到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中的代码也需要更新,在里面增加如下代码:
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包下创建它,里面的代码如下:
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
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类,代码如下:
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,里面的代码如下:
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下新增如下代码:
<style name="loading_dialog" parent="android:style/Theme.Dialog"> <item name="android:windowFrame">@null</item> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@drawable/shape_bg_white_radius_24</item> <item name="android:windowIsFloating">true</item> <item name="android:windowContentOverlay">@null</item> </style> <!--自定义对话框--> <style name="dialog" parent="@android:style/Theme.Dialog"> <item name="android:windowFrame">@null</item> <item name="android:windowIsFloating">true</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:backgroundDimEnabled">true</item> <item name="android:windowNoTitle">true</item> </style> <!--对话框弹出和消失动画--> <style name="dialog_from_bottom_anim"> <item name="android:windowEnterAnimation">@anim/dialog_from_bottom_anim_in</item> <item name="android:windowExitAnimation">@anim/dialog_from_bottom_anim_out</item> </style> <style name="dialog_from_top_anim"> <item name="android:windowEnterAnimation">@anim/dialog_from_top_anim_in</item> <item name="android:windowExitAnimation">@anim/dialog_from_top_anim_out</item> </style> <style name="dialog_scale_anim"> <item name="android:windowEnterAnimation">@anim/dialog_scale_anim_in</item> <item name="android:windowExitAnimation">@anim/dialog_scale_anim_out</item> </style>
这里还用到动画样式文件,在res文件夹下新建一个anim文件夹,里面定义了7个xml文件,如下所示:
新建dialog_from_bottom_anim_in.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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中创建,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <corners android:radius="6dp"/> <solid android:color="@color/white"/> </shape>
同样再创建一个shape_bg_white_radius_12.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <corners android:radius="24dp"/> <solid android:color="@color/white"/> </shape>
⑤ 布局
在本文章将会创建三个弹窗布局,一个用于表示加载状态,一个用于表示修改用户信息,最后一个用于输入信息。
在layout下新建一个dialog_edit.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,代码如下:
<?xml version="1.0" encoding="utf-8"?> <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,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <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中增加一个加载弹窗,
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); loadingDialog.show(); } /** * 隐藏加载弹窗 */ protected void dismissLoading() { if (loadingDialog != null) { loadingDialog.dismiss(); } }
这样在Activity中就可以直接使用,显示加载弹窗,隐藏加载弹窗。
三、权限请求
权限在Android上是一个麻烦但是又不得不做的事情,如果你要是还是Android6.0以下的手机就可以不用管这些,但是很可惜现在都是Android10,11了,因此我们还需要做兼容。
① 权限配置
因为要用到文件读写和相机,所以就需要在AndroidManifest.xml中增加如下代码:
<!-- 相机权限 --> <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" /> <!-- 管理外部存储权限,Android11需要--> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
这还没有完的,在Android10.0上要访问文件,需要在application比前中添加
android:requestLegacyExternalStorage="true"
如下图所示:
同事我们还需要兼容Android7.0,在xml文件夹下新建一个file_paths.xml,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <paths> <!-- 这个是保存拍照图片的路径,必须配置。 --> <external-files-path name="images" path="Pictures" /> </paths> </resources>
然后我们在AndroidManifest.xml中配置它,代码如下:
<!-- Android7.0以后读取文件需要配置Provider --> <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类,里面的代码如下:
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中再封装一层,
/** * 打开相册请求码 */ protected static final int SELECT_PHOTO_CODE = 2000; /** * 打开相机请求码 */ protected static final int TAKE_PHOTO_CODE = 2001;
添加两个请求吗,因为打开相机和相册都需要跳转到系统的页面,还需要获取返回的数据,这里我就提前定义好,然后在onCreate中对PermissionUtils进行初始化。
在BaseActivity中添加如下代码:
/** * 当前是否在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中添加 , 代码如下:
<data> <variable name="homeViewModel" type="com.llw.mvvm.viewmodels.HomeViewModel" /> </data>
然后修改主页面的头像数据DataBinding,代码如下:
<!--圆形图片--> <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中添加如下代码:
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,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <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>
这里面的这一行代码需要说一下
homeViewModel.user.nickname ?? homeViewModel.defaultName
这一行代码就等同于
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类,代码如下:
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 <var>unit</var> and <var>value</var> * 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. * <p>e.g.</p> * <pre> * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() { * Override * public void onGetSize(final View view) { * view.getWidth(); * } * }); * </pre> * * @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类,代码如下:
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类,代码如下:
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,发送给服务器):
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中,首先声明一些变量
//可输入弹窗 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的。
//显示加载弹窗 showLoading();
添加的位置
然后就是在initView方法中增加代码:
//获取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的方式快速创建方法,里面的代码如下:
/** * 显示修改用户弹窗 */ 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方法,我们切换头像有两种方式,这里是通过相册去选取。
/** * 相册选择 */ 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打开相册,是就检查有没有获取读取存储文件的权限,没有获取就去请求这个权限,如果已经获取了就打开相册,我们先看打开外部存储管理的返回,
/** * 页面返回结果 */ @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以上,就没有必要再去多此一举了,如果没有打开开关的话这里就会提示你。
下面我们再去看权限请求的回调,
/** * 权限请求结果 */ @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方法,方法代码如下:
/** * 打开相册 */ private void openAlbum() { startActivityForResult(CameraUtils.getSelectPhotoIntent(), SELECT_PHOTO_CODE); }
一句话就搞定了,不过这里我用的startActivityForResult是已经过时的API了,但是还是可以用的,你也可以用新的API。当我们选择了一个图片之后会返回一个结果,也在onActivityResult回调中,那么我们在这个里面再加一个case。
case SELECT_PHOTO_CODE: //相册中选择图片返回 modifyAvatar(CameraUtils.getImageOnKitKatPath(data, this)); break;
这应该很好理解吧,然后我们保存返回的图片路径,这里又用到一个方法。方法代码如下:
/** * 修改头像 */ private void modifyAvatar(String imagePath) { if (!TextUtils.isEmpty(imagePath)) { //保存到数据表中 modifyContent(2, imagePath); Log.d(TAG, "modifyAvatar: " + imagePath); } else { showMsg("图片获取失败"); } }
这里是修改头像,如果获取到的图片不是空的就调用modifyContent方法去保存,方法代码如下:
/** * 修改内容 * * @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,代码如下:
/** * 相册拍照 */ private void cameraPhoto() { modifyUserInfoDialog.dismiss(); if (!isAndroid6()) { //打开相机 openCamera(); return; } if (!hasPermission(PermissionUtils.CAMERA)) { requestPermission(PermissionUtils.CAMERA); return; } //打开相机 openCamera(); }
这里的逻辑我想不用再重复了,一目了然。下面是相机权限的回调,在onRequestPermissionsResult中增加一个case,代码如下:
case PermissionUtils.REQUEST_CAMERA_CODE: if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { showMsg("您拒绝了相机权限,无法打开相机,抱歉。"); return; } openCamera(); break;
如果通过权限就打开相机,打开相机要比相册麻烦一些,openCamera方法代码如下:
/** * 调起相机拍照 */ 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,
case TAKE_PHOTO_CODE: //相机中拍照返回 modifyAvatar(isAndroid10() ? mCameraUri.toString() : mCameraImagePath); break;
后面的代码就是复用的,因此我们可以运行一下了。
下面就是修改昵称和简介了
③ 修改昵称和简介
再回到修改用户弹窗哪里,现在只有一个方法了,showEditDialog代码如下:
/** * 显示可输入文字弹窗 * @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(); }
这一步就结束了,是不是很突然呢,后面的代码我们都已经写好了,下面运行一下:
这里其实还有优化空间,看你有没有感觉。好了,本篇文章就到这里,写作不易啊。山高水长,后会有期~