Android 开发笔记2.0

本文主要记录一些在开发过程中可能要用到的知识点,持续更新哦!2.0正式上线

目录:

  1. git clone URL之后想切换远程分支只需要git checkout 分支就行,不用再-b了。

  2. 个别手机在锁屏页点击三个功能入口无效,原因:非Activity启动Activity需要加上标记为NEW_TASK。

3.ViewPager 中的 Fragment 可见性问题的埋点解决方案:

4.常用的APP下载地方,可以免登陆下载Google Play的应用:apkpure(需翻墙) apkmirror

5.监听home键广播启动Activity会延迟5s,通过PendingIntent.send()方法启动Activity也不可以立即启动。
6.flutter中的inheritedwidget做到的数据共享只是实现了他和他子树的数据共享而已,但是provider可以做到跨组件数据共享。

7.git 删除分支、重命名分支

8.释放动画资源是(一般是controller?.dispose)需要调用在super.dispose方法之前的,不然会报错。

9.用handler做延时操作时要注意内存泄漏的问题,以免快速退出activity时造成程序崩溃。

10.recyclerview中第一个,最后一个,和中间的item样式不同时可以在onBindViewHolder中根据position来对应设置。

11.fragment的replace, add, show, hide方法区别:replace会替换掉之前的fragment,并且走完其生命周期;hide和show的话之前的fragment并不会走完其生命周期。

12.在replaceFragment出现Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState错误。

13.版本适配的沉浸式状态栏

14.recyclerview同时点击多个item或者同时多次点击单个item引起crash

15.TextView中文英文数字混排导致的自动换行问题

16.SmartRefreshLayout的一些注意事项

17.Fresco设置圆角时的注意事项

18.点击不同tab请求数据时,为防止 List 改变的不够及时,可以将 LIst 的类型换成线程安全的 CopyOnWriteArrayList ,再加上一定时间的防触摸操作即可解决数据出现混乱的问题。

19.RecyclerView的缓存机制(参考链接:https://www.jianshu.com/p/3e9aa4bdaefd)

20.在MainActivity按下返回键不能完全退出应用

21.Android 10 授权读取媒体文件后还是报permission denied 错误。(Android 10 存储适配)

22.快手全屏视频广告播放黑屏

23.Android 不推荐使用枚举

24.打开系统页面

25.查看当前页面布局的具体情况

26.获取签名文件的SHA1值

27.android 与 h5 交互方式:

28.快速截屏模拟器的屏幕

29.AS Build窗口出现乱码

30.ViewPager禁止左右滑动

31.TabLayout 隐藏下划线

32.Android Studio 自动导包

33.TabLayout 实现部分自定义样式(自定义选中时的文字大小,样式,颜色)

34.打正式包时报下面的错误

35.git push免登录提交

36.敏感词校验

37.webview 白屏

38.每次刷新Recyclerview时,记录上次浏览的位置,并滑动到指定位置

39.Android 文件存储

40.实现录音功能

41.去除按钮阴影效果

42.自定义的对话框的背景、宽度、高度不生效

43.PopupWindow 出现在控件上方

44.实现 TextView 部分文字的点击监听(用户服务及隐私协议对话框)

45.Glide设置圆角图片

46.上传到OSS的录音文件无法播放

47.LAME 实现 mp3音频 的录制

48.获取 UTC 字符串的时间戳

49.获取当前的 UTC 时间戳

50.自定义转盘跳动动画

51.自定义 view 圆中心写文字

52.出现at android.view.ViewGroup.jumpDrawablesToCurrentState错误

53.获取当前进程名

54.MAT 打不开 .hprof 文件

55.APK 构建过程

56.CPU架构及so库兼容问题总结

57.高版本手机无法在后台开启震动

58.Git stash命令使用

59.图片文件与Bitmap之间的相互转换

60.安装完APK后点打开,然后回到桌面,再点图标打开时出现APP重建,重走启动页

61.ViewPager嵌套ViewPager中多层Fragment子ViewPager中加载不出来Fragment

62.禁止EditText自动弹出键盘

63.recyclerview 嵌套 scrollview 没有滑到顶部

64.输入法上移布局(不只是移到edittext下方)

65.EditText 的 hint 居中,光标居左

66.接口回调不执行

67.smoothScrollToPosition和scrollToPosition的区别

68.RecyclerView的置顶,上移和下移的动画效果及逻辑实现

69.关闭最近的程序列表中的任务

70.Glide加载大量图片导致数据错乱和OOM

71.自适应高度的 viewpager

72.EventBus 收不到消息

73.gradient中的angle属性

74.国际化语言失效

75.项目报红,却可以运行

76.textview文字颜色渐变

77.等边距的GridLayoutManager

2022-6-30

1. git clone URL之后想切换远程分支只需要git checkout 分支就行,不用再-b了。

虽然本地没有显示有其他分支,但其实是有的,所以只需要git checkout 就行。

2. 个别手机在锁屏页点击三个功能入口无效,原因:非Activity启动Activity需要加上标记为NEW_TASK。

但我是在Activity启动的其他activity,这也不行,可能这个activity是在service里面吧。所以为了各平台手机的
兼容性,最好还是加上标记为NEW_TASK吧。
总结:非Activity的context启动Activity需要加上NEW_TASK标记位。

3.ViewPager 中的 Fragment 可见性问题的埋点解决方案:

当 setUserVisibleHint 方法还没被废弃时:
(1)使用setUserVisibleHint 监听fragment是否可见,
(2)然后监听系统息屏和亮屏广播,利用onResume解决已经在指定fragment时进入后台再回到前台的问题,
(3)最后用一个int变量解决一开始指定fragment会被setUserVisibleHint设为false的问题。

private int userVisibleHintShowTimes = 0;
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if (isVisibleToUser) {
            //到达fragment埋点
        } else {
            if (userVisibleHintShowTimes != 0){
                //离开fragment埋点
            }else {
                userVisibleHintShowTimes++;
            }
        }
        super.setUserVisibleHint(isVisibleToUser);
    }

@Override
    public void onResume() {
        Alog.i("NewsTab", "NewsTabFragment---onResume");

        //isLighted表示是否收到亮屏广播
        if (isLighted && getUserVisibleHint()) {
            //在指定fragment页面回到后台再回到前台时埋点
        }

        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();

        if (getUserVisibleHint()){
            //离开fragment埋点
        }
    }

    private class LockerReceiver extends BroadcastReceiver {
        private long showHomeAdLastTime=0;
        private long showLockAdLastTime=0;
        public LockerReceiver() {
        }

        @Override
        public void onReceive(final Context context, Intent intent) {
            if (!TextUtils.isEmpty(action)) {
                if (action.equals(Intent.ACTION_SCREEN_ON)) {
                   isLighted = true;
                } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
                    isLighted = false;
                }
            }
        }
    }

当 setUserVisibleHint 方法已经被废弃时:
(1)在构建 adapter 的构造函数时,调用 super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); 方法。
(2)然后在 fragment 的相应 onResume 和 onPause 方法中直接埋点即可。

4.常用的APP下载地方,可以免登陆下载Google Play的应用:apkpure(需翻墙) apkmirror
5.监听home键广播启动Activity会延迟5s,通过PendingIntent.send()方法启动Activity也不可以立即启动。
6.flutter中的inheritedwidget做到的数据共享只是实现了他和他子树的数据共享而已,但是provider可以做到跨组件数据共享。
7.git 删除分支、重命名分支

删除本地分支
命令行 : git branch -D

删除远程分支
命令行 : $ git push origin --delete

重命名本地分支
命令行 : $ git branch -m

重命名远程分支
(1)将远程分支删除掉 git push origin --delete
(2)将本地分支重命名 git branch -m
(3)将本地分支推到远程 git push origin

8.释放动画资源是(一般是controller?.dispose)需要调用在super.dispose方法之前的,不然会报错。
9.用handler做延时操作时要注意内存泄漏的问题,以免快速退出activity时造成程序崩溃。
10.recyclerview中第一个,最后一个,和中间的item样式不同时可以在onBindViewHolder中根据position来对应设置。
11.fragment的replace, add, show, hide方法区别:replace会替换掉之前的fragment,并且走完其生命周期;hide和show的话之前的fragment并不会走完其生命周期。

总结:用法上add配合hide或是remove使用,replace一般单独出现。

12.在replaceFragment出现Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState错误。

原因:如果activity的状态被保存了,这里再提交就会检查这个状态,符合条件就抛出一个异常来终止应用进程。也就是说在activity调用了onSaveInstanceState()之后,再commit一个事务就会出现该异常。那如果不想抛出异常,也可以很简单调用commitAllowingStateLoss()方法来略过这个检查就可以了。

13.版本适配的沉浸式状态栏

(1)简单版

/**
   * 设置沉浸式状态栏
   */
  private void setTransparentStatusBar() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
          View decorView = getWindow().getDecorView();
          decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
          getWindow().setStatusBarColor(Color.TRANSPARENT);
      }
  }

(2)第三方库版
https://github.com/LongSh1z/ImmersionBar

14.recyclerview同时点击多个item或者同时多次点击单个item引起crash

解决方案:

public abstract class StaticListener implements View.OnClickListener {
    private static long lastClickTime;
    private static final long MIN_TIME_INTERVAL = 1500;

    protected boolean isTimeEnabled(){
        long currentClickTime = System.currentTimeMillis();
        if ((currentClickTime - lastClickTime) > MIN_TIME_INTERVAL){
            lastClickTime = currentClickTime;
            return true;
        }else {
            return false;
        }
    }
}
使用时:
btn1.setOnClickListener(new StaticListener() {
            @Override
            public void onClick(View v) {
                if (isTimeEnabled()) {
                    Log.d("CCC", "onClick");
                }
            }
        });
15.TextView中文英文数字混排导致的自动换行问题

解决方案:重写TextView

16.SmartRefreshLayout的一些注意事项

默认情况下会启用列表惯性滑动到底部时自动加载更多,解决方法如下:

//是否启用列表惯性滑动到底部时自动加载更多
mSmartRefreshLayout.setEnableAutoLoadMore(false);

默认情况下会在加载完成时滚动列表显示新的内容,解决方法如下:

//是否在加载完成时滚动列表显示新的内容
mSmartRefreshLayout.setEnableScrollContentWhenLoaded(false);

其他的默认情况如下:

//下面示例中的值等于默认值
RefreshLayout refreshLayout = (RefreshLayout)findViewById(R.id.refreshLayout);
refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
refreshLayout.setDragRate(0.5f);//显示下拉高度/手指真实下拉高度=阻尼效果
refreshLayout.setReboundDuration(300);//回弹动画时长(毫秒)

refreshLayout.setHeaderHeight(100);//Header标准高度(显示下拉高度>=标准高度 触发刷新)
refreshLayout.setHeaderHeightPx(100);//同上-像素为单位 (V1.1.0删除)
refreshLayout.setFooterHeight(100);//Footer标准高度(显示上拉高度>=标准高度 触发加载)
refreshLayout.setFooterHeightPx(100);//同上-像素为单位 (V1.1.0删除)

refreshLayout.setFooterHeaderInsetStart(0);//设置 Header 起始位置偏移量 1.0.5
refreshLayout.setFooterHeaderInsetStartPx(0);//同上-像素为单位 1.0.5 (V1.1.0删除)
refreshLayout.setFooterFooterInsetStart(0);//设置 Footer 起始位置偏移量 1.0.5
refreshLayout.setFooterFooterInsetStartPx(0);//同上-像素为单位 1.0.5 (V1.1.0删除)

refreshLayout.setHeaderMaxDragRate(2);//最大显示下拉高度/Header标准高度
refreshLayout.setFooterMaxDragRate(2);//最大显示下拉高度/Footer标准高度
refreshLayout.setHeaderTriggerRate(1);//触发刷新距离 与 HeaderHeight 的比率1.0.4
refreshLayout.setFooterTriggerRate(1);//触发加载距离 与 FooterHeight 的比率1.0.4

refreshLayout.setEnableRefresh(true);//是否启用下拉刷新功能
refreshLayout.setEnableLoadMore(false);//是否启用上拉加载功能
refreshLayout.setEnableAutoLoadMore(true);//是否启用列表惯性滑动到底部时自动加载更多
refreshLayout.setEnablePureScrollMode(false);//是否启用纯滚动模式
refreshLayout.setEnableNestedScroll(false);//是否启用嵌套滚动
refreshLayout.setEnableOverScrollBounce(true);//是否启用越界回弹
refreshLayout.setEnableScrollContentWhenLoaded(true);//是否在加载完成时滚动列表显示新的内容
refreshLayout.setEnableHeaderTranslationContent(true);//是否下拉Header的时候向下平移列表或者内容
refreshLayout.setEnableFooterTranslationContent(true);//是否上拉Footer的时候向上平移列表或者内容
refreshLayout.setEnableLoadMoreWhenContentNotFull(true);//是否在列表不满一页时候开启上拉加载功能
refreshLayout.setEnableFooterFollowWhenLoadFinished(false);//是否在全部加载结束之后Footer跟随内容1.0.4
refreshLayout.setEnableOverScrollDrag(false);//是否启用越界拖动(仿苹果效果)1.0.4

refreshLayout.setEnableScrollContentWhenRefreshed(true);//是否在刷新完成时滚动列表显示新的内容 1.0.5
refreshLayout.srlEnableClipHeaderWhenFixedBehind(true);//是否剪裁Header当时样式为FixedBehind时1.0.5
refreshLayout.srlEnableClipFooterWhenFixedBehind(true);//是否剪裁Footer当时样式为FixedBehind时1.0.5

refreshLayout.setDisableContentWhenRefresh(false);//是否在刷新的时候禁止列表的操作
refreshLayout.setDisableContentWhenLoading(false);//是否在加载的时候禁止列表的操作

refreshLayout.setOnMultiPurposeListener(new SimpleMultiPurposeListener());//设置多功能监听器
refreshLayout.setScrollBoundaryDecider(new ScrollBoundaryDecider());//设置滚动边界判断
refreshLayout.setScrollBoundaryDecider(new ScrollBoundaryDeciderAdapter());//自定义滚动边界

refreshLayout.setRefreshHeader(new ClassicsHeader(context));//设置Header
refreshLayout.setRefreshFooter(new ClassicsFooter(context));//设置Footer
refreshLayout.setRefreshContent(new View(context));//设置刷新Content(用于非xml布局代替addView)1.0.4

refreshLayout.autoRefresh();//自动刷新
refreshLayout.autoLoadMore();//自动加载
refreshLayout.autoRefreshAnimationOnly();//自动刷新,只显示动画不执行刷新
refreshLayout.autoLoadMoreAnimationOnly();//自动加载,只显示动画不执行加载
refreshLayout.autoRefresh(400);//延迟400毫秒后自动刷新
refreshLayout.autoLoadMore(400);//延迟400毫秒后自动加载
refreshLayout.finishRefresh();//结束刷新
refreshLayout.finishLoadMore();//结束加载
refreshLayout.finishRefresh(3000);//延迟3000毫秒后结束刷新
refreshLayout.finishLoadMore(3000);//延迟3000毫秒后结束加载
refreshLayout.finishRefresh(false);//结束刷新(刷新失败)
refreshLayout.finishLoadMore(false);//结束加载(加载失败)
refreshLayout.finishLoadMoreWithNoMoreData();//完成加载并标记没有更多数据 1.0.4
refreshLayout.closeHeaderOrFooter();//关闭正在打开状态的 Header 或者 Footer(1.1.0)
refreshLayout.resetNoMoreData();//恢复没有更多数据的原始状态 1.0.4(1.1.0删除)
refreshLayout.setNoMoreData(false);//恢复没有更多数据的原始状态 1.0.5
17.Fresco设置圆角时的注意事项
18.点击不同tab请求数据时,为防止 List 改变的不够及时,可以将 LIst 的类型换成线程安全的 CopyOnWriteArrayList ,再加上一定时间的防触摸操作即可解决数据出现混乱的问题。
19.RecyclerView的缓存机制(参考链接:https://www.jianshu.com/p/3e9aa4bdaefd)
20.在MainActivity按下返回键不能完全退出应用

如果确定要退出应用的话,可以调用 System.exit(0);

21.Android 10 授权读取媒体文件后还是报permission denied 错误。(Android 10 存储适配)

(1)创建文件适配(视频)

private String isExistDir(String saveDir, Context context) throws IOException {
        File downloadFile;
        // 下载位置
        if (Build.VERSION.SDK_INT >= 29){
            downloadFile = new File(String.valueOf(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)));
        }else {
            downloadFile = new File(Environment.getExternalStorageDirectory(), saveDir);
        }
        String savePath = downloadFile.getAbsolutePath();
        Log.d("savePath:",savePath);
        PreferenceUtils.putString(context, Constant.SAVE_PATH,savePath);
        return savePath;
}

(2)获取文件适配(视频)

public static Uri getImageContentUri(Context context, java.io.File imageFile) {
        if (Build.VERSION.SDK_INT >= 29 || Build.VERSION.SDK_INT <= 23) {
            Log.d("Build.VERSION.SDK_INT:", String.valueOf(Build.VERSION.SDK_INT));
            String filePath = imageFile.getAbsolutePath();
            Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                    new String[]{MediaStore.Video.Media._ID}, MediaStore.Video.Media.DATA + "=? ",
                    new String[]{filePath}, null);
            if (cursor != null && cursor.moveToFirst()) {
                String id = PreferenceUtils.getString(context, Constant.SAVE_PATH, "/storage/emulated/0/Android/data/" + context.getPackageName() + "/files/Movies");
                Uri uri = Uri.withAppendedPath(Uri.parse(id), "" + "live_video.mp4");
                Log.d(TAG, "getImageContentUri: uri:" + uri);
                if (uri == null && Build.VERSION.SDK_INT <= 23) {
                    int uriId = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
                    Uri baseUri = Uri.parse("content://media/external/images/media");
                    return Uri.withAppendedPath(baseUri, "" + uriId);
                }
                return uri;
            } else {
                if (imageFile.exists()) {
                    ContentValues values = new ContentValues();
                    values.put(MediaStore.Video.Media.DATA, filePath);
                    return context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
                } else {
                    return null;
                }
            }
        } else {
            Log.d("Build.VERSION.SDK_INT:", String.valueOf(Build.VERSION.SDK_INT));
            String filePath = imageFile.getAbsolutePath();
            Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "=? ",
                    new String[]{filePath}, null);
            if (cursor != null && cursor.moveToFirst()) {
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
                Uri baseUri = Uri.parse("content://media/external/images/media");
                return Uri.withAppendedPath(baseUri, "" + id);
            } else {
                if (imageFile.exists()) {
                    ContentValues values = new ContentValues();
                    values.put(MediaStore.Images.Media.DATA, filePath);
                    return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                } else {
                    return null;
                }
            }
        }
    if(uri == null){
        String videoPath_ = PreferenceUtils.getString(getApplicationContext(), Constant.SAVE_PATH, "/storage/emulated/0/Android/data/" + getApplicationContext().getPackageName() + "/files/Movies");
        return videoPath_+"/live_video.mp4";
    }else {
        return null;
    }
}
22.快手全屏视频广告播放黑屏

原因:未开启硬件加速
解决方案:在 AndroidManifest.xml 中的 application 节点加上下面一句即可

android:hardwareAccelerated="true"
23.Android 不推荐使用枚举

Android上不应该使用枚举,占内存,应该使用@XXXDef注解来替代。

24.打开系统页面
(1)打开授权“显示在其他应用的上层”页面
startActivityForResult(
    new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
        Uri.parse("package:" + getPackageName())), 0);
(2)打开设置页面
PackageManager packageManager = getPackageManager();
Intent intent = packageManager.getLaunchIntentForPackage("com.android.settings");
startActivity(intent);
(3)打开无障碍服务页面
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
                startActivity(intent);
(4)打开修改系统设置页面
startActivityForResult(
    new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
        Uri.parse("package:" + getPackageName())), 0);
(5)打开本应用设置页面
Intent intent = new Intent("android.settings.APPLICATION_DETAILS_SETTINGS");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
25.查看当前页面布局的具体情况

在SDK目录下的tools -> bin -> 双击 uiautomatorviewer.bat -> [图片上传失败...(image-415538-1656561995774)]
-> 点击第二个按钮,即可查看详细信息。

26.获取签名文件的SHA1值

keytool -list -v -keystore debug.keystore

27.android 与 h5 交互方式:

(1)使用WebView

    /**
     * 第一步,声明带 @JavascriptInterface 注解的方法,以便 H5 调用
     */
    @JavascriptInterface
    public void setMessage(){
        Toast.makeText(this, "我是android中的无参方法", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void setMessage(String msg){
        Toast.makeText(this, "我是android中的有参方法", Toast.LENGTH_SHORT).show();
    }
/**
* 第二步,让WebView支持JS
* 注意 : 如果html文件存于assets:则加前缀:file:///android_asset/
* 如果在Sdcard直接使用file:///sdcard/ or file:/sdcard也可以
* assets目录要放在 app/src/main 目录下
*/
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);//让WebView支持JS
webView.loadUrl("file:///android_asset/Test.html");//加载网页
webView.addJavascriptInterface(this,"android");//需要声明带 @JavascriptInterface 注解的方法
/**
* 第三步,调用 H5 的 JS 无参方法,
* 字符串 javascript:message() 中 javascript: 是固定写法,message 是 H5 中的方法名
*/
webView.loadUrl("javascript:message()");

下面是完整代码:

//MainActivity
public class MainActivity extends AppCompatActivity {

    private Button btn_invokeH5NoParam,btn_invokeH5WithParam;
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn_invokeH5NoParam = findViewById(R.id.btn_invokeH5NoParam);
        btn_invokeH5WithParam = findViewById(R.id.btn_invokeH5WithParam);
        webView = findViewById(R.id.webView);

        /**
         * 第二步,让WebView支持JS
         * 注意 : 如果html文件存于assets:则加前缀:file:///android_asset/
         * 如果在Sdcard直接使用file:///sdcard/ or file:/sdcard也可以
         * assets目录要放在 app/src/main 目录下
         */
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);//让WebView支持JS
        webView.loadUrl("file:///android_asset/Test.html");//加载网页
        webView.addJavascriptInterface(this,"android");//需要声明带 @JavascriptInterface 注解的方法

        btn_invokeH5NoParam.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 第三步,调用 H5 的 JS 无参方法,
                 * 字符串 javascript:message() 中 javascript: 是固定写法,message 是 H5 中的方法名
                 */
                webView.loadUrl("javascript:message()");
            }
        });



        btn_invokeH5WithParam.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 第三步,调用 H5 的 JS 有参方法,
                 * 字符串 javascript:message() 中 javascript: 是固定写法,message2 是 H5 中的方法名
                 */
                String name = "LongSh1z";
                webView.loadUrl("javascript:message2('"+name+"')");
            }
        });

        webView.setWebViewClient(new WebViewClient(){
            @Override
            public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
                super.onReceivedError(view, request, error);
//                Log.i("webView", "onReceivedError: "+error.getDescription());
            }
        });
    }

    /**
     * 第一步,声明带 @JavascriptInterface 注解的方法,以便 H5 调用
     */
    @JavascriptInterface
    public void setMessage(){
        Toast.makeText(this, "我是android中的无参方法", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void setMessage(String msg){
        Toast.makeText(this, "我是android中的有参方法", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void log(String msg){
        Log.i("webView", "log: "+msg);
    }
}

//TEST.html



    
    






点我试试
百度

(2)使用JsBridge

28.快速截屏模拟器的屏幕

打开LogCat -> 左下角相机图标即可

29.AS Build窗口出现乱码

解决方案:先关闭AS,然后打开AS安装目录 / bin / studio.exe.vmoptions以及studio64.exe.vmoptions,在文件最后加上-Dfile.encoding=UTF-8,最后打开AS即可。

30.ViewPager禁止左右滑动
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;

/**
 * description :类的作用
 * author : LongSh1z
 * date : 2020/6/22 11:11
 */
public class NoScrollViewPager extends ViewPager {

    //true 为可以左右滑动,false 为禁止左右滑动
    private boolean canScroll = false;

    public NoScrollViewPager(@NonNull Context context) {
        super(context);
    }

    public NoScrollViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public void setCanScroll(boolean canScroll) {
        this.canScroll = canScroll;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return canScroll && super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return canScroll && super.onTouchEvent(ev);
    }
}
31.TabLayout 隐藏下划线

解决方案:在 xml 文件中添加一句即可。(在代码中的set方法已被废弃)
app:tabIndicatorHeight="0dp"

32.Android Studio 自动导包

操作步骤:File -> Settings -> Auto Settings -> Insert imports on paste 选择All,下面的选项均选上即可(本人AS版本3.3.2)

33.TabLayout 实现部分自定义样式(自定义选中时的文字大小,样式,颜色)

(1)自定义默认tab的文字大小、颜色和样式

  
            

(2)设置选中和未选中的样式

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                tab.setCustomView(null);
                TextView textView = new TextView(getActivity());
                float selectedSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics());
                textView.setTextSize(TypedValue.COMPLEX_UNIT_SP,selectedSize);
                textView.setTextColor(0xFF333333);
                textView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
                textView.setText(tab.getText());
                tab.setCustomView(textView);

            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                tab.setCustomView(null);
                TextView textView = new TextView(getActivity());
                float selectedSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics());
                textView.setTextSize(TypedValue.COMPLEX_UNIT_SP,selectedSize);
                textView.setTextColor(0xFF666666);
                textView.setText(tab.getText());
                tab.setCustomView(textView);
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

//这句一定要放在监听事件之前,不然第一次打开时以上设置不生效
tabLayout.setupWithViewPager(viewPager);
34.打正式包时报下面的错误
Lint found fatal errors while assembling a release target.
  To proceed, either fix the issues identified by lint, or modify your build script as follows:
  ...
  android {
      lintOptions {
          checkReleaseBuilds false
          // Or, if you prefer, you can continue to check for errors in release builds,
          // but continue the build even when errors are found:
          abortOnError false
      }
  }

分析:
提示可以通过关闭 checkReleaseBuilds 来忽略隐患,但这并不是长久之计。Lint 是代码检查,可以优化代码,发现一些潜在的bug,所以尽量不要关闭。
解决方案:
这时候会生成一个错误报告,在 app -> build -> resports -> lint-results-yourBuildName-fatal.html 当中,在网页中打开即可看到详细信息。

35.git push免登录提交

实现步骤:
(1)在GitHub上添加SSH key
(2)修改 .git / config 的 remote 节点

//修改前
[remote "xxx"]
    url = https://github.com/xxx/xxx.git
    fetch = +refs/heads/*:refs/remotes/xxx/*
    
//修改后
[remote "xxx"]
    url = [email protected]:xxx/xxx.git
    fetch = +refs/heads/*:refs/remotes/xxx/*
36.敏感词校验

思路:
(1)穷举匹配法:通过一个一个遍历敏感词列表中的敏感词来判断是否包含敏感词,特点:算法简单但是效率低下
(2)DFA算法:有穷状态机算法,只需要遍历一次待检测的文本,即可找到其中包含的敏感词。先把敏感词中有相同前缀的词组合成一个树形结构,不同前缀的词分属不同的树形分支。特点:效率高,但仍存在一些问题,无法检测那些中间包含特殊字符的敏感词。

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * description :类的作用
 * author : LongSh1z
 * date : 2020/7/10 14:51
 */
public class SensitiveWordsUtils {

    /**
     * 敏感词匹配规则
     */
    public static final int MinMatchTYpe = 1;
    public static final int MaxMatchType = 2;

    /**
     * 敏感词集合
     */
    public static HashMap sensitiveWordMap;

    /**
     * 初始化敏感词库,构建DFA算法模型
     *
     * @param sensitiveWordSet 敏感词库
     */
    public static synchronized void init(Set sensitiveWordSet) {
        initSensitiveWordMap(sensitiveWordSet);
    }

    /**
     * 初始化敏感词库,构建DFA算法模型
     *
     * @param sensitiveWordSet 敏感词库
     */
    private static void initSensitiveWordMap(Set sensitiveWordSet) {
        //初始化敏感词容器,减少扩容操作
        sensitiveWordMap = new HashMap(sensitiveWordSet.size());
        String key;
        Map nowMap;
        Map newWorMap;
        //迭代sensitiveWordSet
        Iterator iterator = sensitiveWordSet.iterator();
        while (iterator.hasNext()) {
            //关键字
            key = iterator.next();
            nowMap = sensitiveWordMap;
            for (int i = 0; i < key.length(); i++) {
                //转换成char型
                char keyChar = key.charAt(i);
                //库中获取关键字
                Object wordMap = nowMap.get(keyChar);
                //如果存在该key,直接赋值,用于下一个循环获取
                if (wordMap != null) {
                    nowMap = (Map) wordMap;
                } else {
                    //不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
                    newWorMap = new HashMap<>();
                    //不是最后一个
                    newWorMap.put("isEnd", "0");
                    nowMap.put(keyChar, newWorMap);
                    nowMap = newWorMap;
                }

                if (i == key.length() - 1) {
                    //最后一个
                    nowMap.put("isEnd", "1");
                }
            }
        }
    }

    /**
     * 判断文字是否包含敏感字符
     *
     * @param txt       文字
     * @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
     * @return 若包含返回true,否则返回false
     */
    public static boolean contains(String txt, int matchType) {
        boolean flag = false;
        for (int i = 0; i < txt.length(); i++) {
            int matchFlag = checkSensitiveWord(txt, i, matchType); //判断是否包含敏感字符
            if (matchFlag > 0) {    //大于0存在,返回true
                flag = true;
            }
        }
        return flag;
    }

    /**
     * 判断文字是否包含敏感字符
     *
     * @param txt 文字
     * @return 若包含返回true,否则返回false
     */
    public static boolean contains(String txt) {
        return contains(txt, MaxMatchType);
    }

    /**
     * 获取文字中的敏感词
     *
     * @param txt       文字
     * @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
     * @return
     */
    public static Set getSensitiveWord(String txt, int matchType) {
        Set sensitiveWordList = new HashSet<>();

        for (int i = 0; i < txt.length(); i++) {
            //判断是否包含敏感字符
            int length = checkSensitiveWord(txt, i, matchType);
            if (length > 0) {//存在,加入list中
                sensitiveWordList.add(txt.substring(i, i + length));
                i = i + length - 1;//减1的原因,是因为for会自增
            }
        }

        return sensitiveWordList;
    }

    /**
     * 获取文字中的敏感词
     *
     * @param txt 文字
     * @return
     */
    public static Set getSensitiveWord(String txt) {
        return getSensitiveWord(txt, MaxMatchType);
    }

    /**
     * 替换敏感字字符
     *
     * @param txt         文本
     * @param replaceChar 替换的字符,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符:*, 替换结果:我爱***
     * @param matchType   敏感词匹配规则
     * @return
     */
    public static String replaceSensitiveWord(String txt, char replaceChar, int matchType) {
        String resultTxt = txt;
        //获取所有的敏感词
        Set set = getSensitiveWord(txt, matchType);
        Iterator iterator = set.iterator();
        String word;
        String replaceString;
        while (iterator.hasNext()) {
            word = iterator.next();
            replaceString = getReplaceChars(replaceChar, word.length());
            resultTxt = resultTxt.replaceAll(word, replaceString);
        }

        return resultTxt;
    }

    /**
     * 替换敏感字字符
     *
     * @param txt         文本
     * @param replaceChar 替换的字符,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符:*, 替换结果:我爱***
     * @return
     */
    public static String replaceSensitiveWord(String txt, char replaceChar) {
        return replaceSensitiveWord(txt, replaceChar, MaxMatchType);
    }

    /**
     * 替换敏感字字符
     *
     * @param txt        文本
     * @param replaceStr 替换的字符串,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符串:[屏蔽],替换结果:我爱[屏蔽]
     * @param matchType  敏感词匹配规则
     * @return
     */
    public static String replaceSensitiveWord(String txt, String replaceStr, int matchType) {
        String resultTxt = txt;
        //获取所有的敏感词
        Set set = getSensitiveWord(txt, matchType);
        Iterator iterator = set.iterator();
        String word;
        while (iterator.hasNext()) {
            word = iterator.next();
            resultTxt = resultTxt.replaceAll(word, replaceStr);
        }

        return resultTxt;
    }

    /**
     * 替换敏感字字符
     *
     * @param txt        文本
     * @param replaceStr 替换的字符串,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符串:[屏蔽],替换结果:我爱[屏蔽]
     * @return
     */
    public static String replaceSensitiveWord(String txt, String replaceStr) {
        return replaceSensitiveWord(txt, replaceStr, MaxMatchType);
    }

    /**
     * 获取替换字符串
     *
     * @param replaceChar
     * @param length
     * @return
     */
    private static String getReplaceChars(char replaceChar, int length) {
        String resultReplace = String.valueOf(replaceChar);
        for (int i = 1; i < length; i++) {
            resultReplace += replaceChar;
        }

        return resultReplace;
    }

    /**
     * 检查文字中是否包含敏感字符,检查规则如下:
* * @param txt * @param beginIndex * @param matchType * @return 如果存在,则返回敏感词字符的长度,不存在返回0 */ private static int checkSensitiveWord(String txt, int beginIndex, int matchType) { //敏感词结束标识位:用于敏感词只有1位的情况 boolean flag = false; //匹配标识数默认为0 int matchFlag = 0; char word; Map nowMap = sensitiveWordMap; for (int i = beginIndex; i < txt.length(); i++) { word = txt.charAt(i); //获取指定key nowMap = (Map) nowMap.get(word); if (nowMap != null) {//存在,则判断是否为最后一个 //找到相应key,匹配标识+1 matchFlag++; //如果为最后一个匹配规则,结束循环,返回匹配标识数 if ("1".equals(nowMap.get("isEnd"))) { //结束标志位为true flag = true; //最小规则,直接返回,最大规则还需继续查找 if (MinMatchTYpe == matchType) { break; } } } else {//不存在,直接返回 break; } } if (matchFlag < 2 && !flag) {//长度必须大于等于1,为词 matchFlag = 0; } return matchFlag; } }
37.webview 白屏

思路:首先可以设置 webview 背景透明,接着可以设置 webview 所在的 activity 的主题为图片背景即可
优缺点:可以防止白屏的出现,但是
解决方案:
(1)首先在 xml 文件的 webview 标签下设置透明背景

android:background="@android:color/transparent"

(2)然后代码中再次设置透明背景

webView.setBackgroundColor(0);
webView.getBackground().setAlpha(0);

(3)设置 webview 所在的 activity 的主题为图片背景



//loading_bg

38.每次刷新Recyclerview时,记录上次浏览的位置,并滑动到指定位置

1.每次刷新Recyclerview时,记录上次浏览的位置,并滑动到指定位置
思路:
(1)实现 recyclerview 的 addOnScrollListener 方法,保存当前页面第一个可见item的位置和位移
(2)调用 recyclerview 的 scrollToPosition 或者 layoutmanager 的 scrollToPositionWithOffset 方法滑动到指定位置
实现:

    private int currentPosition;//当前页面第一个可见item的位置和位移
    private int[] outLocation = new int[2];//当前屏幕的绝对位置(x,y)
    private int offset;//当前页面第一个可见item的位移

    /**
    * 步骤一:实现监听方法
    */
    dataBinding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);

                if (recyclerView.getLayoutManager() != null){
                    if (newState == SCROLL_STATE_IDLE){
                        //获取可视的第一个view
                        View topView = recyclerView.getChildAt(0);
                        if (topView != null){
                            currentPosition = recyclerView.getLayoutManager().getPosition(topView);
                            topView.getLocationOnScreen(outLocation);
                            offset = outLocation[1];//因为项目是沉浸式状态栏,否则的话这个 offset 还需要减去顶部状态栏的高度
                            Log.i(TAG, "----------onScrollStateChanged: -----当前的订单位置-----"+currentPosition+"----------");
                        }
                    }
                }
            }
        });

    /**
     * 滑动到上次位置
     */
    private void scrollToLastPosition() {
        Log.i(TAG, "----------scrollToLastPosition: -----要移动到的订单位置-----"+currentPosition+"-----当前的数据源大小为-----"+dataList.size()+"-----");
        try {
            if (currentPosition < 0){
                dataBinding.recyclerView.scrollToPosition(0);
            }else if (currentPosition <= dataList.size()){
                ((CatchExceptionLayoutManager)dataBinding.recyclerView.getLayoutManager()).scrollToPositionWithOffset(currentPosition,offset);
            }else {
                dataBinding.recyclerView.scrollToPosition(dataList.size() - 1);
            }
        }catch (Exception e){
            Log.i(TAG, "----------Exception: -----异常为-----"+e.getMessage()+"----------");
            e.printStackTrace();
            dataBinding.recyclerView.scrollToPosition(0);
        }
    }
39.Android 文件存储

内部存储:

定义:

应用存储在该位置的文件,默认只能由本 APP 访问,如果该应用被卸载,相应的文件也会被删除。

内部存储的位置:

在 /data/ 下面,其中包括常用 SharedPreferences 和 SQLite 存储。

访问内部存储的 API 方法:

(1)Environment.getDataDirectory()

(2)getFilesDir().getAbsolutePath()

(3)getCacheDir().getAbsolutePath()

(4)getDir(" myFile" , MODE_PRIVATE ).getAbsolutePath()

外部存储:

定义:

4.4系统及以上的手机将机身存储存储(手机自身带的存储叫做机身存储)在概念上分成了 ”内部存储internal” 和 ”外部存储external” 两部分。Android 把机身存储的外部存储路径SD 卡的路径统称为外部存储。

外部存储的位置:

(1)/storage/emulated/0/ (机身存储的外部存储路径)

(2)/storage/xxxxxx/(SD 卡路径,xxxxxx视具体情况而论)

访问外部存储的方法:

(1)Environment.getExternalStorageDirectory().getAbsolutePath() ---> 已被废弃,可以使用 context.getExternalFilesDir()

(2)Environment.getExternalStoragePublicDirectory("").getAbsolutePath() ---> 已被废弃

(3)context.getExternalFilesDir("").getAbsolutePath()

(4)context.getExternalCacheDir().getAbsolutePath()

一些常用的文件访问方法说明:

[图片上传失败...(image-e9e007-1656568464322)]

1、Environment.getDataDirectory() = /data
这个方法是获取内部存储的根路径
2、getFilesDir().getAbsolutePath() = /data/user/0/packname/files
这个方法是获取某个应用在内部存储中的files路径
3、getCacheDir().getAbsolutePath() = /data/user/0/packname/cache
这个方法是获取某个应用在内部存储中的cache路径
4、getDir(“myFile”, MODE_PRIVATE).getAbsolutePath() = /data/user/0/packname/app_myFile
这个方法是获取某个应用在内部存储中的自定义路径
方法2,3,4的路径中都带有包名,说明他们是属于某个应用
…………………………………………………………………………………………
5、Environment.getExternalStorageDirectory().getAbsolutePath() = /storage/emulated/0
这个方法是获取外部存储的根路径
6、Environment.getExternalStoragePublicDirectory(“”).getAbsolutePath() = /storage/emulated/0
这个方法是获取外部存储的根路径
7、getExternalFilesDir(“”).getAbsolutePath() = /storage/emulated/0/Android/data/packname/files
这个方法是获取某个应用在外部存储中的files路径
8、getExternalCacheDir().getAbsolutePath() = /storage/emulated/0/Android/data/packname/cache
这个方法是获取某个应用在外部存储中的cache路径

9、Environment.getDownloadCacheDirectory() = /cache/

10、Environment.getRootDirectory() = /system

从上面我们很清楚的可以看到上面的方法可以分为三类,我用横线隔开了。第一类是位于根目录/data下;还有一类是位于根目录/storage下,可以看到调用它们的API方法都带了一个External;另外一类不在/data下也不再/storage下,比如系统文件/system,或者缓存文件/cache。
/data目录下的文件物理上存放在我们通常所说的内部存储里面
/storage目录下的文件物理上存放在我们通常所说的外部存储里面
/system用于存放系统文件,/cache用于存放一些缓存文件,物理上它们也是存放在内部存储里面的

一般程序员会做判断是否有外部存储,没有再使用内部存储:

public static String getFilePath(Context context,String dir) {
    String directoryPath="";
    if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ) {//判断外部存储是否可用 
        directoryPath =context.getExternalFilesDir(dir).getAbsolutePath();
        }else{//没外部存储就使用内部存储  
        directoryPath=context.getFilesDir()+File.separator+dir;
        }
        File file = new File(directoryPath);
        if(!file.exists()){//判断文件目录是否存在
        file.mkdirs();
        }
    return directoryPath;
}
40.实现录音功能

实现方案:
(1)使用 AudioRecord 类
(2)使用 MediaRecorder 类
两者的区别:
AudioRecord:
AudioRecord 使用较为复杂,可以对音频进行实时处理以及边录边播,相比于 MediaRecorder 专业,输出的是PCM语音数据。如果保存成音频文件,是不能被播放器播放的,必须先编码和压缩
MediaRecorder:
MediaRecorder 使用较简单,录制的音频文件是经过压缩的,需要设置编码器,并且录制的音频文件可以用系统自带的 Music 播放器播放。MediaRecorder已经集成了录音、编码、压缩等,但是只支持少量的音频格式以及无法实时处理音频数据
MediaRecorder使用方法:

/**
 * description :录音管理器
 * author : LongSh1z
 * date : 2020/7/30 19:43
 */
public class MediaRecorderManager {

    private static final String TAG = "MediaRecorderManager";

    private volatile static MediaRecorderManager instance;
    private volatile static MediaRecorder recorder;

    public static MediaRecorderManager getInstance() {
        if (instance == null) {
            synchronized (MediaRecorderManager.class) {
                if (instance == null) {
                    instance = new MediaRecorderManager();
                }
            }
        }
        getRecorder();
        return instance;
    }

    private static MediaRecorder getRecorder() {
        if (recorder == null) {
            synchronized (MediaRecorderManager.class) {
                if (recorder == null) {
                    recorder = new MediaRecorder();
                }
            }
        }
        return recorder;
    }

    /**
     * 开始录音
     */
    public void startRecord(Context context) {
        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        recorder.setOutputFile(getFilePath(context));
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

        try {
            recorder.prepare();
        } catch (IOException e) {
            Log.i(TAG, "----------prepare: fail----------");
            e.printStackTrace();
        }

        recorder.start();
    }

    /**
     * 停止录音
     */
    public void pauseRecord() {
        recorder.stop();
        recorder.release();
        recorder = null;
    }

    /**
     * 获取录音文件路径
     *
     * @return
     */
    private static String getFilePath(Context context) {
        String filePath;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            filePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/Tarot-Talent_Android/";
        } else {
            filePath = context.getFilesDir().getAbsolutePath() + "/Tarot-Talent_Android/";
        }

        File file = new File(filePath);
        if (!file.exists()){
            file.mkdir();
        }

        filePath += "record.mp3";

        return filePath;
    }

    /**
     * 获取录音文件路径,不判断文件是否存在
     *
     * @return
     */
    public static String getFilePathByNotJudge(Context context) {
        String filePath;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            filePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/Tarot-Talent_Android/record.mp3";
        } else {
            filePath = context.getFilesDir().getAbsolutePath() + "/Tarot-Talent_Android/record.mp3";
        }
        return filePath;
    }

    public static boolean isExistRecordFile(Context context){
        boolean result;
        String filePath;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            filePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/Tarot-Talent_Android/record.mp3";
        } else {
            filePath = context.getFilesDir().getAbsolutePath() + "/Tarot-Talent_Android/record.mp3";
        }

        File file = new File(filePath);
        if (file.exists()){
            result = true;
        }else {
            result = false;
        }
        Log.i(TAG, "----------isExistRecordFile: -----" + (result ? "文件存在" : "文件不存在") + "----------");
        return result;
    }

    public static void deleteFile(Context context){
        File file = new File(getFilePath(context));
        if (file.exists()){
            file.delete();
        }
    }
}
41.去除按钮阴影效果
style="?android:borderlessButtonStyle"
42.自定义的对话框的背景、宽度、高度不生效

原因:与原本的背景冲突了,所以显示不了
解决方法:重新获取 window 后自行设置即可

//一定要在show方法之后调用
Window window = recordingDialog.getWindow();
//主要是为了自定义的圆角背景生效
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
//主要是为了自定义的宽度生效
WindowManager.LayoutParams params = window.getAttributes();
params.width = DimensionUtils.dip2px(this,320f);
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(params);
43.PopupWindow 出现在控件上方
int[] btnRecordLocation = new int[2];
//获取按钮在屏幕中的位置
btnStartRecord.getLocationOnScreen(btnRecordLocation);
//自定义布局
dialogView = LayoutInflater.from(this).inflate(R.layout.popupwindow_recording,null);

recordingPopupWindow = new PopupWindow(dialogView,
    ViewGroup.LayoutParams.WRAP_CONTENT,
    ViewGroup.LayoutParams.WRAP_CONTENT);
dialogView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
recordingPopupWindow.showAtLocation(btnStartRecord,Gravity.NO_GRAVITY,
    (btnRecordLocation[0] + btnStartRecord.getWidth() / 2) - dialogView.getMeasuredWidth() / 2,
    btnRecordLocation[1] - dialogView.getMeasuredHeight());
44.实现 TextView 部分文字的点击监听(用户服务及隐私协议对话框)
View dialogView = LayoutInflater.from(SplashActivity.this).inflate(R.layout.dialog_server_and_private,null);
                            AlertDialog dialog = new AlertDialog.Builder(SplashActivity.this).setView(dialogView).create();
                            TextView tv_des = dialogView.findViewById(R.id.tv_des);
                            Button btn_noAgree = dialogView.findViewById(R.id.btn_noAgree);
                            Button btn_agree = dialogView.findViewById(R.id.btn_agree);
                            btn_noAgree.setOnClickListener(new View.OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    dialog.dismiss();

                                    System.exit(0);
                                }
                            });
                            btn_agree.setOnClickListener(new View.OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    dialog.dismiss();

                                    LoginActivity.launch(SplashActivity.this,true,true);
                                    SPUtils.putBoolean(SplashActivity.this, SPUtils.IS_FIRST_INSTALL,false);
                                }
                            });

                            final SpannableStringBuilder style = new SpannableStringBuilder();
                            //设置文字
                            style.append("亲爱的最塔罗用户,欢迎使用最塔罗。在您使用前请仔细阅读《用户协议》和《隐私权政策》。我们将严格遵守监管部门的各项规定,为您提供更优质的服务。");
                            //《用户协议》 点击事件
                            ClickableSpan clickableSpan1 = new ClickableSpan() {
                                @Override
                                public void onClick(View widget) {
                                    QuestionnaireActivity.start(SplashActivity.this,Global.TERMS_OF_SERVICE);
                                }
                            };
                            //《隐私权政策》 点击事件
                            ClickableSpan clickableSpan2 = new ClickableSpan() {
                                @Override
                                public void onClick(View widget) {
                                    QuestionnaireActivity.start(SplashActivity.this,Global.PRIVATE_AGREEMENT);
                                }
                            };
                            style.setSpan(clickableSpan1, 27, 33, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            style.setSpan(clickableSpan2, 35, 42, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            tv_des.setText(style);
                            //设置部分文字颜色
                            ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.parseColor("#0000FF"));
                            style.setSpan(foregroundColorSpan, 28, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            style.setSpan(foregroundColorSpan, 35, 42, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            //配置给TextView
                            tv_des.setMovementMethod(LinkMovementMethod.getInstance());
                            tv_des.setText(style);

                            dialog.setCancelable(false);
                            dialog.setCanceledOnTouchOutside(false);
                            dialog.show();

                            Window window = dialog.getWindow();
                            //主要是为了自定义的圆角背景生效
                            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
                            //主要是为了自定义的宽度生效
                            WindowManager.LayoutParams params = window.getAttributes();
                            params.width = DimensionUtils.dip2px(SplashActivity.this, 320f);
                            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
                            window.setAttributes(params);

//dialog_server_and_private.xml



    

    

    

        
45.Glide设置圆角图片
RequestOptions options = RequestOptions.bitmapTransform(new RoundedCorners(15));
Glide.with(this).load(leftCardUrl).placeholder(R.drawable.head_moren).error(R.drawable.head_moren).into(ivLeftImg);
46.上传到OSS的录音文件无法播放

原因:
(1)上传文件时选择的 content-type 不是 audio/mpeg,导致打开 URL 之后直接打开下载窗口
(content-type:告诉客户端实际返回的内容的内容类型)
(2) URL 有误
解决方法:设置content-type 为 audio/mpeg 即可,其他类型的视情况而论

47.LAME 实现 mp3音频 的录制

参考链接:

Android音频开发(5):Mp3的录制 - 编译Lame源码:https://www.jianshu.com/p/dd7cd9bd44a5

按照上述链接做完之后,还需要做如下改动

(1)在步骤2.5.2中,将 Application.mk 改为

APP_ABI := all
APP_MODULES := mp3lame
APP_CFLAGS += -DSTDC_HEADERS
APP_PLATFORM := android-21

(2)所有步骤完成之后,将生成 libs 文件夹移至 src 同级目录,删除生成的 obj 文件夹
(3)在 app/build.gradle 中添加以下内容

android {
    。。。
        
    defaultConfig {
        。。。
    }
    
    。。。

    //待添加的内容
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

    。。。
}

(4)最后再 ndk-build 编译以下即可

48.获取 UTC 字符串的时间戳
fun UTCToTimestamp(UTCStr: String?): Long {
            var date: Date? = null
            val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
            return try {
                date = sdf.parse(UTCStr)
                val calendar = Calendar.getInstance()
                calendar.time = date
                //            calendar.set(Calendar.HOUR, calendar.get(Calendar.HOUR) + 8);
                Log.i("DateUtils", "----------DateUtils-----UTCToTimestamp: -----result-----" + calendar.timeInMillis + "-----")
                calendar.timeInMillis
            } catch (e: ParseException) {
                Log.i("DateUtils", "----------DateUtils-----UTCToTimestamp: -----ParseException-----result-----" + 0 + "-----")
                e.printStackTrace()
                0
            }
        }
49.获取当前的 UTC 时间戳
val zeroTimezoneTimestamp: Long
            get() {
                val calendar = Calendar.getInstance()
                val zoneOffset = calendar.get(Calendar.ZONE_OFFSET)
                val dstOffset = calendar.get(Calendar.DST_OFFSET)
                calendar.add(Calendar.MILLISECOND,-(zoneOffset + dstOffset))
//                calendar.time = Date(Date().time - 8 * 60 * 60 * 1000)
                val date = Date()
                Log.i("DateUtils", "----------DateUtils-----getZeroTimezoneTimestamp: -----calendar.getTimeInMillis()-----" + calendar.timeInMillis + "-----")
                Log.i("DateUtils", "----------DateUtils-----getZeroTimezoneTimestamp: -----date.getTime()-----" + date.time + "-----")
                return calendar.timeInMillis
            }
50.自定义转盘跳动动画

思路:每次都绘制12个圆外加一个被选中的圆,动画用 postDelayed 实现

public class MyRotationView extends View {

    private Context context;
    private float diceRadius = 50f;                 //占星的半径
    private float instance = 50f;                   //占星离中心的距离
    private float selectedRadius = diceRadius * 1.5f;             //选中的占星的背景半径
    private String diceColor = "#564099";           //占星的颜色
    private String selectedColor = "#FFF1C2";       //选中的占星的背景颜色
    private Paint txtPaint;                         //数字的画笔
    private float txtSize = 25f;                    //数字的大小
    private Paint dicePaint;                        //占星的画笔
    private Paint selectedPaint;                    //选中的占星的背景的画笔
    private float mWidth;                           //自定义View的宽度
    private float mHeight;                          //自定义View的高度
    private int desIndex = 0;
    private int index = 0;
    private ArrayList runnables = new ArrayList<>();
    private Rect txtRect;

    public MyRotationView(Context context) {
        super(context);
        init(context);
    }

    public MyRotationView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MyRotationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        this.context = context;

        dicePaint = new Paint();
        dicePaint.setAntiAlias(true);
        dicePaint.setColor(Color.parseColor(diceColor));

        txtPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
        txtPaint.setTypeface(Typeface.DEFAULT_BOLD);
        txtPaint.setTextAlign(Paint.Align.CENTER);
        txtPaint.setColor(Color.WHITE);
        txtPaint.setTextSize(txtSize);

        txtRect = new Rect();

        selectedPaint = new Paint();
        selectedPaint.setAntiAlias(true);
        selectedPaint.setColor(Color.parseColor(selectedColor));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
    }

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);

        instance = mWidth / 2 - selectedRadius;

        canvas.translate(mWidth / 2 , mHeight / 2);

        drawInstantState(canvas,index);
    }

    private void drawInstantState(Canvas canvas, int index) {
        canvas.rotate(15f);
        if (index % 12 == 0){
            canvas.drawCircle(instance,0,selectedRadius,selectedPaint);
        }
        canvas.drawCircle(instance,0,diceRadius,dicePaint);
        canvas.rotate(-90f);

        txtPaint.getTextBounds("1",0,1,txtRect);
        int textHeight = txtRect.bottom - txtRect.top;
        canvas.drawText("1",0,instance + textHeight / 2f,txtPaint);
        canvas.rotate(90f);

        for (int i = 1; i < 12; i++) {
            canvas.rotate(30f);

            if (i == index % 12){
                canvas.drawCircle(instance,0,selectedRadius,selectedPaint);
            }

            canvas.drawCircle(instance,0,diceRadius,dicePaint);
            canvas.rotate(-90f);

            txtPaint.getTextBounds("1",0,1,txtRect);
            int textHeight1 = txtRect.bottom - txtRect.top;
            canvas.drawText(String.valueOf(i+1),0,instance + textHeight1 / 2f,txtPaint);

            canvas.rotate(90f);
        }

        if (this.index != desIndex){
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    invalidate();
                }
            };
            postDelayed(runnable,500);
            runnables.add(runnable);

            this.index++;
        }
    }

    public void startAnimation(int desIndex){
        for (int i = 0; i < runnables.size(); i++) {
            removeCallbacks(runnables.get(i));
        }
        this.desIndex = desIndex;
        this.index = 0;
        invalidate();
    }

    public void stopAnimation(){
        this.desIndex = this.index;
    }
}
51.自定义 view 圆中心写文字

思路:因为文字是以BaseLine作为基准(BaseLine相当于文字下方对齐的一条线),所以直接 drawText 的话会出现文字位于圆心的右上角。
因此,我们需要将文字置于横向居中后再 drawText Y轴上偏移多一半的文字高度即可:
横向居中可以设置 countPaint.setTextAlign(Paint.Align.CENTER);
Y轴偏移:cy + textHeight/2

Paint countPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
countPaint.setColor(Color.BLUE);
countPaint.setTextSize(20f);
countPaint.setTypeface(Typeface.DEFAULT_BOLD);
countPaint.setTextAlign(Paint.Align.CENTER);
Rect textBounds = new Rect();
String numberStr = String.valueOf(number);
countPaint.getTextBounds(numberStr, 0, numberStr.length(), textBounds);//get text bounds, that can get the text width and height
int textHeight = textBounds.bottom - textBounds.top;
Log.i("TAG","bounds: left = "+textBounds.left + ", right = "+textBounds.right+", top = "+textBounds.top+", bottom = "+textBounds.bottom);
canvas.drawText(numberStr, cx, cy + textHeight/2,countPaint);
52.出现at android.view.ViewGroup.jumpDrawablesToCurrentState错误

报错:在多 fragment 的 viewpager 中运行时报错 at android.view.ViewGroup.jumpDrawablesToCurrentState
原因:在创建 fragment 布局的时候如果使用以下方式,会将该布局绑定到外层的activity的viewpager上。此时如果有多个fragment,则会崩溃

rootView = inflater.inflate(R.layout.fragment_message, container)

解决方案:
改用以下方式加载fragment布局

rootView = inflater.inflate(R.layout.fragment_message, container, false)
53.获取当前进程名
//方法思路:
//1.我们优先通过 Application.getProcessName() 方法获取进程名
//2.如果获取失败,我们再反射 ActivityThread.currentProcessName() 获取进程名
//3.如果失败,我们才通过常规方法 ActivityManager 来获取进程名
public class ProcessUtil {
  private static String currentProcessName;

  /**
  * @return 当前进程名
  */
  @Nullable
  public static String getCurrentProcessName(@NonNull Context context) {
    if (!TextUtils.isEmpty(currentProcessName)) {
      return currentProcessName;
    }

    //1)通过Application的API获取当前进程名
    currentProcessName = getCurrentProcessNameByApplication();
    if (!TextUtils.isEmpty(currentProcessName)) {
      return currentProcessName;
    }

    //2)通过反射ActivityThread获取当前进程名
    currentProcessName = getCurrentProcessNameByActivityThread();
    if (!TextUtils.isEmpty(currentProcessName)) {
      return currentProcessName;
    }

    //3)通过ActivityManager获取当前进程名
    currentProcessName = getCurrentProcessNameByActivityManager(context);

    return currentProcessName;
  }


  /**
  * 通过Application新的API获取进程名,无需反射,无需IPC,效率最高。
  */
  public static String getCurrentProcessNameByApplication() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
      return Application.getProcessName();
    }
    return null;
  }

  /**
  * 通过反射ActivityThread获取进程名,避免了ipc
  */
  public static String getCurrentProcessNameByActivityThread() {
    String processName = null;
    try {
      final Method declaredMethod = Class.forName("android.app.ActivityThread", false, Application.class.getClassLoader())
        .getDeclaredMethod("currentProcessName", (Class[]) new Class[0]);
      declaredMethod.setAccessible(true);
      final Object invoke = declaredMethod.invoke(null, new Object[0]);
      if (invoke instanceof String) {
        processName = (String) invoke;
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
    return processName;
  }

  /**
  * 通过ActivityManager 获取进程名,需要IPC通信
  */
  public static String getCurrentProcessNameByActivityManager(@NonNull Context context) {
    if (context == null) {
      return null;
    }
    int pid = Process.myPid();
    ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    if (am != null) {
      List runningAppList = am.getRunningAppProcesses();
      if (runningAppList != null) {
        for (ActivityManager.RunningAppProcessInfo processInfo : runningAppList) {
          if (processInfo.pid == pid) {
            return processInfo.processName;
          }
        }
      }
    }
    return null;
  }
}
54.MAT 打不开 .hprof 文件

原因:格式文件
解决方法:进入android SDK 目录下的 platform-tools 目录,使用以下命令转换格式

hprof-conv your.hprof out.hprof
55.APK 构建过程

(1)概括来讲,整个构建过程分为两个主要操作:编译(Compile)、打包(APK Package)
编译:编译器(Compiler)通过编译 源码、AIDL文件、资源文件、依赖包,最终生成Dex文件和编译后的资源文件。
打包:打包器(APK Packager)利用签名文件(KeyStore)和上一步编译过程中生成的Dex文件、编译后的资源文件打包成最终的APK文件。

56.CPU架构及so库兼容问题总结

(1)CPU架构分类
armeabi
armeabi-v7a(目前大部分机器)
armeabi-v8a(高端机型)
x86
x86_64
mips
mips64
Android手机大部分采用的是ARM架构的CPU
(2)CPU之间的架构兼容

57.高版本手机无法在后台开启震动
import android.app.Service
import android.content.Context
import android.media.AudioAttributes
import android.os.Build
import android.os.Vibrator

/**
 * description :类的作用
 * author : LongSh1z
 * date : 2020/11/4 19:34
 */
class VibratorUtils {
    companion object {
        var vibrator: Vibrator? = null

        fun init(context: Context){
            vibrator = context.getSystemService(Service.VIBRATOR_SERVICE) as Vibrator
        }

        fun start() {
            val audioAttributes = AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .setUsage(AudioAttributes.USAGE_ALARM) // 源码中isAlarm判断可通过
                    .build()
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                vibrator?.vibrate(longArrayOf(100, 1000, 100, 1000), 0,audioAttributes)
            } else {
                vibrator?.vibrate(longArrayOf(100, 1000, 100, 1000), 0)
            }
        }

        fun cancel() {
            vibrator?.cancel()
        }
    }
}
58.Git stash命令使用

git stash : 把所有未提交的修改(包括暂存的和非暂存的)都保存起来,用于后续恢复当前工作目录
git stash save "some stash description" : 在git stash 的基础上增加描述说明
git stash pop : 将缓存堆栈中的第一个stash删除,并将对应修改应用到当前的工作目录下
git stash apply : 将缓存堆栈中的stash多次应用到工作目录中,但并不删除stash拷贝

59.图片文件与Bitmap之间的相互转换

图片转Bitmap

BitmapFactory.decodeFile(filePath)
    
//如果图片过大,可能导致Bitmap对象装不下图片,可以将图片相应缩小一点
BitmapFactory.decodeFile(filePath,getBitmapOption(2))//这里将图片的长和宽缩小为原来的1/2
private Options getBitmapOption(int inSampleSize){
    System.gc();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inSampleSize = inSampleSize;
    return options;
}

截取对应View的Bitmap转图片

ScreenShotUtils.getBitmapByView(this@DailyAttendanceResultActivity, rl_root_result, object : ScreenShotUtils.OnConvertBitmapListener {
                    override fun onSuccess(bmp: Bitmap) {
                        Log.i(Global.TAG, "-----DailyAttendanceResultActivity-----getBitmapByView-----onSuccess-----")
                        saveJPGByBitmap(bmp, file)
                    }

                    override fun onFailure() {
                        Log.i(Global.TAG, "-----DailyAttendanceResultActivity-----getBitmapByView-----onFail-----")
                        ToastUtils.showInCenter(this@DailyAttendanceResultActivity, "获取截图失败,请退出重试!")

                        WXShareUtils.REQUEST_SHARE_TO_WEIXIN = HttpUtils.STATE_REQUEST_IDLE
                    }
                })
    
    fun saveJPGByBitmap(bitmap: Bitmap, file: File) {
        Thread(Runnable {
            try {
                val out = FileOutputStream(file)
                if (bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out)) {
                    out.flush()
                    out.close()
                }
            } catch (e: FileNotFoundException) {
                e.printStackTrace()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }).start()
    }
class ScreenShotUtils {
    companion object {
        fun getBitmapByView(activity: Activity, view: View, listener: OnConvertBitmapListener) {
            if (activity.isDestroyed || activity.isFinishing) {
                return
            }
            val window = activity.window
            val locationOfView = IntArray(2)
            view.getLocationInWindow(locationOfView)
            val rect = Rect(locationOfView[0], locationOfView[1], locationOfView[0] + view.width, locationOfView[1] + view.height)
            //如果SDK版本大于等于26,即8.0
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val destBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
                PixelCopy.request(window, rect, destBitmap, { copyResult ->
                    Log.i(Global.TAG, "-----ScreenShotUtils-----getBitmapByView-----onPixelCopyFinished-----copyResult-----${copyResult}-----")
                    if (copyResult == PixelCopy.SUCCESS) {
                        listener.onSuccess(destBitmap)
                    } else {
                        listener.onFailure()
                    }
                }, Handler(Looper.getMainLooper()))
            } else {
                listener.onSuccess(getBitmapByViewUnderAndroid8(view))
            }
        }

        private fun getBitmapByViewUnderAndroid8(view: View): Bitmap {
            view.isDrawingCacheEnabled = true
            val drawingCache = view.drawingCache
            val bitmap = Bitmap.createBitmap(drawingCache)
            view.isDrawingCacheEnabled = false
            return bitmap
        }
    }

    interface OnConvertBitmapListener {
        fun onSuccess(bm: Bitmap)
        fun onFailure()
    }
}
60.安装完APK后点打开,然后回到桌面,再点图标打开时出现APP重建,重走启动页

问题描述:

在安装完APK或升级已经安装好的APK完成后,页面会出现两个按钮,一个是“完成”,另一个是“打开”。如果我们选择了“完成”,然后再打开APP,这时候什么事情都没有。但如果我们点击了“打开”,进入APP之后,这时候再回到桌面,再点击图标进入App,我们会发现APP会先打开启动页重新进入主页,而不是打开处于后台的APP,如果我们不清理后台的APP或者彻底关闭APP重新打开,这个问题就会一直存在

原因:

[图片上传失败...(image-f8a4b6-1656569108338)]

解决方法:

 
@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    // 解决APK安装后点打开按钮,打开程序后按home键后再通过APP图标唤醒会重新调用oncreate的问题
    if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {    
        finish();  
        return;  
    }  
    setContentView(R.layout.activity_main);  
}

必须在super.onCreate之后,setContentView之前调用这个方法,并且只有当APP启动的第一个activity的启动模式不为singleTask才有效

61.ViewPager嵌套ViewPager中多层Fragment子ViewPager中加载不出来Fragment

解决方法:
Activity的Fragment要使用FragmentManager(),
而在Fragment中动态的添加Fragment要使用getChildFragmetManager()来管理。

62.禁止EditText自动弹出键盘

解决方法:
在EditText的父布局重设置

android:focusable="true"
android:focusableInTouchMode="true"
63.recyclerview 嵌套 scrollview 没有滑到顶部

原因:两者抢占焦点导致
解决方法:在 ScrollView 的唯一子控件中加入下面属性

android:descendantFocusability="blocksDescendants"
64.输入法上移布局(不只是移到edittext下方)

参考链接:暂无
实现方法:
(1)在AndroidManifest文件的相应Activity添加

android:windowSoftInputMode="adjustResize"

(2)在布局的根控件加入

android:fitsSystemWindows="true"

注意:
在根布局添加fitsSystemWindows属性之后,会多出一块空白,这里提供一个不同于链接的解决方法:
可以先实现沉浸式布局,然后调整上边距即可

65.EditText 的 hint 居中,光标居左

实现方法:这里可以用一个EditText + TextView,并结合 addTextChangedListener 巧妙实现。当输入文字不为空时隐藏TextView,否则显示TextView
具体代码:



                

                

            
et_input.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {}

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                if (TextUtils.isEmpty(s)) {
                    tv_hint.visibility = View.VISIBLE
                } else {
                    tv_hint.visibility = View.GONE
                }
            }

        })
66.接口回调不执行

现象:在自定义PopupWindow中自定义点击回调,自定义view中有log日志,但是在具体实现的地方没有日志
原因:同一个点击事件写了两遍防误触方法,导致最外层回调没有执行

if (!Global.isFastClick(500)) {
67.smoothScrollToPosition和scrollToPosition的区别
68.RecyclerView的置顶,上移和下移的动画效果及逻辑实现

相关问题描述:

[图片上传失败...(image-dd1e79-1656569428090)]

[图片上传失败...(image-b2b495-1656569428090)]

中间的recyclerview是在titlebar之下,底部功能区之上,recyclerview的高度在通过底部功能区的高度动态改变后不会相应的去改变。弹起底部功能区的时候使用smoothScrollToPosition不行,但是改成scrollToPosition却又行了

原因:smoothScrollToPosition的话如果是在可见区域里就不会滚动了

两者区别:两者都是滑动到相应的位置,不过前者是平稳的滑动,后者是直接滑到指定位置,没有动画效果;然后就是smoothScrollToPosition的话如果是在可见区域里就不会滚动了。

69.关闭最近的程序列表中的任务

使用 [finishAndRemoveTask()](https://developer.android.com/reference/android/app/ActivityManager.AppTask?hl=zh-cn#finishAndRemoveTask())方法
参考链接:https://developer.android.com/guide/components/activities/recents?hl=zh-cn#apptask-remove

可供参考的使用场景:

APP中需实现悬浮窗的通话界面,首先要实现悬浮窗效果的话通话界面的launchmode就不能是standard,因为这样之后再开启悬浮窗就浏览不了其他的界面了,然后launchmode比如为singleTask并重新设置taskaffinity时通话界面结束之后在最近的程序列表依然可以找到该任务,再次点击依旧可以进去通话界面,和需求不符。此时我们可以用finishAndRemoveTask方法替换finish方法,在通话界面结束后移除最近的程序列表中的通话界面任务即可。

70.Glide加载大量图片导致数据错乱和OOM

解决思路:
OOM:在RecyclerView滚动时停止加载,空闲时再加载
数据错乱:设置 placeholder 和给 imageview 设置 tag

//在RecyclerView滚动时停止加载,空闲时再加载
messageRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if ([email protected] || [email protected]) {
                        Glide.with(this@ChatActivity).resumeRequests()
                    }
                } else {
                    if ([email protected] || [email protected]) {
                        Glide.with(this@ChatActivity).pauseRequests()
                    }
                }
            }
        })
//设置tag
holder.getView(R.id.iv_message).setTag(R.id.iv_message,getItemPosition(item))
        Glide.with(context).asBitmap().load(item.message?.message_content).placeholder(R.drawable.pai_shibai)
                .into(object : CustomTarget() {
                    override fun onLoadCleared(placeholder: Drawable?) {

                    }

                    override fun onResourceReady(resource: Bitmap, transition: Transition?) {
//                                        val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bg_tarot)
                        val bitmap = resource
                        val picWidth = bitmap.width
                        val picHeight = bitmap.height
                        //判断tag
                        if (getItemPosition(item) == holder.getView(R.id.iv_message).getTag(R.id.iv_message))
                        holder.getView(R.id.iv_message).setImageBitmap(ThumbnailUtils.extractThumbnail(bitmap, DimensionUtils.dip2px(context, 100f), DimensionUtils.dip2px(context, 100f) * picHeight / picWidth))
                    }
                })
71.自适应高度的 viewpager

注意:子view如果有recyclerview的话高度记得改为wrap_content

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.viewpager.widget.ViewPager

class AutoFitHeightViewPager : ViewPager {

    constructor(context: Context) : super(context, null)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        addOnPageChangeListener(object : OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {

            }

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                requestLayout()
            }

            override fun onPageSelected(position: Int) {
                requestLayout()
            }
        })
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val view = getChildAt(currentItem)
        view?.measure(widthMeasureSpec, heightMeasureSpec)

        setMeasuredDimension(measuredWidth, measureHeight(heightMeasureSpec, view))
    }

    private fun measureHeight(measureSpec: Int, view: View?): Int {
        var result = 0
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize
        } else {
            // set the height from the base view if available
            if (view != null) {
                result = view.measuredHeight
            }
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize)
            }
        }
        return result
    }
}
72.EventBus 收不到消息

原因:观察者还未订阅,订阅者就已经发送消息了
解决方法:发送粘性事件即可
注意:还需要在接收方法里面加上 sticky = true

//将图片的url传给购买界面  (MemoryShopBuyAct..)
EventBus.getDefault().postSticky(new CustomizedImageBean(imageBackUrl));

@Subscribe(threadMode = ThreadMode.MAIN,sticky = true) //在ui线程执行
public void getImageUrl(CustomizedImageBean imageBean){
    Logger.e("从CustomizeActivity来的~~~~~~~"+ imageBean.getImageUrl());
    frontImageUrl=imageBean.getImageUrl();
}
73.gradient中的angle属性

解释:0度代表从左到右,90度代表从下到上,180度代表从右到左,270度代表从上到下,并且角度是45的整数倍

74.国际化语言失效

原因之一是 build.gradle 中设置了资源配置,把下面的配置去掉即可

resConfigs("en","US","zh")
75.项目报红,却可以运行

解决方法:点击右上角“project_structive”,查看项目gradle插件版本和gradle版本是否对应,然后去gradle-wrapper.properties文件中修改gradle版本,接着重新下载gradle版本(重点!!!)放进原有目录,最后删除缓存重启AS即可

76.textview文字颜色渐变
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import com.sharp.astrology.R

class GradientColorTextView : androidx.appcompat.widget.AppCompatTextView {

    lateinit var linearGradient: LinearGradient
    lateinit var paint: Paint
    var mWidth = 0
    var mTextBound = Rect()

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun onDraw(canvas: Canvas?) {
        mWidth = measuredWidth
        paint = getPaint()
        val txtStr = text.toString()
        paint.getTextBounds( txtStr,0,txtStr.length,mTextBound)
        linearGradient = LinearGradient(0F,0F,mWidth.toFloat(),0F,Color.parseColor("#FFA343F7"),Color.parseColor("#FF6C62D9"),Shader.TileMode.CLAMP)
        paint.shader = linearGradient
        canvas?.drawText(
            txtStr,
            (measuredWidth / 2 - mTextBound.width() / 2).toFloat(),
            (measuredHeight / 2 + mTextBound.height() / 2).toFloat(),
            paint
        )
    }
}
77.等边距的GridLayoutManager
inner class EvenItemDecoration(private val space: Int, private val column: Int) : RecyclerView.ItemDecoration() {
        override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
            val position = parent.getChildAdapterPosition(view)
            // 每列分配的间隙大小,包括左间隙和右间隙
            val colPadding = space * (column + 1) / column
            // 列索引
            val colIndex = position % column
            // 列左、右空隙。右间隙=space-左间隙
            outRect.left = space * (colIndex + 1) - colPadding * colIndex
            outRect.right = colPadding * (colIndex + 1) - space * (colIndex + 1)
            // 行间距
            if (position >= column) {
                outRect.top = space
            }
        }
    }

你可能感兴趣的:(Android 开发笔记2.0)