From 51d4417c7c21519e715880649b3cff77cdc3ce74 Mon Sep 17 00:00:00 2001 From: yela <306431037@qq.com> Date: Wed, 29 Apr 2020 17:09:02 +0800 Subject: [PATCH] commit --- .gitignore | 14 + .idea/codeStyles/Project.xml | 116 + .idea/gradle.xml | 20 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 77 + app/proguard-rules.pro | 21 + .../baseframe/ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 26 + .../com/example/baseframe/FirstFragment.java | 35 + .../com/example/baseframe/SecondFragment.java | 35 + .../baseframe/api/ActivityManager.java | 185 + .../java/com/example/baseframe/api/App.java | 144 + .../com/example/baseframe/api/AppApi.java | 64 + .../example/baseframe/api/AppApplication.java | 83 + .../example/baseframe/api/CommObserver.java | 115 + .../example/baseframe/api/CommonObserver.java | 75 + .../com/example/baseframe/api/ConfigApi.java | 14 + .../baseframe/api/HeaderInterceptor.java | 143 + .../com/example/baseframe/api/HttpReq.java | 105 + .../baseframe/api/RetrofitFactory.java | 86 + .../example/baseframe/base/AppManager.java | 201 + .../example/baseframe/base/BaseActivity.java | 237 ++ .../example/baseframe/base/BaseFragment.java | 587 +++ .../base/BaseMvvmRecyclerAdapter.java | 144 + .../example/baseframe/base/BaseViewModel.java | 242 ++ .../baseframe/base/ContainerActivity.java | 101 + .../example/baseframe/base/RootActivity.java | 434 ++ .../example/baseframe/bean/ArticlesBean.java | 234 ++ .../example/baseframe/bean/HomeListBean.java | 112 + .../com/example/baseframe/bean/LoginBean.java | 124 + .../example/baseframe/bean/ResultBean.java | 47 + .../example/baseframe/bean/ResultBeans.java | 48 + .../java/com/example/baseframe/bean/User.java | 112 + .../baseframe/bean/WanAndroidBannerBean.java | 109 + .../bindingadapter/DataBindingAdapter.java | 100 + .../bindingadapter/ViewAdapters.java | 85 + .../java/com/example/baseframe/bus/RxBus.java | 112 + .../com/example/baseframe/bus/RxBusCode.java | 12 + .../example/baseframe/bus/RxBusMessage.java | 19 + .../baseframe/bus/RxSubscriptions.java | 36 + .../example/baseframe/bus/RxTimerUtil.java | 280 ++ .../baseframe/bus/event/SingleLiveEvent.java | 75 + .../baseframe/bus/event/SnackbarMessage.java | 52 + .../example/baseframe/crash/CaocConfig.java | 300 ++ .../baseframe/crash/CaocInitProvider.java | 62 + .../crash/CustomActivityOnCrash.java | 690 ++++ .../baseframe/crash/DefaultErrorActivity.java | 125 + .../baseframe/download/DownLoadManager.java | 92 + .../baseframe/download/DownLoadStateBean.java | 77 + .../download/DownLoadSubscriber.java | 37 + .../baseframe/download/ProgressCallBack.java | 101 + .../download/ProgressResponseBody.java | 63 + .../baseframe/downloadapk/DownloadAPk.java | 230 ++ .../example/baseframe/entity/DemoEntity.java | 174 + .../example/baseframe/http/BaseResponse.java | 39 + .../baseframe/http/ExceptionHandle.java | 124 + .../example/baseframe/http/NetworkUtil.java | 178 + .../baseframe/http/ResponseThrowable.java | 15 + .../baseframe/http/cookie/CookieJarImpl.java | 38 + .../http/cookie/store/CookieStore.java | 36 + .../http/cookie/store/MemoryCookieStore.java | 90 + .../cookie/store/PersistentCookieStore.java | 270 ++ .../cookie/store/SerializableHttpCookie.java | 60 + .../http/interceptor/BaseInterceptor.java | 34 + .../http/interceptor/CacheInterceptor.java | 52 + .../baseframe/http/interceptor/logging/I.java | 28 + .../http/interceptor/logging/Level.java | 40 + .../http/interceptor/logging/Logger.java | 18 + .../logging/LoggingInterceptor.java | 235 ++ .../http/interceptor/logging/Printer.java | 232 ++ .../baseframe/listener/ClickListener.java | 12 + .../example/baseframe/listener/Listener.java | 10 + .../baseframe/listener/ResultCallback.java | 8 + .../baseframe/receiver/AppStateReceiver.java | 138 + .../baseframe/receiver/BatteryReceiver.java | 166 + .../baseframe/receiver/NetWorkReceiver.java | 181 + .../baseframe/receiver/PhoneReceiver.java | 177 + .../baseframe/receiver/ScreenReceiver.java | 126 + .../baseframe/receiver/SmsReceiver.java | 155 + .../baseframe/receiver/TimeReceiver.java | 126 + .../baseframe/receiver/WifiReceiver.java | 276 ++ .../java/com/example/baseframe/rx/RxBus.java | 112 + .../com/example/baseframe/rx/RxBusCode.java | 12 + .../example/baseframe/rx/RxBusMessage.java | 19 + .../example/baseframe/rx/RxSubscriptions.java | 36 + .../com/example/baseframe/rx/RxTimerUtil.java | 279 ++ .../baseframe/ui/MainDetailActivity.java | 138 + .../java/com/example/baseframe/ui/Test.java | 21 + .../baseframe/ui/TestDetailFragment.java | 155 + .../baseframe/ui/TestWeightActivity.java | 89 + .../baseframe/ui/main/MainNewActivity.java | 145 + .../ui/viewmodel/MainDetialViewModel.java | 69 + .../ui/viewmodel/MainNewViewModel.java | 134 + .../com/example/baseframe/utils/ADBUtils.java | 2253 ++++++++++ .../baseframe/utils/ActivityUtils.java | 989 +++++ .../baseframe/utils/AnimationsContainer.java | 315 ++ .../com/example/baseframe/utils/AppUtils.java | 1356 ++++++ .../example/baseframe/utils/ArithUtils.java | 218 + .../example/baseframe/utils/ArrayUtils.java | 3665 +++++++++++++++++ .../com/example/baseframe/utils/BarUtils.java | 716 ++++ .../com/example/baseframe/utils/Base64.java | 273 ++ .../example/baseframe/utils/ButtonUtils.java | 49 + .../example/baseframe/utils/CheckNetwork.java | 53 + .../example/baseframe/utils/ClassUtil.java | 43 + .../example/baseframe/utils/ClickUtils.java | 513 +++ .../example/baseframe/utils/CloseUtils.java | 51 + .../example/baseframe/utils/CommUtils.java | 557 +++ .../example/baseframe/utils/ConvertUtils.java | 1839 +++++++++ .../example/baseframe/utils/DensityUtil.java | 106 + .../example/baseframe/utils/DeviceUtils.java | 701 ++++ .../example/baseframe/utils/EncryptUtils.java | 1254 ++++++ .../example/baseframe/utils/FileIOUtils.java | 698 ++++ .../example/baseframe/utils/FileUtils.java | 2127 ++++++++++ .../baseframe/utils/GlideImageLoader.java | 25 + .../com/example/baseframe/utils/GsonUtil.java | 154 + .../example/baseframe/utils/HandlerUtil.java | 24 + .../example/baseframe/utils/ImageUtils.java | 954 +++++ .../example/baseframe/utils/ImageUtilss.java | 2044 +++++++++ .../example/baseframe/utils/IntentUtils.java | 781 ++++ .../example/baseframe/utils/LanguageUtil.java | 148 + .../example/baseframe/utils/QRCodeUtil.java | 332 ++ .../baseframe/utils/ResourceUtils.java | 924 +++++ .../example/baseframe/utils/ScreenUtils.java | 593 +++ .../example/baseframe/utils/ShellUtils.java | 218 + .../baseframe/utils/StatusBarUtil.java | 671 +++ .../example/baseframe/utils/StringUtil.java | 643 +++ .../example/baseframe/utils/ToastUtils.java | 451 ++ .../example/baseframe/utils/VoicePlayer.java | 219 + .../baseframe/utils/animations/Attention.java | 146 + .../baseframe/utils/animations/Bounce.java | 93 + .../baseframe/utils/animations/Fade.java | 152 + .../baseframe/utils/animations/Flip.java | 86 + .../baseframe/utils/animations/Other.java | 40 + .../baseframe/utils/animations/Render.java | 64 + .../baseframe/utils/animations/Rotate.java | 178 + .../utils/animations/RxAnimation.java | 125 + .../baseframe/utils/animations/Slide.java | 139 + .../baseframe/utils/animations/Zoom.java | 176 + .../baseframe/utils/cipher/Base64.java | 735 ++++ .../baseframe/utils/cipher/Base64Cipher.java | 75 + .../baseframe/utils/cipher/Cipher.java | 8 + .../baseframe/utils/cipher/CipherUtils.java | 59 + .../baseframe/utils/cipher/Decrypt.java | 15 + .../baseframe/utils/cipher/Encrypt.java | 15 + .../baseframe/utils/encrypt/AESUtils.java | 79 + .../baseframe/utils/encrypt/CRCUtils.java | 89 + .../baseframe/utils/encrypt/DESUtils.java | 82 + .../baseframe/utils/encrypt/EncryptUtils.java | 994 +++++ .../baseframe/utils/encrypt/EscapeUtils.java | 141 + .../baseframe/utils/encrypt/MD5Utils.java | 158 + .../baseframe/utils/encrypt/SHAUtils.java | 164 + .../utils/encrypt/TripleDESUtils.java | 79 + .../baseframe/utils/encrypt/XorUtils.java | 77 + .../utils/glide/GlideCircleTransform.java | 50 + .../utils/glide/GlideRoundTransform.java | 71 + .../baseframe/utils/glide/GlideUtil.java | 195 + .../utils/permission/PermissionsUtil.java | 236 ++ .../utils/permission/PermissionsUtils.java | 154 + .../baseframe/utils/scroll/GestureHelper.java | 208 + .../baseframe/utils/scroll/ScrollHelper.java | 220 + .../utils/scroll/ViewScrollHelper.java | 42 + .../baseframe/utils/wifi/WifiHotUtils.java | 620 +++ .../baseframe/utils/wifi/WifiUtils.java | 1060 +++++ .../example/baseframe/utils/wifi/WifiVo.java | 148 + .../baseframe/webview/BaseWebAcivity.java | 423 ++ .../example/baseframe/webview/WebTools.java | 193 + .../baseframe/webview/WebViewActivity.java | 223 + .../webview/config/FullscreenHolder.java | 22 + .../webview/config/IWebPageView.java | 87 + .../webview/config/MyJavascriptInterface.java | 64 + .../webview/config/MyWebChromeClient.java | 187 + .../webview/config/MyWebViewClient.java | 118 + .../baseframe/webview/config/WebProgress.java | 359 ++ .../baseframe/weight/AudioWaveView.java | 111 + .../baseframe/weight/CountDownView.java | 150 + .../baseframe/weight/IProgressBar.java | 351 ++ .../baseframe/weight/JumpingBeans.java | 381 ++ .../baseframe/weight/JumpingBeansSpan.java | 147 + .../baseframe/weight/LineWaveVoiceView.java | 175 + .../example/baseframe/weight/LoadDialog.java | 97 + .../baseframe/weight/NoScrollViewPager.java | 58 + .../baseframe/weight/RoundImageView.java | 72 + .../baseframe/weight/RunningTextView.java | 108 + .../baseframe/weight/ShadowLayout.java | 340 ++ .../baseframe/weight/SmartScrollView.java | 91 + .../baseframe/weight/SuperTextView.java | 241 ++ .../baseframe/weight/banner/Banner.java | 621 +++ .../baseframe/weight/banner/BannerConfig.java | 40 + .../weight/banner/BannerScroller.java | 36 + .../baseframe/weight/banner/Transformer.java | 43 + .../baseframe/weight/banner/WeakHandler.java | 478 +++ .../weight/banner/loader/ImageLoader.java | 15 + .../banner/loader/ImageLoaderInterface.java | 14 + .../banner/loader/OnBannerListener.java | 5 + .../banner/transformer/ABaseTransformer.java | 131 + .../transformer/AccordionTransformer.java | 29 + .../BackgroundToForegroundTransformer.java | 36 + .../banner/transformer/CubeInTransformer.java | 36 + .../transformer/CubeOutTransformer.java | 35 + .../transformer/CubePageTransformer.java | 71 + .../transformer/DefaultTransformer.java | 32 + .../transformer/DepthPageTransformer.java | 46 + .../FlipHorizontalTransformer.java | 44 + .../transformer/FlipVerticalTransformer.java | 43 + .../ForegroundToBackgroundTransformer.java | 36 + .../transformer/RotateDownTransformer.java | 41 + .../transformer/RotateUpTransformer.java | 41 + .../transformer/ScaleInOutTransformer.java | 16 + .../banner/transformer/StackTransformer.java | 28 + .../banner/transformer/TabletTransformer.java | 54 + .../banner/transformer/ZoomInTransformer.java | 33 + .../transformer/ZoomOutSlideTransformer.java | 55 + .../banner/transformer/ZoomOutTranformer.java | 36 + .../weight/banner/view/BannerViewPager.java | 48 + .../weight/recyclerview/DividerLine.java | 174 + .../weight/recyclerview/LayoutManagers.java | 135 + .../widget/ControlDistributeLinearLayout.java | 48 + .../customactivityoncrash_error_image.png | Bin 0 -> 4672 bytes .../customactivityoncrash_error_image.png | Bin 0 -> 7790 bytes .../drawable-v24/ic_launcher_foreground.xml | 30 + .../customactivityoncrash_error_image.png | Bin 0 -> 11080 bytes .../res/drawable-xhdpi/login_clear_input.xml | 5 + .../res/drawable-xxhdpi/actionbar_more.png | Bin 0 -> 222 bytes .../main/res/drawable-xxhdpi/back_white.png | Bin 0 -> 1563 bytes .../main/res/drawable-xxhdpi/bg_no_data.png | Bin 0 -> 20139 bytes .../customactivityoncrash_error_image.png | Bin 0 -> 17641 bytes .../res/drawable-xxhdpi/home_arrow_right.png | Bin 0 -> 1090 bytes .../drawable-xxhdpi/home_arrow_right_gray.png | Bin 0 -> 1107 bytes .../main/res/drawable-xxhdpi/icon_back.png | Bin 0 -> 887 bytes .../res/drawable-xxhdpi/indicator_select.png | Bin 0 -> 230 bytes .../drawable-xxhdpi/indicator_unselect.png | Bin 0 -> 196 bytes .../main/res/drawable-xxhdpi/no_banner.png | Bin 0 -> 2545 bytes .../customactivityoncrash_error_image.png | Bin 0 -> 27398 bytes .../main/res/drawable/black_background.xml | 6 + .../main/res/drawable/dialog_shape_bgs.xml | 10 + app/src/main/res/drawable/gray_radius.xml | 7 + .../res/drawable/ic_launcher_background.xml | 170 + .../main/res/drawable/pb_notify_custom.xml | 29 + .../main/res/drawable/progress_large_holo.xml | 12 + .../main/res/drawable/shape_bg_loading.xml | 6 + .../res/drawable/spinner_76_inner_holo.png | Bin 0 -> 4728 bytes .../res/drawable/spinner_76_outer_holo.png | Bin 0 -> 3438 bytes app/src/main/res/drawable/toast_bg.png | Bin 0 -> 17740 bytes app/src/main/res/drawable/toast_shape_bg.xml | 10 + app/src/main/res/drawable/white_radius.xml | 8 + app/src/main/res/layout/activity_binding.xml | 20 + .../main/res/layout/activity_container.xml | 5 + .../main/res/layout/activity_main_detail.xml | 126 + app/src/main/res/layout/activity_new_main.xml | 44 + app/src/main/res/layout/activity_tab_bar.xml | 30 + .../main/res/layout/activity_test_weight.xml | 157 + app/src/main/res/layout/activity_webview.xml | 83 + app/src/main/res/layout/banner.xml | 95 + app/src/main/res/layout/comm_load_dialogs.xml | 39 + app/src/main/res/layout/comm_show_dialogs.xml | 102 + app/src/main/res/layout/content_main.xml | 19 + app/src/main/res/layout/custom_notify.xml | 53 + ...activityoncrash_default_error_activity.xml | 56 + app/src/main/res/layout/empty_view.xml | 29 + .../main/res/layout/fragment_base_pager.xml | 42 + app/src/main/res/layout/fragment_first.xml | 28 + app/src/main/res/layout/fragment_second.xml | 27 + app/src/main/res/layout/header_new_wan.xml | 28 + app/src/main/res/layout/item_message.xml | 105 + app/src/main/res/layout/title_layout.xml | 47 + app/src/main/res/layout/toast.xml | 28 + .../res/layout/video_loading_progress.xml | 38 + app/src/main/res/menu/menu_main.xml | 10 + app/src/main/res/menu/menu_webview.xml | 29 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes app/src/main/res/mipmap-mdpi/back.png | Bin 0 -> 1888 bytes app/src/main/res/mipmap-mdpi/btn_login.png | Bin 0 -> 15290 bytes app/src/main/res/mipmap-mdpi/clean_edit.png | Bin 0 -> 1885 bytes app/src/main/res/mipmap-mdpi/huanzhe.png | Bin 0 -> 1976 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes app/src/main/res/mipmap-xxhdpi/login_back.png | Bin 0 -> 60151 bytes app/src/main/res/mipmap-xxhdpi/logo.png | Bin 0 -> 21046 bytes .../main/res/mipmap-xxhdpi/password_icon.png | Bin 0 -> 1971 bytes .../mipmap-xxhdpi/registered_delete_bule.png | Bin 0 -> 2384 bytes .../mipmap-xxhdpi/registered_delete_grey.png | Bin 0 -> 2378 bytes .../mipmap-xxhdpi/registered_show_blue.png | Bin 0 -> 1736 bytes .../mipmap-xxhdpi/registered_show_white.png | Bin 0 -> 19482 bytes app/src/main/res/mipmap-xxhdpi/show_psw.png | Bin 0 -> 1194 bytes .../main/res/mipmap-xxhdpi/show_psw_press.png | Bin 0 -> 896 bytes .../main/res/mipmap-xxhdpi/toolbar_more.png | Bin 0 -> 601 bytes app/src/main/res/mipmap-xxhdpi/user_edit.png | Bin 0 -> 10094 bytes app/src/main/res/mipmap-xxhdpi/user_icon.png | Bin 0 -> 2294 bytes .../main/res/mipmap-xxhdpi/wode_select.png | Bin 0 -> 3198 bytes .../main/res/mipmap-xxhdpi/xiaoxi_select.png | Bin 0 -> 2590 bytes app/src/main/res/mipmap-xxhdpi/yingyong.png | Bin 0 -> 1798 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes app/src/main/res/navigation/nav_graph.xml | 28 + app/src/main/res/values/attrs.xml | 171 + app/src/main/res/values/colors.xml | 85 + app/src/main/res/values/dimens.xml | 11 + app/src/main/res/values/ids.xml | 18 + app/src/main/res/values/strings.xml | 14 + app/src/main/res/values/styles.xml | 186 + .../example/baseframe/ExampleUnitTest.java | 17 + build.gradle | 30 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + settings.gradle | 2 + 318 files changed, 52751 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/baseframe/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/baseframe/FirstFragment.java create mode 100644 app/src/main/java/com/example/baseframe/SecondFragment.java create mode 100644 app/src/main/java/com/example/baseframe/api/ActivityManager.java create mode 100644 app/src/main/java/com/example/baseframe/api/App.java create mode 100644 app/src/main/java/com/example/baseframe/api/AppApi.java create mode 100644 app/src/main/java/com/example/baseframe/api/AppApplication.java create mode 100644 app/src/main/java/com/example/baseframe/api/CommObserver.java create mode 100644 app/src/main/java/com/example/baseframe/api/CommonObserver.java create mode 100644 app/src/main/java/com/example/baseframe/api/ConfigApi.java create mode 100644 app/src/main/java/com/example/baseframe/api/HeaderInterceptor.java create mode 100644 app/src/main/java/com/example/baseframe/api/HttpReq.java create mode 100644 app/src/main/java/com/example/baseframe/api/RetrofitFactory.java create mode 100644 app/src/main/java/com/example/baseframe/base/AppManager.java create mode 100644 app/src/main/java/com/example/baseframe/base/BaseActivity.java create mode 100644 app/src/main/java/com/example/baseframe/base/BaseFragment.java create mode 100644 app/src/main/java/com/example/baseframe/base/BaseMvvmRecyclerAdapter.java create mode 100644 app/src/main/java/com/example/baseframe/base/BaseViewModel.java create mode 100644 app/src/main/java/com/example/baseframe/base/ContainerActivity.java create mode 100644 app/src/main/java/com/example/baseframe/base/RootActivity.java create mode 100644 app/src/main/java/com/example/baseframe/bean/ArticlesBean.java create mode 100644 app/src/main/java/com/example/baseframe/bean/HomeListBean.java create mode 100644 app/src/main/java/com/example/baseframe/bean/LoginBean.java create mode 100644 app/src/main/java/com/example/baseframe/bean/ResultBean.java create mode 100644 app/src/main/java/com/example/baseframe/bean/ResultBeans.java create mode 100644 app/src/main/java/com/example/baseframe/bean/User.java create mode 100644 app/src/main/java/com/example/baseframe/bean/WanAndroidBannerBean.java create mode 100644 app/src/main/java/com/example/baseframe/bindingadapter/DataBindingAdapter.java create mode 100644 app/src/main/java/com/example/baseframe/bindingadapter/ViewAdapters.java create mode 100644 app/src/main/java/com/example/baseframe/bus/RxBus.java create mode 100644 app/src/main/java/com/example/baseframe/bus/RxBusCode.java create mode 100644 app/src/main/java/com/example/baseframe/bus/RxBusMessage.java create mode 100644 app/src/main/java/com/example/baseframe/bus/RxSubscriptions.java create mode 100644 app/src/main/java/com/example/baseframe/bus/RxTimerUtil.java create mode 100644 app/src/main/java/com/example/baseframe/bus/event/SingleLiveEvent.java create mode 100644 app/src/main/java/com/example/baseframe/bus/event/SnackbarMessage.java create mode 100644 app/src/main/java/com/example/baseframe/crash/CaocConfig.java create mode 100644 app/src/main/java/com/example/baseframe/crash/CaocInitProvider.java create mode 100644 app/src/main/java/com/example/baseframe/crash/CustomActivityOnCrash.java create mode 100644 app/src/main/java/com/example/baseframe/crash/DefaultErrorActivity.java create mode 100644 app/src/main/java/com/example/baseframe/download/DownLoadManager.java create mode 100644 app/src/main/java/com/example/baseframe/download/DownLoadStateBean.java create mode 100644 app/src/main/java/com/example/baseframe/download/DownLoadSubscriber.java create mode 100644 app/src/main/java/com/example/baseframe/download/ProgressCallBack.java create mode 100644 app/src/main/java/com/example/baseframe/download/ProgressResponseBody.java create mode 100644 app/src/main/java/com/example/baseframe/downloadapk/DownloadAPk.java create mode 100644 app/src/main/java/com/example/baseframe/entity/DemoEntity.java create mode 100644 app/src/main/java/com/example/baseframe/http/BaseResponse.java create mode 100644 app/src/main/java/com/example/baseframe/http/ExceptionHandle.java create mode 100644 app/src/main/java/com/example/baseframe/http/NetworkUtil.java create mode 100644 app/src/main/java/com/example/baseframe/http/ResponseThrowable.java create mode 100644 app/src/main/java/com/example/baseframe/http/cookie/CookieJarImpl.java create mode 100644 app/src/main/java/com/example/baseframe/http/cookie/store/CookieStore.java create mode 100644 app/src/main/java/com/example/baseframe/http/cookie/store/MemoryCookieStore.java create mode 100644 app/src/main/java/com/example/baseframe/http/cookie/store/PersistentCookieStore.java create mode 100644 app/src/main/java/com/example/baseframe/http/cookie/store/SerializableHttpCookie.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/BaseInterceptor.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/CacheInterceptor.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/logging/I.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/logging/Level.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/logging/Logger.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/logging/LoggingInterceptor.java create mode 100644 app/src/main/java/com/example/baseframe/http/interceptor/logging/Printer.java create mode 100644 app/src/main/java/com/example/baseframe/listener/ClickListener.java create mode 100644 app/src/main/java/com/example/baseframe/listener/Listener.java create mode 100644 app/src/main/java/com/example/baseframe/listener/ResultCallback.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/AppStateReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/BatteryReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/NetWorkReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/PhoneReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/ScreenReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/SmsReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/TimeReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/receiver/WifiReceiver.java create mode 100644 app/src/main/java/com/example/baseframe/rx/RxBus.java create mode 100644 app/src/main/java/com/example/baseframe/rx/RxBusCode.java create mode 100644 app/src/main/java/com/example/baseframe/rx/RxBusMessage.java create mode 100644 app/src/main/java/com/example/baseframe/rx/RxSubscriptions.java create mode 100644 app/src/main/java/com/example/baseframe/rx/RxTimerUtil.java create mode 100644 app/src/main/java/com/example/baseframe/ui/MainDetailActivity.java create mode 100644 app/src/main/java/com/example/baseframe/ui/Test.java create mode 100644 app/src/main/java/com/example/baseframe/ui/TestDetailFragment.java create mode 100644 app/src/main/java/com/example/baseframe/ui/TestWeightActivity.java create mode 100644 app/src/main/java/com/example/baseframe/ui/main/MainNewActivity.java create mode 100644 app/src/main/java/com/example/baseframe/ui/viewmodel/MainDetialViewModel.java create mode 100644 app/src/main/java/com/example/baseframe/ui/viewmodel/MainNewViewModel.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ADBUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ActivityUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/AnimationsContainer.java create mode 100644 app/src/main/java/com/example/baseframe/utils/AppUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ArithUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ArrayUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/BarUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/Base64.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ButtonUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/CheckNetwork.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ClassUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ClickUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/CloseUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/CommUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ConvertUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/DensityUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/DeviceUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/EncryptUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/FileIOUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/FileUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/GlideImageLoader.java create mode 100644 app/src/main/java/com/example/baseframe/utils/GsonUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/HandlerUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ImageUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ImageUtilss.java create mode 100644 app/src/main/java/com/example/baseframe/utils/IntentUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/LanguageUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/QRCodeUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ResourceUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ScreenUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ShellUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/StatusBarUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/StringUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/ToastUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/VoicePlayer.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Attention.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Bounce.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Fade.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Flip.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Other.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Render.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Rotate.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/RxAnimation.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Slide.java create mode 100644 app/src/main/java/com/example/baseframe/utils/animations/Zoom.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/Base64.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/Base64Cipher.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/Cipher.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/CipherUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/Decrypt.java create mode 100644 app/src/main/java/com/example/baseframe/utils/cipher/Encrypt.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/AESUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/CRCUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/DESUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/EncryptUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/EscapeUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/MD5Utils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/SHAUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/TripleDESUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/encrypt/XorUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/glide/GlideCircleTransform.java create mode 100644 app/src/main/java/com/example/baseframe/utils/glide/GlideRoundTransform.java create mode 100644 app/src/main/java/com/example/baseframe/utils/glide/GlideUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtil.java create mode 100644 app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/scroll/GestureHelper.java create mode 100644 app/src/main/java/com/example/baseframe/utils/scroll/ScrollHelper.java create mode 100644 app/src/main/java/com/example/baseframe/utils/scroll/ViewScrollHelper.java create mode 100644 app/src/main/java/com/example/baseframe/utils/wifi/WifiHotUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/wifi/WifiUtils.java create mode 100644 app/src/main/java/com/example/baseframe/utils/wifi/WifiVo.java create mode 100644 app/src/main/java/com/example/baseframe/webview/BaseWebAcivity.java create mode 100644 app/src/main/java/com/example/baseframe/webview/WebTools.java create mode 100644 app/src/main/java/com/example/baseframe/webview/WebViewActivity.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/FullscreenHolder.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/IWebPageView.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/MyJavascriptInterface.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/MyWebChromeClient.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/MyWebViewClient.java create mode 100644 app/src/main/java/com/example/baseframe/webview/config/WebProgress.java create mode 100644 app/src/main/java/com/example/baseframe/weight/AudioWaveView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/CountDownView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/IProgressBar.java create mode 100644 app/src/main/java/com/example/baseframe/weight/JumpingBeans.java create mode 100644 app/src/main/java/com/example/baseframe/weight/JumpingBeansSpan.java create mode 100644 app/src/main/java/com/example/baseframe/weight/LineWaveVoiceView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/LoadDialog.java create mode 100644 app/src/main/java/com/example/baseframe/weight/NoScrollViewPager.java create mode 100644 app/src/main/java/com/example/baseframe/weight/RoundImageView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/RunningTextView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/ShadowLayout.java create mode 100644 app/src/main/java/com/example/baseframe/weight/SmartScrollView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/SuperTextView.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/Banner.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/BannerConfig.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/BannerScroller.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/Transformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/WeakHandler.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoader.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoaderInterface.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/loader/OnBannerListener.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ABaseTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/AccordionTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/BackgroundToForegroundTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeInTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeOutTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/CubePageTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/DefaultTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/DepthPageTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipHorizontalTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipVerticalTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ForegroundToBackgroundTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateDownTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateUpTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ScaleInOutTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/StackTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/TabletTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomInTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutSlideTransformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutTranformer.java create mode 100644 app/src/main/java/com/example/baseframe/weight/banner/view/BannerViewPager.java create mode 100644 app/src/main/java/com/example/baseframe/weight/recyclerview/DividerLine.java create mode 100644 app/src/main/java/com/example/baseframe/weight/recyclerview/LayoutManagers.java create mode 100644 app/src/main/java/com/example/baseframe/widget/ControlDistributeLinearLayout.java create mode 100644 app/src/main/res/drawable-mdpi/customactivityoncrash_error_image.png create mode 100644 app/src/main/res/drawable-v24/customactivityoncrash_error_image.png create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable-xhdpi/customactivityoncrash_error_image.png create mode 100644 app/src/main/res/drawable-xhdpi/login_clear_input.xml create mode 100644 app/src/main/res/drawable-xxhdpi/actionbar_more.png create mode 100644 app/src/main/res/drawable-xxhdpi/back_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/bg_no_data.png create mode 100644 app/src/main/res/drawable-xxhdpi/customactivityoncrash_error_image.png create mode 100644 app/src/main/res/drawable-xxhdpi/home_arrow_right.png create mode 100644 app/src/main/res/drawable-xxhdpi/home_arrow_right_gray.png create mode 100644 app/src/main/res/drawable-xxhdpi/icon_back.png create mode 100644 app/src/main/res/drawable-xxhdpi/indicator_select.png create mode 100644 app/src/main/res/drawable-xxhdpi/indicator_unselect.png create mode 100644 app/src/main/res/drawable-xxhdpi/no_banner.png create mode 100644 app/src/main/res/drawable-xxxhdpi/customactivityoncrash_error_image.png create mode 100644 app/src/main/res/drawable/black_background.xml create mode 100644 app/src/main/res/drawable/dialog_shape_bgs.xml create mode 100644 app/src/main/res/drawable/gray_radius.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/pb_notify_custom.xml create mode 100644 app/src/main/res/drawable/progress_large_holo.xml create mode 100644 app/src/main/res/drawable/shape_bg_loading.xml create mode 100644 app/src/main/res/drawable/spinner_76_inner_holo.png create mode 100644 app/src/main/res/drawable/spinner_76_outer_holo.png create mode 100644 app/src/main/res/drawable/toast_bg.png create mode 100644 app/src/main/res/drawable/toast_shape_bg.xml create mode 100644 app/src/main/res/drawable/white_radius.xml create mode 100644 app/src/main/res/layout/activity_binding.xml create mode 100644 app/src/main/res/layout/activity_container.xml create mode 100644 app/src/main/res/layout/activity_main_detail.xml create mode 100644 app/src/main/res/layout/activity_new_main.xml create mode 100644 app/src/main/res/layout/activity_tab_bar.xml create mode 100644 app/src/main/res/layout/activity_test_weight.xml create mode 100644 app/src/main/res/layout/activity_webview.xml create mode 100644 app/src/main/res/layout/banner.xml create mode 100644 app/src/main/res/layout/comm_load_dialogs.xml create mode 100644 app/src/main/res/layout/comm_show_dialogs.xml create mode 100644 app/src/main/res/layout/content_main.xml create mode 100644 app/src/main/res/layout/custom_notify.xml create mode 100644 app/src/main/res/layout/customactivityoncrash_default_error_activity.xml create mode 100644 app/src/main/res/layout/empty_view.xml create mode 100644 app/src/main/res/layout/fragment_base_pager.xml create mode 100644 app/src/main/res/layout/fragment_first.xml create mode 100644 app/src/main/res/layout/fragment_second.xml create mode 100644 app/src/main/res/layout/header_new_wan.xml create mode 100644 app/src/main/res/layout/item_message.xml create mode 100644 app/src/main/res/layout/title_layout.xml create mode 100644 app/src/main/res/layout/toast.xml create mode 100644 app/src/main/res/layout/video_loading_progress.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/menu/menu_webview.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/back.png create mode 100644 app/src/main/res/mipmap-mdpi/btn_login.png create mode 100644 app/src/main/res/mipmap-mdpi/clean_edit.png create mode 100644 app/src/main/res/mipmap-mdpi/huanzhe.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/login_back.png create mode 100644 app/src/main/res/mipmap-xxhdpi/logo.png create mode 100644 app/src/main/res/mipmap-xxhdpi/password_icon.png create mode 100644 app/src/main/res/mipmap-xxhdpi/registered_delete_bule.png create mode 100644 app/src/main/res/mipmap-xxhdpi/registered_delete_grey.png create mode 100644 app/src/main/res/mipmap-xxhdpi/registered_show_blue.png create mode 100644 app/src/main/res/mipmap-xxhdpi/registered_show_white.png create mode 100644 app/src/main/res/mipmap-xxhdpi/show_psw.png create mode 100644 app/src/main/res/mipmap-xxhdpi/show_psw_press.png create mode 100644 app/src/main/res/mipmap-xxhdpi/toolbar_more.png create mode 100644 app/src/main/res/mipmap-xxhdpi/user_edit.png create mode 100644 app/src/main/res/mipmap-xxhdpi/user_icon.png create mode 100644 app/src/main/res/mipmap-xxhdpi/wode_select.png create mode 100644 app/src/main/res/mipmap-xxhdpi/xiaoxi_select.png create mode 100644 app/src/main/res/mipmap-xxhdpi/yingyong.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/example/baseframe/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..5cd135a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..5fca2f5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,77 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "com.example.baseframe" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions{ + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + dataBinding { + enabled true + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment:2.3.0-alpha05' + implementation 'androidx.navigation:navigation-ui:2.3.0-alpha05' + implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0' + implementation 'io.reactivex.rxjava3:rxjava:3.0.2' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + implementation 'com.trello.rxlifecycle3:rxlifecycle:3.1.0' + implementation 'com.trello.rxlifecycle3:rxlifecycle-components:3.1.0' + implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0' + implementation 'com.github.tbruyelle:rxpermissions:0.10.2' + implementation 'com.squareup.okhttp3:okhttp:4.0.0' + api 'com.squareup.okhttp3:logging-interceptor:4.0.0' + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.8.1' + implementation 'com.github.bumptech.glide:glide:4.11.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.afollestad.material-dialogs:core:0.9.6.0' + implementation 'com.afollestad.material-dialogs:commons:0.9.4.5' + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter:4.0.0' + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-recyclerview:4.0.0' + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-viewpager2:4.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' + //ALog + api 'com.blankj:alog:1.9.1' + api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.50' + // //智能下拉刷新 + api 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0-alpha-14' + //滴滴的调试插件 https://github.com/didi/DoraemonKit + debugImplementation 'com.didichuxing.doraemonkit:doraemonkit:2.0.1' + releaseImplementation 'com.didichuxing.doraemonkit:doraemonkit-no-op:2.0.2' + implementation 'androidx.multidex:multidex:2.0.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/example/baseframe/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/baseframe/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3c1d7ef --- /dev/null +++ b/app/src/androidTest/java/com/example/baseframe/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.baseframe; + +import android.content.Context; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.example.baseframe", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a839f0e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/FirstFragment.java b/app/src/main/java/com/example/baseframe/FirstFragment.java new file mode 100644 index 0000000..26c9e6c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/FirstFragment.java @@ -0,0 +1,35 @@ +package com.example.baseframe; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import androidx.navigation.fragment.NavHostFragment; + +public class FirstFragment extends Fragment { + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_first, container, false); + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavHostFragment.findNavController(FirstFragment.this) + .navigate(R.id.action_FirstFragment_to_SecondFragment); + } + }); + } +} diff --git a/app/src/main/java/com/example/baseframe/SecondFragment.java b/app/src/main/java/com/example/baseframe/SecondFragment.java new file mode 100644 index 0000000..9634fd8 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/SecondFragment.java @@ -0,0 +1,35 @@ +package com.example.baseframe; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import androidx.navigation.fragment.NavHostFragment; + +public class SecondFragment extends Fragment { + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_second, container, false); + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + view.findViewById(R.id.button_second).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavHostFragment.findNavController(SecondFragment.this) + .navigate(R.id.action_SecondFragment_to_FirstFragment); + } + }); + } +} diff --git a/app/src/main/java/com/example/baseframe/api/ActivityManager.java b/app/src/main/java/com/example/baseframe/api/ActivityManager.java new file mode 100644 index 0000000..a250ef4 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/ActivityManager.java @@ -0,0 +1,185 @@ +package com.example.baseframe.api; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.example.baseframe.listener.Listener; +import com.example.baseframe.ui.main.MainNewActivity; + + +import java.util.Stack; + + +public class ActivityManager { + + private static ActivityManager manager; + public static ActivityManager getInstance(){ + if(manager==null){ + synchronized (ActivityManager.class){ + if(manager==null){ + manager= new ActivityManager(); + } + } + } + return manager; + } + + private ActivityManager() { + activities = new Stack<>(); + } + + private Stack activities; + + public void addActivity(Activity activity){ + activities.add(activity); + } + + public void removeActivity(Activity act) { + if (activities != null) { + activities.remove(act); + } + } + + /** + * + * @description 结束给定的Activity实例 + */ + public void removeFinishActivity(Activity activity){ + if (activity !=null){ + activities.remove(activity); + activity.finish(); + activity = null; + } + } + + /** + * + * @description 结束当前的Activity实例 + * @date: 2019/10/25 10:12 + * @return activity的实例 + */ + public void finishCurrentActivity(){ + Activity activity = activities.lastElement(); + removeFinishActivity(activity); + } + + /** + * + * @description 获取当前的Activity实例 + * @date: 2019/10/25 10:12 + * @return activity的实例 + */ + public Activity getCurrentActivity(){ + if (!activities.empty()){ + return activities.lastElement(); + } + return null; + } + + /** + * + * @description 结束指定类名的Activity + * @date: 2019/10/25 10:12 + * @param cls + */ + public void finishTargetActivity(Class cls){ + for (Activity activity: activities) { + if (activity.getClass().getName().equals(cls.getName())){ + removeFinishActivity(activity); + } + } + } + + public void finishAllActivity(){ + for (Activity activity: activities) { + if (null != activity){ + activity.finish(); + activity = null; + } + } + activities.clear(); + } + + public void exitApp(Context context){ + try { + finishAllActivity(); + System.exit(0); + }catch (Exception e){ + Log.e("error","退出程序失败"); + } + } + + + /** + * 结束指定的activity + * @param activityName + */ + public void finishActivity(Listener listener, Class... activityName) { + if(activityName==null||activities==null||activities.isEmpty()){ + listener.onResult(); + return; + } + + for (int j = 0; j WebViewActivity.loadUrl(url,"DoraemonKit测试")); + } + + public void initALog() { + ALog.Config config = ALog.init(this) + // 设置 log 总开关,包括输出到控制台和文件,默认开 + .setLogSwitch(BuildConfig.DEBUG) + // 设置是否输出到控制台开关,默认开 + .setConsoleSwitch(BuildConfig.DEBUG) + // 设置 log 全局标签,默认为空 + .setGlobalTag(null) + // 当全局标签不为空时,我们输出的 log 全部为该 tag, + // 为空时,如果传入的 tag 为空那就显示类名,否则显示 tag + // 设置 log 头信息开关,默认为开 + .setLogHeadSwitch(true) + // 打印 log 时是否存到文件的开关,默认关 + .setLog2FileSwitch(false) + // 当自定义路径为空时,写入应用的 /cache/log/ 目录中 + .setDir("") + // 当文件前缀为空时,默认为 "alog",即写入文件为 "alog-MM-dd.txt" + .setFilePrefix("") + // 输出日志是否带边框开关,默认开 + .setBorderSwitch(true) + // 一条日志仅输出一条,默认开,为美化 AS 3.1 的 Logcat + .setSingleTagSwitch(false) + // log 的控制台过滤器,和 logcat 过滤器同理,默认 Verbose + .setConsoleFilter(ALog.V) + // log 文件过滤器,和 logcat 过滤器同理,默认 Verbose + .setFileFilter(ALog.V) + // log 栈深度,默认为 1 + .setStackDeep(1) + // 设置栈偏移,比如二次封装的话就需要设置,默认为 0 + .setStackOffset(0) + // 设置日志可保留天数,默认为 -1 表示无限时长 + .setSaveDays(3) + // 新增 ArrayList 格式化器,默认已支持 Array, Throwable, Bundle, Intent 的格式化输出 + .addFormatter(new ALog.IFormatter() { + @Override + public String format(ArrayList list) { + return "ALog Formatter ArrayList { " + list.toString() + " }"; + } + }); + ALog.d(config.toString()); + } + + + + /** + *设置SmartRefreshLayout默认的header Footer样式,需要在布局生成之前设置,建议代码放在 Application 中 + */ + private void setSmartRefreshLayout() { + //设置全局的Header构建器 + SmartRefreshLayout.setDefaultRefreshHeaderCreator(new DefaultRefreshHeaderCreator() { + @NonNull + @Override + public RefreshHeader createRefreshHeader(@NonNull Context context, @NonNull RefreshLayout layout) { + ClassicsHeader header = new ClassicsHeader(context).setSpinnerStyle(SpinnerStyle.Scale); + header.setPrimaryColorId(R.color.ui_activity_bg); + header.setAccentColorId( R.color.ui_gray); + header.setTextSizeTime(13); + header.setTextSizeTitle(15); + return header;//指定为经典Header,默认是 贝塞尔雷达Header + } + }); + //设置全局的Footer构建器 +// SmartRefreshLayout.setDefaultRefreshFooterCreator(new DefaultRefreshFooterCreator() { +// @NonNull +// @Override +// public RefreshFooter createRefreshFooter(@NonNull Context context, @NonNull RefreshLayout layout) { +// layout.setEnableLoadMoreWhenContentNotFull(true);//内容不满一页时候启用加载更多 +// ClassicsFooter footer = new ClassicsFooter(context); +// footer.setTextSizeTitle(15); +// footer.setBackgroundResource(android.R.color.transparent); +// footer.setSpinnerStyle(SpinnerStyle.Scale);//设置为拉伸模式 +// return footer;//指定为经典Footer,默认是 BallPulseFooter +// } +// }); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/api/AppApi.java b/app/src/main/java/com/example/baseframe/api/AppApi.java new file mode 100644 index 0000000..cee96c6 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/AppApi.java @@ -0,0 +1,64 @@ +package com.example.baseframe.api; + +import com.example.baseframe.bean.HomeListBean; +import com.example.baseframe.bean.LoginBean; +import com.example.baseframe.bean.ResultBean; +import com.example.baseframe.bean.ResultBeans; +import com.example.baseframe.bean.WanAndroidBannerBean; + + +import java.util.Map; + +import io.reactivex.Observable; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +/** + * @Description: AppApi类作用描述 + * @Author: yzh + * @CreateDate: 2019/11/16 14:17 + */ +public interface AppApi { + + /** + * 玩安卓轮播图 + */ + @GET("banner/json") + Observable> getWanBanner(); + /** + * 玩安卓,文章列表、知识体系下的文章 + * + * @param page 页码,从0开始 + * @param cid 体系id + */ + @GET("article/list/{page}/json") + Observable> getHomeList(@Path("page") int page, @Query("cid") Integer cid); + + @GET("article/list/{page}/json") + Observable> getHomeList(@Path("page") int page, @QueryMap Map map); + + + + + /** + * 玩安卓登录 + * + * @param username 用户名 + * @param password 密码 + */ + @FormUrlEncoded + @POST("user/login") + Observable login(@Field("username") String username, @Field("password") String password); + + /** + * 玩安卓注册 + */ + @FormUrlEncoded + @POST("user/register") + Observable register(@Field("username") String username, @Field("password") String password, @Field("repassword") String repassword); +} diff --git a/app/src/main/java/com/example/baseframe/api/AppApplication.java b/app/src/main/java/com/example/baseframe/api/AppApplication.java new file mode 100644 index 0000000..ac48432 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/AppApplication.java @@ -0,0 +1,83 @@ +package com.example.baseframe.api; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.crash.CaocConfig; +import com.example.baseframe.ui.main.MainNewActivity; + + +/** + * Anthor yzh Date 2019/11/6 10:36 + */ +public class AppApplication extends App { + + @Override + public void onCreate() { + super.onCreate(); + initCrash(); + registerActivityLifecycleCallbacks(this); + } + + private void initCrash() { + CaocConfig.Builder.create() + .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,开启沉浸式 + .enabled(true) //是否启动全局异常捕获 + .showErrorDetails(true) //是否显示错误详细信息 + .showRestartButton(true) //是否显示重启按钮 + .trackActivities(true) //是否跟踪Activity + .minTimeBetweenCrashesMs(2000) //崩溃的间隔时间(毫秒) + .errorDrawable(R.drawable.customactivityoncrash_error_image) //错误图标 + .restartActivity(MainNewActivity.class) //重新启动后的activity +// .errorActivity(MyDefaultErrorActivity.class) //崩溃后的错误activity(不设置使用默认) +// .eventListener(new YourCustomEventListener()) //崩溃后的错误监听 + .apply(); + } + + + //注册activity生命周期, activity生命周期监听可以做一些事情 + private void registerActivityLifecycleCallbacks(Application application) { + application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) { + ALog.v(activity.getClass().getSimpleName()+"-onActivityCreated"); + ActivityManager.getInstance().addActivity(activity); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + ALog.v(activity.getClass().getSimpleName()+"-onActivityResumed"); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + ALog.v(activity.getClass().getSimpleName()+"-onActivityDestroyed"); + ActivityManager.getInstance().removeActivity(activity); + } + }); + } + + + +} diff --git a/app/src/main/java/com/example/baseframe/api/CommObserver.java b/app/src/main/java/com/example/baseframe/api/CommObserver.java new file mode 100644 index 0000000..f70cd53 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/CommObserver.java @@ -0,0 +1,115 @@ +package com.example.baseframe.api; + +import android.content.Context; + +import com.blankj.ALog; +import com.example.baseframe.bean.ResultBean; +import com.example.baseframe.bean.ResultBeans; +import com.example.baseframe.weight.LoadDialog; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +import static com.example.baseframe.api.ConfigApi.ERROR_CODE; + +/** + * date : 2019/10/21 + * desc : 请求是否加载等待框 + */ +public abstract class CommObserver implements Observer { + + protected LoadDialog dialog; + private boolean isShowDialog; + private Context context; + + public CommObserver(Context context, boolean isShowDialog) { + this.context = context; + this.isShowDialog = isShowDialog; + if (this.isShowDialog && dialog == null) { + createLoading(); + } + } + + + + public abstract void success(T data); + + public abstract void error(Throwable e); + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + error(e); + dismiss(); + ALog.e("onError==="+e.toString()); + } + + @Override + public void onNext(T t) { + success(t); + dismiss(); + + if(t instanceof ResultBean){ + ResultBean bean= (ResultBean) t; + if(bean.getErrorCode()==ERROR_CODE){ + ALog.e("请求失败========"+ERROR_CODE+" "+bean.getErrorMsg()); + } + }else if(t instanceof ResultBeans){ + ResultBeans bean= (ResultBeans) t; + if(bean.getErrorCode()==ERROR_CODE){ + ALog.e("请求失败~~~~~~~~"+ERROR_CODE+" "+bean.getErrorMsg()); + } + } + + } + + @Override + public void onSubscribe(Disposable d) { + if (isShowDialog && dialog != null) { + dialog.show(); + } + + + } + + @Override + public void onComplete() { + dismiss(); + } + + /** + * 请求时的进度条 + * @param cancelAble 是否能取消,true_点击外部和返回时取消loading,false_点外部不能取消loading, + */ + public void createLoading(final boolean... cancelAble) { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + return; + } + try { + + if (cancelAble == null || cancelAble.length == 0) { + dialog = new LoadDialog(context, "Loading...", false);//默认不可关闭 + } else { + dialog = new LoadDialog(context, "Loading...", cancelAble[0]); + } + dialog.show(); + + } catch (Exception e) { + // handle exception + // Log.e(TAG, e.toString()); + } + + } + + public void dismiss(){ + if (dialog != null) { + try { + dialog.dismiss(); + dialog = null; + } catch (Exception e) { + } + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/api/CommonObserver.java b/app/src/main/java/com/example/baseframe/api/CommonObserver.java new file mode 100644 index 0000000..47c602d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/CommonObserver.java @@ -0,0 +1,75 @@ +package com.example.baseframe.api; + +import com.blankj.ALog; + +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.bean.ResultBean; +import com.example.baseframe.bean.ResultBeans; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +import static com.example.baseframe.api.ConfigApi.ERROR_CODE; + +/** + * ViewModel里 使用的CommonObserver + * date : 2019/10/21 + * desc : 请求是否加载等待框 + */ +public abstract class CommonObserver implements Observer { + + private boolean isShowDialog; + private BaseViewModel mViewModel; + + public CommonObserver(BaseViewModel mViewModel, boolean isShowDialog) { + this.mViewModel = mViewModel; + this.isShowDialog = isShowDialog; + } + + public abstract void success(T data); + + public abstract void error(Throwable e); + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + error(e); + mViewModel.dismissDialog(); + ALog.e("onError==="+e.toString()); + } + + @Override + public void onNext(T t) { + success(t); + mViewModel.dismissDialog(); + + if(t instanceof ResultBean){ + ResultBean bean= (ResultBean) t; + if(bean.getErrorCode()==ERROR_CODE){ + ALog.e("请求失败========"+ERROR_CODE+" "+bean.getErrorMsg()); + } + }else if(t instanceof ResultBeans){ + ResultBeans bean= (ResultBeans) t; + if(bean.getErrorCode()==ERROR_CODE){ + ALog.e("请求失败~~~~~~~~"+ERROR_CODE+" "+bean.getErrorMsg()); + } + } + + } + + @Override + public void onSubscribe(Disposable d) { + if (isShowDialog ) { + mViewModel.showDialog(); + } + //调用addSubscribe()添加Disposable,请求与View周期同步 + mViewModel.addDisposable(d); + } + + @Override + public void onComplete() { mViewModel.dismissDialog(); + } + + + +} diff --git a/app/src/main/java/com/example/baseframe/api/ConfigApi.java b/app/src/main/java/com/example/baseframe/api/ConfigApi.java new file mode 100644 index 0000000..5f3332a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/ConfigApi.java @@ -0,0 +1,14 @@ +package com.example.baseframe.api; + +public class ConfigApi { + + /**接口请求失败错误码**/ + public static final int ERROR_CODE=-404; + + public static final String BASE_URL = "https://www.wanandroid.com/";//测试服 + + public static final String TEM_URL = "http://106.52.130.188:8082/app/"; + + /**是否存在emptyView**/ + public static boolean EMPTY_VIEW=false; +} diff --git a/app/src/main/java/com/example/baseframe/api/HeaderInterceptor.java b/app/src/main/java/com/example/baseframe/api/HeaderInterceptor.java new file mode 100644 index 0000000..b52dc81 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/HeaderInterceptor.java @@ -0,0 +1,143 @@ +package com.example.baseframe.api; + +import android.util.Log; + +import com.blankj.ALog; +import com.example.baseframe.BuildConfig; +import com.example.baseframe.utils.AppUtils; +import com.example.baseframe.utils.CheckNetwork; +import com.example.baseframe.utils.DeviceUtils; +import com.example.baseframe.utils.ToastUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; + +public class HeaderInterceptor implements Interceptor { + + /** + * 用于转换 Request 的 body 字符串 + */ + private static final Buffer BUFFER = new Buffer(); + /** + * 用于格式化输出网络请求 + */ + private static final String OUTPUT = "%1$s\n-\n%2$s\n-\n%3$s"; + + private static final String TAG = "HeaderInterceptor"; + + public HeaderInterceptor() { + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + // builder.addHeader("token", SpUtil.getInstance().getString(SpKey.KEY_TOKEN)); + builder.addHeader("App-Version", String.valueOf(AppUtils.getAppVersionCode())); + builder.addHeader("Model", DeviceUtils.getDevice()); + builder.addHeader("Content-Type", "application/json;charset=UTF-8"); + builder.addHeader("Accept", "application/json;versions=1"); + if (CheckNetwork.isNetworkConnected()) { + int maxAge = 60; + builder.addHeader("Cache-Control", "public, max-age=" + maxAge); + } else { + int maxStale = 60 * 60 * 24 * 28; + builder.addHeader("Cache-Control", "public, only-if-cached, max-stale=" + maxStale); + } + + Request request = builder.build(); + long startTime = System.currentTimeMillis(); + Response response = chain.proceed(request); + long endTime = System.currentTimeMillis(); + Response.Builder responseBuilder = response.newBuilder(); + response = responseBuilder.build(); + + //外面已经添加了addInterceptor 日志打印 + // response = printRequestAndResponse(request, response, endTime - startTime); + + if(!CheckNetwork.isNetworkConnected()){ + ToastUtils.showShortSafe("没有网络连接~",2500); + ALog.e("没有网络连接~~~~~~~~" ); + } + return response; + } + + /** + * 将 Request 和 Response 的大致内容输出 + * + * @param request Request + * @param response Response + * @param time 请求时间 + * @return 返回 Response,因为 Response 的 body 只能读取一次,所以读取打印之后需要再塞回去生成新的 Response + */ + private Response printRequestAndResponse(Request request, Response response, long time) { + if (!CheckNetwork.isNetworkConnected()) { + ToastUtils.showShortSafe("没有网络连接~"); + } + + String requestBody = ""; + String responseBody = ""; + try { + if (request.body() != null) { + request.body().writeTo(BUFFER); + requestBody = BUFFER.readUtf8(); + } + ResponseBody body = response.body(); + if (body != null) { + responseBody = unicodeToString(body.string()); + response = response.newBuilder().body(ResponseBody.create(body.contentType(), responseBody)).build(); + } + } catch (IOException e) { + ALog.e(TAG,"is in trycatch"); + if (BuildConfig.DEBUG) { + e.printStackTrace(); + } + } + Log.w(TAG, ">>>>>>> Http Request Start >>>>>>>"); + ALog.w(TAG, String.format( + OUTPUT, + String.valueOf(time) + "ms", + request.url() + "\n" + request.headers().toString() + "\n" + requestBody, + response.headers().toString() + "\n" + responseBody + )); + Log.w(TAG, "<<<<<<< Http Request End <<<<<<<"); + return response; + } + + /** + * Unicode 转成字符串 + * + * @param str 输入 + * @return 返回字符串 + */ + public String unicodeToString(String str) { + Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))"); + Matcher matcher = pattern.matcher(str); + char ch; + while (matcher.find()) { + ch = (char) Integer.parseInt(matcher.group(2), 16); + str = str.replace(matcher.group(1), ch + ""); + } + return str; + } + + private void addHeaders(Request.Builder builder, HashMap headers){ + for (Map.Entry entry : headers.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + ALog.w(TAG, "key="+entry.getKey()+"value="+entry.getValue()); + } + } + + + + + +} diff --git a/app/src/main/java/com/example/baseframe/api/HttpReq.java b/app/src/main/java/com/example/baseframe/api/HttpReq.java new file mode 100644 index 0000000..7a36b4e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/HttpReq.java @@ -0,0 +1,105 @@ +package com.example.baseframe.api; + + +import com.example.baseframe.bean.HomeListBean; +import com.example.baseframe.bean.ResultBean; +import com.example.baseframe.bean.ResultBeans; +import com.example.baseframe.bean.WanAndroidBannerBean; + + +import java.util.HashMap; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import static com.example.baseframe.api.ConfigApi.ERROR_CODE; + + +/** + * @Description: Http请求类 + * 基于原有的 RetrofitFactory.getInstance()请求类 在封装的一层。 + * @Author: yzh + * @CreateDate: 2019/10/28 9:57 + */ +public class HttpReq { + + private static volatile HttpReq mInstance; + public static HttpReq getInstance() { + if (mInstance == null) { + synchronized (HttpReq.class) { + if (mInstance == null) { + mInstance = new HttpReq(); + } + } + } + return mInstance; + } + + /** + * 返回 Observable> 类型 + */ + private Observable> requests(Observable> beanObservable) { + return beanObservable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorReturn(t -> new ResultBean<>(ERROR_CODE, t.getMessage()));//onErrorReturn 请求失败了返回一个错误信息的对象 + } + private Observable> requestss(Observable> beanObservable) { + return beanObservable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorReturn(t -> new ResultBeans<>(ERROR_CODE, t.getMessage())); + } + + private Observable requestT(Observable beanObservable) { + return beanObservable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * 返回 Observable 类型 + */ + private Observable request(Observable beanObservable) { + return beanObservable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorReturn(t -> new ResultBean<>(ERROR_CODE, t.getMessage())); + } + + + +// /** +// * 获取消息列表 +// */ +// public Observable> getMessageList(String pageNo, String pageSize) { +// return RetrofitFactory.getInstance().create(SeniorApi.class) +// .getMessageList(pageNo,pageSize) +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .onErrorReturn(t -> new ResultBean<>(ERROR_CODE, t.getMessage())); +// } + + + /** + * 获取消息列表 + */ +// public Observable> getMessageList(String pageNo, String pageSize) { +// return requests(RetrofitFactory.getInstance().create(SeniorApi.class) +// .getMessageList(pageNo,pageSize) +// ); +// } + + + public Observable> getWanBanner() { + return requestss(RetrofitFactory.getInstance().create(AppApi.class) + .getWanBanner()); + } + + public Observable> getHomeList(int page, int cid ) { + HashMap map=new HashMap<>(); + if(cid!=-1){ + map.put("cid",cid); + } + return requests(RetrofitFactory.getInstance().create(AppApi.class) + .getHomeList(page,map)); + } + +} diff --git a/app/src/main/java/com/example/baseframe/api/RetrofitFactory.java b/app/src/main/java/com/example/baseframe/api/RetrofitFactory.java new file mode 100644 index 0000000..9c11474 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/api/RetrofitFactory.java @@ -0,0 +1,86 @@ +package com.example.baseframe.api; + + +import android.util.Log; + +import com.blankj.ALog; +import com.example.baseframe.utils.CommUtils; + +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; + +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + + +/** + * Retrofit 生产类 + */ +public class RetrofitFactory { + public static final int TIMEOUT = 30; + private static OkHttpClient okHttpClient; + + private static OkHttpClient getOkHttpClient() { + if (okHttpClient == null) { + okHttpClient= new OkHttpClient.Builder() + .connectTimeout(TIMEOUT, TimeUnit.SECONDS)//设置连接超时时间 + .readTimeout(TIMEOUT, TimeUnit.SECONDS)//设置读取超时时间 + .writeTimeout(TIMEOUT, TimeUnit.SECONDS)//设置写入超时时间 + .addInterceptor(new HeaderInterceptor())//加header he token 比如要加保密这些都可以从这里走 + //请求日志拦截打印 + .addInterceptor(new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { + @Override + public void log(String message) { + if(CommUtils.isJson(message)){ + ALog.json(message); + }else{ + Log.w("RetrofitFactory",message); + } + + } + }).setLevel(HttpLoggingInterceptor.Level.BODY)) + // .addInterceptor(new CaptureInfoInterceptor())//com.github.DingProg.NetworkCaptureSelf:library:v1.0.1 抓包工具,可屏蔽 + .build(); + + } + return okHttpClient; + } + /** + * 初始化 + * @return + */ + public static Retrofit getInstance() { + Retrofit mRetrofit = new Retrofit.Builder() + .client(getOkHttpClient()) + // 设置请求的域名 + .baseUrl(ConfigApi.BASE_URL) + // 设置解析转换工厂 + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build(); + return mRetrofit; + } + + + + /** + * 获取微信小程序二维码 + * @return + */ + public static Retrofit getWchatInstance() { + return new Retrofit.Builder() + .baseUrl("https://api.weixin.qq.com/wxa/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build(); + } + + + + + + +} diff --git a/app/src/main/java/com/example/baseframe/base/AppManager.java b/app/src/main/java/com/example/baseframe/base/AppManager.java new file mode 100644 index 0000000..0f8ae8c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/AppManager.java @@ -0,0 +1,201 @@ +package com.example.baseframe.base; + +import android.app.Activity; + + +import androidx.fragment.app.Fragment; + +import java.util.Stack; + +/** + * Created by goldze on 2017/6/15. + * activity堆栈式管理 + */ +public class AppManager { + + private static Stack activityStack; + private static Stack fragmentStack; + private static AppManager instance; + + private AppManager() { + } + + /** + * 单例模式 + * + * @return AppManager + */ + public static AppManager getAppManager() { + if (instance == null) { + instance = new AppManager(); + } + return instance; + } + + public static Stack getActivityStack() { + return activityStack; + } + + public static Stack getFragmentStack() { + return fragmentStack; + } + + + /** + * 添加Activity到堆栈 + */ + public void addActivity(Activity activity) { + if (activityStack == null) { + activityStack = new Stack(); + } + activityStack.add(activity); + } + + /** + * 移除指定的Activity + */ + public void removeActivity(Activity activity) { + if (activity != null) { + activityStack.remove(activity); + } + } + + + /** + * 是否有activity + */ + public boolean isActivity() { + if (activityStack != null) { + return !activityStack.isEmpty(); + } + return false; + } + + /** + * 获取当前Activity(堆栈中最后一个压入的) + */ + public Activity currentActivity() { + Activity activity = activityStack.lastElement(); + return activity; + } + + /** + * 结束当前Activity(堆栈中最后一个压入的) + */ + public void finishActivity() { + Activity activity = activityStack.lastElement(); + finishActivity(activity); + } + + /** + * 结束指定的Activity + */ + public void finishActivity(Activity activity) { + if (activity != null) { + if (!activity.isFinishing()) { + activity.finish(); + } + } + } + + /** + * 结束指定类名的Activity + */ + public void finishActivity(Class cls) { + for (Activity activity : activityStack) { + if (activity.getClass().equals(cls)) { + finishActivity(activity); + break; + } + } + } + + /** + * 结束所有Activity + */ + public void finishAllActivity() { + for (int i = 0, size = activityStack.size(); i < size; i++) { + if (null != activityStack.get(i)) { + finishActivity(activityStack.get(i)); + } + } + activityStack.clear(); + } + + /** + * 获取指定的Activity + * + * @author kymjs + */ + public Activity getActivity(Class cls) { + if (activityStack != null) + for (Activity activity : activityStack) { + if (activity.getClass().equals(cls)) { + return activity; + } + } + return null; + } + + + /** + * 添加Fragment到堆栈 + */ + public void addFragment(Fragment fragment) { + if (fragmentStack == null) { + fragmentStack = new Stack(); + } + fragmentStack.add(fragment); + } + + /** + * 移除指定的Fragment + */ + public void removeFragment(Fragment fragment) { + if (fragment != null) { + fragmentStack.remove(fragment); + } + } + + + /** + * 是否有Fragment + */ + public boolean isFragment() { + if (fragmentStack != null) { + return !fragmentStack.isEmpty(); + } + return false; + } + + /** + * 获取当前Activity(堆栈中最后一个压入的) + */ + public Fragment currentFragment() { + if (fragmentStack != null) { + Fragment fragment = fragmentStack.lastElement(); + return fragment; + } + return null; + } + + + /** + * 退出应用程序 + */ + public void AppExit() { + try { + finishAllActivity(); + // 杀死该应用进程 +// android.os.Process.killProcess(android.os.Process.myPid()); +// 调用 System.exit(n) 实际上等效于调用: +// Runtime.getRuntime().exit(n) +// finish()是Activity的类方法,仅仅针对Activity,当调用finish()时,只是将活动推向后台,并没有立即释放内存,活动的资源并没有被清理;当调用System.exit(0)时,退出当前Activity并释放资源(内存),但是该方法不可以结束整个App如有多个Activty或者有其他组件service等不会结束。 +// 其实android的机制决定了用户无法完全退出应用,当你的application最长时间没有被用过的时候,android自身会决定将application关闭了。 + //System.exit(0); + } catch (Exception e) { + activityStack.clear(); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/base/BaseActivity.java b/app/src/main/java/com/example/baseframe/base/BaseActivity.java new file mode 100644 index 0000000..72329ca --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/BaseActivity.java @@ -0,0 +1,237 @@ +package com.example.baseframe.base; + +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProviders; + +import com.example.baseframe.base.RootActivity; +import com.example.baseframe.utils.ClassUtil; +import com.example.baseframe.weight.LoadDialog; + +import java.util.Map; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * @Author: yzh + * @Date: 2019/12/02 17:09 + * @Use: Mvvm模式Activity基类 + */ +public abstract class BaseActivity extends RootActivity { + public V mBinding; + public VM mViewModel;//如果某个页面很简单不需要单独的ViewModel去展示。VM直接传BaseViewModel即可,mViewModel对象将不会被创建 + private CompositeDisposable mCompositeDisposable; + protected LoadDialog dialog; + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initViewDataBinding(); + //页面间传值 + if (savedInstanceState != null) { + initBundle(savedInstanceState); + } else if (getIntent() != null && getIntent().getExtras() != null) { + initBundle(getIntent().getExtras()); + } + initView(); + } + + /** + * 绑定mViewModel + */ + private void initViewDataBinding() { + //DataBindingUtil类需要在project的build中配置 dataBinding {enabled true }, 同步后会自动关联android.databinding包 + if(getLayoutId()==0){ + return; + } + mBinding = DataBindingUtil.setContentView(this, getLayoutId()); + mViewModel = initMVVMViewModel(); + + if (mViewModel == null) { + createViewModel(); + } + + if (mViewModel != null) { + mBinding.setVariable(initVariableId(), mViewModel); + + registorLiveDataCallBack(); + //页面事件监听的方法 用于ViewModel层转到View层的事件注册 + initViewObservable(); + } + + + } + + private void initBundle(Bundle bundle) { + if (mViewModel != null) { + mViewModel.onBundle(bundle); + } + } + + /** + * 初始化布局的id + * + * @return 布局的id + */ + protected abstract int getLayoutId(); + + /** + * 不使用类名传来的ViewModel,使用临时自定义的ViewModel + * @return 重写次方法返回 + */ + public VM initMVVMViewModel(){ + return null; + } + + /** + * 布局文件里的ViewModel默命名为viewModel(命名为其它请重写方法返回自定义的命名) + */ + public int initVariableId() { + //因为commlib 是无法引用 主app 里的BR(com.example.mvvmapp.BR.viewModel).所以我这里创建activity_binding.xml + // 里命名了一个占位的viewModel以便通过编译期,实际运行时会被替换主app里的BR + return com.example.baseframe.BR.viewModel; + } + + + public abstract void initViewObservable(); + /** + * 初始化view + */ + protected abstract void initView(); + + @Override + protected void onDestroy() { + super.onDestroy(); + //防止空指针异常 + if (mViewModel != null) { +// mViewModel.removeRxBus(); +// mViewModel.onDestroy(); + mViewModel = null; + } + if (mBinding != null) { + mBinding.unbind(); + } + unsubscribe(); + + } + + + /** + * 添加activity里的订阅者 对订阅者统一管理,避免内存泄漏 + * + * @param disposable + */ + public void addRxDisposable(Disposable disposable) { + if (mCompositeDisposable == null) { + mCompositeDisposable = new CompositeDisposable(); + } + mCompositeDisposable.add(disposable); + } + + /** + * 解绑 + */ + public void unsubscribe() { + if (this.mCompositeDisposable != null) { + mCompositeDisposable.dispose(); + this.mCompositeDisposable.clear(); + mCompositeDisposable = null; + } + } + + /** + * 创建ViewModel + */ + private void createViewModel() { + Class viewModelClass = ClassUtil.getViewModel(this); + if (viewModelClass != null) { + this.mViewModel = ViewModelProviders.of(this).get(viewModelClass); + } + } + + + /** + * 请求时的进度条 + * @param cancelAble 是否能取消,true_点击外部和返回时取消loading,false_点外部不能取消loading, + */ + public void showDialog(String msg,final boolean... cancelAble) { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + return; + } + if(TextUtils.isEmpty(msg)){ + msg="Loading..."; + } + try { + if (cancelAble == null || cancelAble.length == 0) { + dialog = new LoadDialog(mContext, msg, false);//默认不可关闭 + } else { + dialog = new LoadDialog(mContext,msg, cancelAble[0]); + } + dialog.show(); + } catch (Exception e) { + // Log.e(TAG, e.toString()); + } + } + + public void dismissDialog(){ + if (dialog != null) { + try { + dialog.dismiss(); + dialog = null; + } catch (Exception ignored) {} + } + } + + + /** + * 注册(初始化)ViewModel与View的UI回调事件 + */ + protected void registorLiveDataCallBack() { + //加载对话框显示 + mViewModel.getUILiveData().getShowDialogEvent().observe(this,s -> showDialog(s)); + //加载对话框消失 + mViewModel.getUILiveData().getDismissDialogEvent().observe(this,aVoid ->dismissDialog()); + //跳入新页面 + mViewModel.getUILiveData().getStartActivityEvent().observe(this, new Observer>() { + @Override + public void onChanged(@Nullable Map params) { + Class clz = (Class) params.get(BaseViewModel.ParameterType.CLASS); + Bundle bundle = (Bundle) params.get(BaseViewModel.ParameterType.BUNDLE); + startActivity(clz, bundle); + } + }); + + //跳入新页面(共享元素动画) + mViewModel.getUILiveData().getStartActivityAnimationEvent().observe(this,stringObjectMap -> { + Class clz = (Class) stringObjectMap.get(BaseViewModel.ParameterType.CLASS); + Bundle bundle = (Bundle) stringObjectMap.get(BaseViewModel.ParameterType.BUNDLE); + View v= (View) stringObjectMap.get(BaseViewModel.ParameterType.VIEW); + String name= (String) stringObjectMap.get(BaseViewModel.ParameterType.VIEW_NAME); + startActivityAnimation(clz,v,name,bundle); + }); + + //finish界面 + mViewModel.getUILiveData().getFinishEvent().observe(this,aVoid ->finish()); + //关闭上一层 + mViewModel.getUILiveData().getOnBackPressedEvent().observe(this, aVoid ->onBackPressed()); + + //跳转一个共用显示fragment页面 + mViewModel.getUILiveData().getStartContainerActivityEvent().observe(this,params -> { + String canonicalName = (String) params.get(BaseViewModel.ParameterType.FARGMENT_NAME); + Bundle bundle = (Bundle) params.get(BaseViewModel.ParameterType.BUNDLE); + startContainerActivity(canonicalName, bundle); + }); + } + + + + +} diff --git a/app/src/main/java/com/example/baseframe/base/BaseFragment.java b/app/src/main/java/com/example/baseframe/base/BaseFragment.java new file mode 100644 index 0000000..49ab4aa --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/BaseFragment.java @@ -0,0 +1,587 @@ +package com.example.baseframe.base; + +import android.app.ActivityOptions; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProviders; + +import com.example.baseframe.listener.Listener; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.weight.LoadDialog; +import com.trello.rxlifecycle3.components.support.RxFragment; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +import static android.app.Activity.RESULT_OK; + + +public abstract class BaseFragment extends RxFragment { + public V mBinding; + public VM mViewModel; + private CompositeDisposable mCompositeDisposable; + protected LoadDialog dialog; + /** + * Fragment是否可见状态 + */ + private boolean isFragmentVisible = false; + /** + * 标志位,View是否已经初始化完成。 + */ + private boolean isPrepared = false; + /** + * 是否第一次加载 + */ + private boolean isFirstLoad = true; + + + /** + *
+     * 忽略isFirstLoad的值,强制刷新数据,但仍要Visible & Prepared
+     * 一般用于PagerAdapter需要刷新各个子Fragment的场景
+     * 不要new 新的 PagerAdapter 而采取reset数据的方式
+     * 所以要求Fragment重新走initData方法
+     * 故使用 {@link BaseFragment#setForceLoad(boolean)}来让Fragment下次执行initData
+     * 
+ */ + private boolean forceLoad = false; + + + protected BaseActivity mActivity; + private void initBundle(Bundle bundle) { + if (mViewModel != null) { + mViewModel.onBundle(bundle); + } + } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mActivity= (BaseActivity) getActivity(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View view=inflater.inflate(getLayoutId(inflater, container), container, false); + + initViewDataBinding(view); + //页面间传值 + if (savedInstanceState != null) { + initBundle(savedInstanceState); + } else if (getArguments() != null) { + initBundle(getArguments()); + } + + // 若 viewpager 不设置 setOffscreenPageLimit 或设置数量不够 + // 销毁的Fragment onCreateView 每次都会执行(但实体类没有从内存销毁) + isFirstLoad = true; + loadData(); + initView(); + return view; + + } + /** + * 绑定mViewModel + */ + private void initViewDataBinding(View view) { + if(view==null){ + return; + } + mBinding= DataBindingUtil.bind(view); + mViewModel = initViewModel(); + if (mViewModel == null) { + Class modelClass; + Type type = getClass().getGenericSuperclass(); + if (type instanceof ParameterizedType) { + modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1]; + } else { + //如果没有指定泛型参数,则默认使用BaseViewModel + modelClass = BaseViewModel.class; + } + mViewModel = (VM) createViewModel(this, modelClass); + } + + if (mViewModel != null) { + mBinding.setVariable(initVariableId(), mViewModel); + } + + //页面事件监听的方法 用于ViewModel层转到View层的事件注册 + initViewObservable(); + registorLiveDataCallBack(); + } + + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + //界面初始化完成 + isPrepared = true; + loadData(); + } + + /** + * 如果是与ViewPager一起使用,调用的是setUserVisibleHint + *

+ * 这个方法执行的时候onCreateView并不一定执行(切记) + * + * @param isVisibleToUser 是否显示出来了 + */ + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (getUserVisibleHint()) { + onVisible(); + } else { + onInvisible(); + } + } + + /** + * 如果是通过FragmentTransaction的show和hide的方法来控制显示,调用的是onHiddenChanged. + * 若是初始就show的Fragment 为了触发该事件 需要先hide再show + * + * @param hidden hidden True if the fragment is now hidden, false if it is not + * visible. + */ + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + if (!hidden) { + onVisible(); + } else { + onInvisible(); + } + } + + public void onInvisible() { + isFragmentVisible = false; + } + + /** + * 当界面可见的时候执行 + */ + public void onVisible() { + isFragmentVisible = true; + loadData(); + + } + + + /** + * 这里执行懒加载的逻辑 + * 只会执行一次(如果不想只执行一次此方法): {@link BaseFragment#setForceLoad(boolean)} + */ + public void lazyLoad() { + Log.d("BaseMVVMFragment","BaseMVVMFragment: lazyLoad"); + } + + private void loadData() { + //判断View是否已经初始化完成 并且 fragment是可见 并且是第一次加载 + if (isPrepared && isFragmentVisible && isFirstLoad) { + if (forceLoad || isFirstLoad) { + forceLoad = false; + isFirstLoad = false; + lazyLoad(); + } + } + } + + /** + * @param forceLoad 设置为true lazyLoad()方法会执行多次 否则只会执行一次 + */ + public void setForceLoad(boolean forceLoad) { + this.forceLoad = forceLoad; + } + + + @Override + public void onDestroy() { + super.onDestroy(); + //防止空指针异常 + if (mViewModel != null) { +// mViewModel.removeRxBus(); +// mViewModel.onDestroy(); + mViewModel = null; + } + if (mBinding != null) { + mBinding.unbind(); + } + isPrepared = false; + unsubscribe(); + } + + /** + * 初始化布局的id + * + * @return 布局的id + */ + protected abstract int getLayoutId(LayoutInflater inflater, @Nullable ViewGroup container); + + /** + * 初始化ViewModel + * + * @return 继承BaseViewModel的ViewModel + */ + public VM initViewModel(){ + return null; + } + /** + * 初始化ViewModel的id + * + * @return BR的id + */ + // protected abstract int initVariableId(); + + /** + * 布局文件里的ViewModel默命名为viewModel(命名为其它重写方法返回自定义的命名) + */ + public int initVariableId() { + return com.example.baseframe.BR.viewModel; + } + + + public abstract void initViewObservable(); + /** + * 初始化view + */ + protected abstract void initView(); + + /** + * 创建ViewModel + */ + public T createViewModel(Fragment fragment, Class cls) { + return ViewModelProviders.of(fragment).get(cls); + } + + + /** + * 添加activity里的订阅者 对订阅者统一管理,避免内存泄漏 + * + * @param disposable + */ + public void addRxDisposable(Disposable disposable) { + if (mCompositeDisposable == null) { + mCompositeDisposable = new CompositeDisposable(); + } + mCompositeDisposable.add(disposable); + } + + /** + * 解绑 + */ + public void unsubscribe() { + if (this.mCompositeDisposable != null) { + mCompositeDisposable.dispose(); + this.mCompositeDisposable.clear(); + mCompositeDisposable = null; + } + } + + + + /** + * 注册(初始化)ViewModel与View的UI回调事件 + */ + protected void registorLiveDataCallBack() { + //加载对话框显示 + mViewModel.getUILiveData().getShowDialogEvent().observe(this,s -> showDialog(s)); + //加载对话框消失 + mViewModel.getUILiveData().getDismissDialogEvent().observe(this,aVoid ->dismissDialog()); + //跳入新页面 + mViewModel.getUILiveData().getStartActivityEvent().observe(this, new Observer>() { + @Override + public void onChanged(@Nullable Map params) { + Class clz = (Class) params.get(BaseViewModel.ParameterType.CLASS); + Bundle bundle = (Bundle) params.get(BaseViewModel.ParameterType.BUNDLE); + startActivity(clz, bundle); + } + }); + + //finish界面 + mViewModel.getUILiveData().getFinishEvent().observe(this,aVoid ->mActivity.finish()); + //关闭上一层 + mViewModel.getUILiveData().getOnBackPressedEvent().observe(this, aVoid ->mActivity.onBackPressed()); + + //跳转一个共用显示fragment页面 + mViewModel.getUILiveData().getStartContainerActivityEvent().observe(this,params -> { + String canonicalName = (String) params.get(BaseViewModel.ParameterType.FARGMENT_NAME); + Bundle bundle = (Bundle) params.get(BaseViewModel.ParameterType.BUNDLE); + startContainerActivity(canonicalName, bundle); + }); + } + + + + + /** + * 请求时的进度条 + * @param cancelAble 是否能取消,true_点击外部和返回时取消loading,false_点外部不能取消loading, + */ + public void showDialog(String msg,final boolean... cancelAble) { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + return; + } + if(TextUtils.isEmpty(msg)){ + msg="Loading..."; + } + try { + if (cancelAble == null || cancelAble.length == 0) { + dialog = new LoadDialog(mActivity, msg, false);//默认不可关闭 + } else { + dialog = new LoadDialog(mActivity,msg, cancelAble[0]); + } + dialog.show(); + } catch (Exception e) { + // Log.e(TAG, e.toString()); + } + } + + public void dismissDialog(){ + if (dialog != null) { + try { + dialog.dismiss(); + dialog = null; + } catch (Exception ignored) {} + } + } + + + + + //************************************** Activity跳转(兼容4.4) **************************************// + + /** + * Activity跳转 + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivity(Class clz) { + startActivity(new Intent(getActivity(), clz)); + } + + + /** + * Activity携带数据的跳转 + * + * @param clz 要跳转的Activity的类名 + * @param bundle bundle + */ + public void startActivity(Class clz, Bundle bundle) { + Intent intent = new Intent(getActivity(), clz); + if (bundle != null) { + intent.putExtras(bundle); + } + startActivity(intent); + } + + /** + * 跳转公共的一个ContainerActivity 用来显示Fragment + * + * @param canonicalName 通过 Fragment.class.getCanonicalName()获取 + */ + public void startContainerActivity(String canonicalName) { + startContainerActivity(canonicalName, null); + } + /** + * 跳转容器页面 + * + * @param canonicalName 通过 Fragment.class.getCanonicalName()获取 + * @param bundle + */ + public void startContainerActivity(String canonicalName, Bundle bundle) { + Intent intent = new Intent(mActivity, ContainerActivity.class); + intent.putExtra(ContainerActivity.FRAGMENT, canonicalName); + if (bundle != null) { + intent.putExtra(ContainerActivity.BUNDLE, bundle); + } + startActivity(intent); + } + + + /** + * Activity跳转(带动画) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(new Intent(getActivity(), clz), ActivityOptions.makeSceneTransitionAnimation(getActivity()).toBundle()); + } else { + startActivity(clz); + } + } + + /** + * Activity跳转(共享元素动画) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(new Intent(getActivity(), clz), ActivityOptions.makeSceneTransitionAnimation(getActivity(), view, shareView).toBundle()); + } else { + startActivity(clz); + } + } + + /** + * Activity跳转(共享元素动画,带Bundle数据) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareView, Bundle bundle) { + Intent intent = new Intent(getActivity(), clz); + if (bundle != null) { + intent.putExtras(bundle); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(getActivity(), view, shareView).toBundle()); + } else { + startActivity(intent); + } + } + + /** + * Activity跳转(带动画,带Bundle数据) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, Bundle bundle) { + Intent intent = new Intent(getActivity(), clz); + if (bundle != null) { + intent.putExtras(bundle); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(getActivity()).toBundle()); + } else { + startActivity(intent); + } + } + + + /** + * 通过Class打开编辑界面 + * + * @param cls + * @param requestCode + */ + public void startActivityForResult(Class cls, int requestCode) { + startActivityForResult(new Intent(getActivity(), cls), requestCode); + } + + /** + * 含有Bundle通过Class打开编辑界面 + * + * @param cls + * @param bundle + * @param requestCode + */ + public void startActivityForResult(Class cls, Bundle bundle, + int requestCode) { + Intent intent = new Intent(); + + intent.setClass(getActivity(), cls); + if (bundle != null) { + intent.putExtras(bundle); + } + startActivityForResult(intent, requestCode); + } + + /** + * 有动画的Finish掉界面 + */ + public void AnimationFinish() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getActivity().finishAfterTransition(); + } else { + getActivity().finish(); + } + } + + //************************************** Activity跳转 **************************************// + public int getColors(int colorId) { + return ContextCompat.getColor(mActivity, colorId); + } + + + + /** + * 8.0需要校验安装未知源权限 + */ + public void canInstallAPK(Listener listener){ + boolean hasInstallPerssion = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + hasInstallPerssion = mActivity.getPackageManager().canRequestPackageInstalls(); + } + if (hasInstallPerssion) { + //去下载安装应用 + listener.onResult(); + } else { + //跳转至“安装未知应用”权限界面,引导用户开启权限,可以在onActivityResult中接收权限的开启结果 + this.listener=listener; + showDialogBysure("应用安装","更新app需要您开启安装权限",() -> { + Uri packageURI = Uri.parse("package:"+mActivity.getPackageName()); + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageURI); + startActivityForResult(intent, 0x33); + }).setCancelable(true); + } + } + + Listener listener; + //接收“安装未知应用”权限的开启结果 + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if(requestCode==0x33){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O&&resultCode == RESULT_OK) { + listener.onResult(); + listener=null; + } + } + + } + /** + * 只有确定按钮的简化弹窗 + * @param title + * @param msg + * @param listener + * @return + */ + public Dialog showDialogBysure(String title, String msg, Listener listener){ + return CommUtils.showDialog(mActivity,title,msg,"确定" + ,null, listener,null); + } + +} diff --git a/app/src/main/java/com/example/baseframe/base/BaseMvvmRecyclerAdapter.java b/app/src/main/java/com/example/baseframe/base/BaseMvvmRecyclerAdapter.java new file mode 100644 index 0000000..c6b2835 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/BaseMvvmRecyclerAdapter.java @@ -0,0 +1,144 @@ +package com.example.baseframe.base; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ObservableArrayList; +import androidx.databinding.ObservableList; +import androidx.databinding.ViewDataBinding; +import androidx.recyclerview.widget.RecyclerView; + +import com.blankj.ALog; +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.BaseViewHolder; +import com.example.baseframe.R; +import com.example.baseframe.api.ConfigApi; + +import java.util.List; + +/** + * 基于BaseQuickAdapter 实现MVVM模式的BaseMvvmRecyclerAdapter + * @anthor yzh + * @time 2019/11/23 11:17 + */ +public abstract class BaseMvvmRecyclerAdapter extends BaseQuickAdapter { + private ObservableList mTObservableList;//让list数据变更后自动notifyItemRangeChanged刷新 + + public RecyclerView recyclerView; + + //在RecyclerView提供数据的时候调用 + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + this.recyclerView = recyclerView; + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + this.recyclerView = null; + } + + + protected BaseMvvmRecyclerAdapter(@LayoutRes int layoutResId, @Nullable List data) { + super(layoutResId, data); + this.mTObservableList = data == null ? new ObservableArrayList() : (ObservableList) data; + if (layoutResId != 0) { + this.mLayoutResId = layoutResId; + } + mTObservableList.addOnListChangedCallback(new ObservableList.OnListChangedCallback>() { + @Override + public void onChanged(ObservableList ts) { + notifyDataSetChanged(); + ALog.e("onChanged()"); + } + @Override + public void onItemRangeChanged(ObservableList ts, int positionStart, int itemCount) { + notifyItemRangeChanged(positionStart, itemCount); + ALog.e("onItemRangeChanged()"); + } + + @Override + public void onItemRangeInserted(ObservableList ts, int positionStart, int itemCount) { + ALog.e("onItemRangeInserted() "+getEmptyViewCount() +ConfigApi.EMPTY_VIEW); + + //踩坑提示:使用 quickadapter.setEmptyView 设置空布局后, 刷新又有了数据 必须调用mAdapter.setNewData(mList); 而不是调用notifyDataSetChanged()系列; 否则会报错 + +// if(getEmptyViewCount()>0){ 不能用这个在这里判断,因为mTObservableList有值后getEmptyViewCount 会变成0 + if(ConfigApi.EMPTY_VIEW){ + setNewData(ts); + }else{ + notifyItemRangeInserted(positionStart, itemCount); + } + + } + + @Override + public void onItemRangeMoved(ObservableList ts, int fromPosition, int toPosition, int itemCount) { + for (int i = 0; i < itemCount; i++) { + notifyItemMoved(fromPosition + i, toPosition + i); + } + ALog.e("onItemRangeMoved()"); + } + @Override + public void onItemRangeRemoved(ObservableList ts, int positionStart, int itemCount) { + notifyItemRangeRemoved(positionStart, itemCount); + ALog.e("onItemRangeRemoved()"); + } + }); + } + + // @Override +// public BaseMvvmRecyclerHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { +// super.onCreateViewHolder(parent,viewType); +// ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutResId, parent, false); +// return BaseMvvmRecyclerHolder.getRecyclerHolder(binding,binding.getRoot()); +// } + @Override + protected View getItemView(int layoutResId, ViewGroup parent) { + ViewDataBinding binding = DataBindingUtil.inflate(mLayoutInflater, layoutResId, parent, false); + if (binding == null) { + return super.getItemView(layoutResId, parent); + } + View view = binding.getRoot(); + view.setTag(R.id.BaseQuickAdapter_databinding_support, binding); + return view; + } + + @Override + protected void convert(@NonNull BaseMvvmRecyclerAdapter.BindingViewHolder helper, T item) { + ViewDataBinding binding = helper.getBinding(); + // 建议item.xml里的bean的别名都取itemBean,自定义命名的话,构造函数又要增加一个别名参数(variableId) + binding.setVariable(com.example.baseframe.BR.itemBean, item); + binding.executePendingBindings(); + + //BaseQuickAdapter position获取 + int position = helper.getAdapterPosition(); + if (position == RecyclerView.NO_POSITION) { + return; + } + position -= this.getHeaderLayoutCount(); + + convert(helper,item,position); + } + + /** + * 填充RecyclerView适配器的方法 + */ + public abstract void convert(BaseMvvmRecyclerAdapter.BindingViewHolder holder, T item,int position); + + + public static class BindingViewHolder extends BaseViewHolder { + public BindingViewHolder(View view) { + super(view); + } + public ViewDataBinding getBinding() { + return (ViewDataBinding) itemView.getTag(R.id.BaseQuickAdapter_databinding_support); + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/base/BaseViewModel.java b/app/src/main/java/com/example/baseframe/base/BaseViewModel.java new file mode 100644 index 0000000..3473216 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/BaseViewModel.java @@ -0,0 +1,242 @@ +package com.example.baseframe.base; + +import android.app.Application; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableArrayList; +import androidx.databinding.ObservableInt; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; + + +import com.example.baseframe.bus.event.SingleLiveEvent; + +import java.util.HashMap; +import java.util.Map; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +import static com.example.baseframe.utils.CommUtils.isListNotNull; + + +/** + * BaseViewModel只使用 LiveData 方式去刷新数据 + * 也有其实的实现方式(但不太建议) + * + * anthor yzh time 2019/11/27 10:07 + */ +public abstract class BaseViewModel extends AndroidViewModel { + // public int mPage = 1;//列表分页使用默认1开始 + public ObservableInt mPage= new ObservableInt(1); + private CompositeDisposable mCompositeDisposable; + private UILiveData mUILiveData; + +// void initBundle(Bundle bundle) { +// onCreate(bundle); +// } + + public abstract void onBundle(Bundle bundle); + + public BaseViewModel(@NonNull Application application) { + super(application); + } + + //避免Rxjava内存泄漏, + //1、可以将Rxjava 订阅的时间添至CompositeDisposable进来,Activity销毁时切断订阅 + //2、也可以用 RxLifeCycle 将Rxjava绑定Acitivty/Fragment,销毁时自动取消订阅 + + public void addDisposable(Disposable disposable) { + if (this.mCompositeDisposable == null) { + this.mCompositeDisposable = new CompositeDisposable(); + } + this.mCompositeDisposable.add(disposable); + } + + @Override + protected void onCleared() {//此方法会在Activity/Fragment销毁时调用,可以在这里做一些额外释放资源的操作。 + super.onCleared(); + if (this.mCompositeDisposable != null && !mCompositeDisposable.isDisposed()) { + this.mCompositeDisposable.clear(); + } + } + + + public void showDialog() { + getUILiveData().getShowDialogEvent().postValue(null); + } + public void showDialog(String title) { + getUILiveData().getShowDialogEvent().postValue(title); + } + + public void dismissDialog() { + getUILiveData().getDismissDialogEvent().call(); + } + + /** + * 跳转页面 + * @param clz 所跳转的目的Activity类 + */ + public void startActivity(Class clz) { + startActivity(clz, null); + } + + /** + * 跳转页面 + * @param clz 所跳转的目的Activity类 + */ + public void startActivity(Class clz, Bundle bundle) { + Map params = new HashMap<>(); + params.put(ParameterType.CLASS, clz); + if (bundle != null) { + params.put(ParameterType.BUNDLE, bundle); + } + getUILiveData().startActivityEvent.postValue(params); + } + + /** + * Activity跳转(共享元素动画) + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareView) { + startActivityAnimation(clz,view,shareView,null); + } + + /** + * Activity跳转(共享元素动画,带Bundle数据) + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareName, Bundle bundle) { + + Map params = new HashMap<>(); + params.put(ParameterType.CLASS, clz); + params.put(ParameterType.VIEW, view); + params.put(ParameterType.VIEW_NAME, shareName); + if (bundle != null) { + params.put(ParameterType.BUNDLE, bundle); + } + getUILiveData().startActivityAnimationEvent.postValue(params); + } + + + /** + * 跳转显示一个fragment的公共页面 + * @param canonicalName 规范名 : Fragment.class.getCanonicalName() + */ + public void startContainerActivity(String canonicalName) { + startContainerActivity(canonicalName, null); + } + + /** + *跳转显示一个fragment的公共页面 + * @param canonicalName 规范名 : Fragment.class.getCanonicalName() + */ + protected void startContainerActivity(String canonicalName, Bundle bundle) { + //ALog.i("canonicalName---"+canonicalName); + Map params = new HashMap<>(); + params.put(ParameterType.FARGMENT_NAME, canonicalName); + if (bundle != null) { + params.put(ParameterType.BUNDLE, bundle); + } + getUILiveData().startContainerActivityEvent.postValue(params); + } + + + public UILiveData getUILiveData() { + if (mUILiveData == null) { + mUILiveData = new UILiveData(); + } + return mUILiveData; + } + + /** + * UILiveData 的作用 放一些常用的事件,减少去new 重复的SingleLiveEvent() + */ + public final class UILiveData extends SingleLiveEvent { + private SingleLiveEvent showDialogEvent; + private SingleLiveEvent dismissDialogEvent; + private SingleLiveEvent> startActivityEvent; + private SingleLiveEvent> startActivityAnimationEvent; + private SingleLiveEvent finishEvent; + private SingleLiveEvent onBackPressedEvent; + private SingleLiveEvent> startContainerActivityEvent; + private SingleLiveEvent commEvent; + + + //普通通用的一般回调事件 + public SingleLiveEvent getCommEvent() { + return commEvent = createLiveData(commEvent); + } + + + public SingleLiveEvent getShowDialogEvent() { + return showDialogEvent = createLiveData(showDialogEvent); + } + + public SingleLiveEvent getDismissDialogEvent() { + return dismissDialogEvent = createLiveData(dismissDialogEvent); + } + + //Activity跳转事件 + public SingleLiveEvent> getStartActivityEvent() { + return startActivityEvent = createLiveData(startActivityEvent); + } + + //Activity跳转(共享元素动画,带Bundle数据)事件 + public SingleLiveEvent> getStartActivityAnimationEvent() { + return startActivityAnimationEvent = createLiveData(startActivityAnimationEvent); + } + + public SingleLiveEvent getFinishEvent() { + return finishEvent = createLiveData(finishEvent); + } + + public SingleLiveEvent getOnBackPressedEvent() { + return onBackPressedEvent = createLiveData(onBackPressedEvent); + } + public SingleLiveEvent> getStartContainerActivityEvent() { + return startContainerActivityEvent = createLiveData(startContainerActivityEvent); + } + private SingleLiveEvent createLiveData(SingleLiveEvent liveData) { + if (liveData == null) { + liveData = new SingleLiveEvent(); + } + return liveData; + } + + @Override + public void observe(LifecycleOwner owner, Observer observer) { + super.observe(owner, observer); + } + } + + public static final class ParameterType { + public static String CLASS = "CLASS"; + public static String BUNDLE = "BUNDLE"; + public static String FARGMENT_NAME = "FARGMENT_NAME"; + //Activity跳转共享元素动画 + public static String VIEW = "VIEW"; + public static String VIEW_NAME = "VIEW_NAME"; + } + + + /** + * 请求成功后,设置下一次请求的分页 + * @param isRefresh 是否是下拉刷新 + */ + public void setPage(ObservableArrayList mList, boolean isRefresh){ + if(isListNotNull(mList)){ + if(isRefresh){ + //mPage=2; + mPage.set(2); + }else{ + mPage.set(mPage.get()+1); + // mPage++; + } + } + // ALog.i("下一次请求的分页数:"+mPage.get()); + } +} diff --git a/app/src/main/java/com/example/baseframe/base/ContainerActivity.java b/app/src/main/java/com/example/baseframe/base/ContainerActivity.java new file mode 100644 index 0000000..d07744a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/ContainerActivity.java @@ -0,0 +1,101 @@ +package com.example.baseframe.base; + +import android.content.Intent; +import android.os.Bundle; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.example.baseframe.R; + +import java.lang.ref.WeakReference; + + +/** + * 一个公用来显示 Fragment的Activity + * 一些普通界面只需要编写Fragment,使用此Activity显示,这样就不需要每个界面都在AndroidManifest中注册一遍 + */ +public class ContainerActivity extends BaseActivity { + private static final String FRAGMENT_TAG = "content_fragment_tag"; + public static final String FRAGMENT = "fragment"; + public static final String BUNDLE = "bundle"; + protected WeakReference mFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_container); + FragmentManager fm = getSupportFragmentManager(); + Fragment fragment = null; + if (savedInstanceState != null) { + fragment = fm.getFragment(savedInstanceState, FRAGMENT_TAG); + } + if (fragment == null) { + fragment = initFromIntent(getIntent()); + } + FragmentTransaction trans = getSupportFragmentManager() + .beginTransaction(); + trans.replace(R.id.contentLayout, fragment); + trans.commitAllowingStateLoss(); + mFragment = new WeakReference<>(fragment); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + getSupportFragmentManager().putFragment(outState, FRAGMENT_TAG, mFragment.get()); + } + + protected Fragment initFromIntent(Intent data) { + if (data == null) { + throw new RuntimeException( + "you must provide a page info to display"); + } + try { + String fragmentName = data.getStringExtra(FRAGMENT); + if (fragmentName == null || "".equals(fragmentName)) { + throw new IllegalArgumentException("can not find page fragmentName"); + } + Class fragmentClass = Class.forName(fragmentName); + Fragment fragment = (Fragment) fragmentClass.newInstance(); + Bundle args = data.getBundleExtra(BUNDLE); + if (args != null) { + fragment.setArguments(args); + } + return fragment; + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + } + throw new RuntimeException("fragment initialization failed!!"); + } + + @Override + public void onBackPressed() { +/* Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.content); + if (fragment instanceof BaseFragment) { + }*/ + super.onBackPressed(); + } + + + + @Override + protected int getLayoutId() { + return 0; + } + + @Override + public void initViewObservable() { + + } + + @Override + protected void initView() { + + } + +} diff --git a/app/src/main/java/com/example/baseframe/base/RootActivity.java b/app/src/main/java/com/example/baseframe/base/RootActivity.java new file mode 100644 index 0000000..0825db5 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/base/RootActivity.java @@ -0,0 +1,434 @@ +package com.example.baseframe.base; + +import android.app.ActivityOptions; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.api.ConfigApi; +import com.example.baseframe.listener.Listener; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.ToastUtils; +import com.example.baseframe.utils.permission.PermissionsUtils; +import com.scwang.smartrefresh.layout.SmartRefreshLayout; +import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; + +import java.util.List; + +import static com.example.baseframe.utils.ButtonUtils.isFastDoubleClick; +import static com.example.baseframe.utils.CommUtils.isListNull; + + +/** + * @Description: 最終的根acitvity + * @Author: yzh + * @CreateDate: 2019/11/6 12:01 + */ +public class RootActivity extends RxAppCompatActivity { + + /** + * 当statusbar颜色需要为白色的时候,需要设置,默认不是白色 + * true:白色 false:其他颜色 + */ + protected boolean isStatusBarWhite; + + protected RxAppCompatActivity mContext; + private final int ACCESS_FINE_LOCATION_CODE = 2;//申请获得定位权限 + private final int ACTION_LOCATION_SOURCE_SETTINGS_CODE = 3; //打开GPS设置界面 + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // translateStatueBar(); + Configuration mConfiguration = this.getResources().getConfiguration(); //获取设置的配置信息 + int orientation = mConfiguration.orientation; //获取屏幕方向 + if (orientation == mConfiguration.ORIENTATION_PORTRAIT) { + // DensityUtil.setDensity(getApplication(), this,600); 根据自己项目设计图去修改值--我这里先屏蔽 + }else { + // DensityUtil.setDensity(getApplication(), this,960); + } + mContext = this; + + + } + + + /** + * 查看是否打开GPS 跳转设置GPS + */ + public void isOpenGPS() { + LocationManager locationManager = (LocationManager) this + .getSystemService(Context.LOCATION_SERVICE); + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + // 转到手机设置界面,用户设置GPS + Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + startActivityForResult(intent, ACTION_LOCATION_SOURCE_SETTINGS_CODE); // 设置完成后返回到原来的界面 + } + } + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionsUtils.getInstance().onRequestPermissionsResult(this, requestCode, permissions, grantResults); + switch (requestCode) { + case ACCESS_FINE_LOCATION_CODE: + //定位权限 加设置GPS + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + //申请权限可获得位置权限成功了 + isOpenGPS(); + } else { + ToastUtils.showShort("申请失败,可能导致定位不准确"); + } + break; + } + } + + + + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + //设置相应的 设计图 dp 比率 + if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){ + //"横屏" + // DensityUtil.setDensity(getApplication(), this,960); + }else{ + // "竖屏" + // DensityUtil.setDensity(getApplication(), this,600); + } + } + + + + + + + //************************************** Activity跳转(兼容4.4) **************************************// + + + /** + * Activity跳转 + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivity(Class clz) { + if(isFastDoubleClick()){ + return; + } + startActivity(new Intent(this, clz)); + } + + + /** + * Activity携带数据的跳转 + * + * @param clz 要跳转的Activity的类名 + * @param bundle bundle + */ + public void startActivity(Class clz, Bundle bundle) { + if(isFastDoubleClick()){ + return; + } + Intent intent = new Intent(this, clz); + if (bundle != null) { + intent.putExtras(bundle); + } + startActivity(intent); + } + + /** + * Activity跳转(带动画) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz) { + if(isFastDoubleClick()){ + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(new Intent(this, clz), ActivityOptions.makeSceneTransitionAnimation(this).toBundle()); + } else { + startActivity(clz); + } + } + + /** + * Activity跳转(共享元素动画) + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareView) { + startActivityAnimation(clz,view,shareView,null); + } + + /** + * Activity跳转(共享元素动画,带Bundle数据) + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, View view, String shareView, Bundle bundle) { + if(isFastDoubleClick()){ + return; + } + Intent intent = new Intent(this, clz); + if (bundle != null) { + intent.putExtras(bundle); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, view, shareView).toBundle()); + } else { + startActivity(intent); + } + } + + /** + * Activity跳转(带动画,带Bundle数据) + * + * @param clz 要跳转的Activity的类名 + */ + public void startActivityAnimation(Class clz, Bundle bundle) { + if(isFastDoubleClick()){ + return; + } + Intent intent = new Intent(this, clz); + if (bundle != null) { + intent.putExtras(bundle); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()); + } else { + startActivity(intent); + } + } + + + /** + * 通过Class打开编辑界面 + * + * @param cls + * @param requestCode + */ + public void startActivityForResult(Class cls, int requestCode) { + if(isFastDoubleClick()){ + return; + } + startActivityForResult(new Intent(this, cls), requestCode); + } + + /** + * 含有Bundle通过Class打开编辑界面 + * + * @param cls + * @param bundle + * @param requestCode + */ + public void startActivityForResult(Class cls, Bundle bundle,int requestCode) { + if(isFastDoubleClick()){ + return; + } + Intent intent = new Intent(); + intent.setClass(this, cls); + if (bundle != null) { + intent.putExtras(bundle); + } + startActivityForResult(intent, requestCode); + } + + /** + * 有动画的Finish掉界面 + */ + public void AnimationFinish() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + finishAfterTransition(); + } else { + finish(); + } + } + + + /** + * 跳转公共的一个ContainerActivity 用来显示Fragment + * + * @param canonicalName 通过 Fragment.class.getCanonicalName()获取 + */ + public void startContainerActivity(String canonicalName) { + startContainerActivity(canonicalName, null); + } + /** + * 跳转容器页面 + * + * @param canonicalName 通过 Fragment.class.getCanonicalName()获取 + * @param bundle + */ + public void startContainerActivity(String canonicalName, Bundle bundle) { + Intent intent = new Intent(this, ContainerActivity.class); + intent.putExtra(ContainerActivity.FRAGMENT, canonicalName); + if (bundle != null) { + intent.putExtra(ContainerActivity.BUNDLE, bundle); + } + startActivity(intent); + } + + + //************************************** Activity跳转 **************************************// + + + /** + * 8.0需要校验安装未知源权限 + */ + public void canInstallAPK(Listener listener){ + boolean hasInstallPerssion = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + hasInstallPerssion = getPackageManager().canRequestPackageInstalls(); + } + if (hasInstallPerssion) { + //去下载安装应用 + listener.onResult(); + } else { + //跳转至“安装未知应用”权限界面,引导用户开启权限,可以在onActivityResult中接收权限的开启结果 + this.listener=listener; + showDialogBysure("应用安装","更新app需要您开启安装权限",() -> { + Uri packageURI = Uri.parse("package:"+getPackageName()); + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageURI); + startActivityForResult(intent, 0x33); + }).setCancelable(true); + } + } + + Listener listener; + //接收“安装未知应用”权限的开启结果 + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if(requestCode==0x33){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O&&resultCode == RESULT_OK) { + listener.onResult(); + listener=null; + } + } + + } + + + /** + * 为textview 设值,避免空值情况 + * + * @param tv + * @param str + */ + public void setTextValues(TextView tv, String str) { + if (tv != null && !TextUtils.isEmpty(str)) { + tv.setText(str); + } + } + public void setTextValues(TextView tv, @StringRes int id) { + String str = getString(id); + if (tv != null && !TextUtils.isEmpty(str)) { + tv.setText(str); + } + } + + /** + * 获取 + * @param colorId + * @return + */ + public int getColors(int colorId) { + return ContextCompat.getColor(this, colorId); + } + + public View getView(@LayoutRes int layoutId){ + return LayoutInflater.from(mContext).inflate(layoutId,null); + } + + +// /** +// * 为下拉刷新的RecyclerView 数据为空时添加空布局,并控制上拉加载状态 +// * @param list +// * @param adapter +// * @param refreshLayout +// * @param +// */ +// public void setFooterView(List list, BaseRecyclerAdapters adapter, SmartRefreshLayout refreshLayout){ +// adapter.recyclerView.setAdapter(adapter);//切换设置FooterView,需要重新setAdapter,不然会报错 +// if(isListNull(list)){ +// adapter.setFooterView(R.layout.empty_view); +// refreshLayout.setEnableLoadMore(false); +// }else{ +// adapter.setFooterView(null); +// refreshLayout.setEnableLoadMore(true); +// } +// } + + + + /** + * 请求数据后 为下拉刷新的RecyclerView 数据为空时添加空布局,并控制上拉加载状态 + * @param list + * @param adapter + * @param + */ + public void showEmptyView(List list, BaseMvvmRecyclerAdapter adapter, SmartRefreshLayout mRefresh, String content){ + if(isListNull(list)){ + adapter.setEmptyView(getEmptyView(content)); + if(mRefresh!=null){ + mRefresh.setEnableLoadMore(false); + mRefresh.finishRefresh(); + mRefresh.finishLoadMore(); + } + ConfigApi.EMPTY_VIEW=true; + }else{ + if(mRefresh!=null) { + mRefresh.setEnableLoadMore(true); + mRefresh.finishRefresh(); + mRefresh.finishLoadMore(); + } + ConfigApi.EMPTY_VIEW=false; + } + ALog.v(adapter.getEmptyViewCount()+"---adapter.getEmptyViewCount()"); + } + + private View emptyView; + private View getEmptyView(String contet){ + if(emptyView==null){ + emptyView=getView(R.layout.empty_view); + emptyView.setOnClickListener(v -> ToastUtils.showShort("点击emptyView刷新不够优雅,直接下拉emptyView刷新吧")); + ViewGroup.LayoutParams lp=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); + emptyView.setLayoutParams(lp); + } + CommUtils.setTextValues(emptyView.findViewById(R.id.tv_empty),contet); + return emptyView; + } + + /** + * 只有确定按钮的简化弹窗 + * @param title + * @param msg + * @param listener + * @return + */ + public Dialog showDialogBysure(String title, String msg, Listener listener){ + return CommUtils.showDialog(mContext,title,msg,"确定" + ,null, listener,null); + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/ArticlesBean.java b/app/src/main/java/com/example/baseframe/bean/ArticlesBean.java new file mode 100644 index 0000000..af283f5 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/ArticlesBean.java @@ -0,0 +1,234 @@ +package com.example.baseframe.bean; + + +public class ArticlesBean { + + /** + * apkLink : http://www.wanandroid.com/blogimgs/e8faab6b-ecb1-4bc2-af96-f7e5039032b3.apk + * author : GcsSloop + * chapterId : 294 + * chapterName : 完整项目 + * collect : false + * courseId : 13 + * desc : Diycode 社区客户端,可以更方便的在手机上查看社区信息。应用采用了数据多级缓存,并且实现了离线浏览(访问过一次的页面会被缓存下来,没有网络也可查看),相比于网页版,在一定程度上可以减少在手机上访问的流量消耗。由于目前功能尚未完善,还存在一些已知或未知的bug,所以当前版本仅为 beta 测试版。 + * envelopePic : http://www.wanandroid.com/blogimgs/8876bcc1-7d12-4443-bf95-3f9a698685a6.png + * id : 2241 + * link : http://www.wanandroid.com/blog/show/2033 + * niceDate : 2018-01-29 + * origin : + * projectLink : https://github.com/GcsSloop/diycode + * publishTime : 1517236491000 + * title : 【开源完整项目】diycode客户端 + * visible : 1 + * zan : 0 + */ + private int userId; + private String apkLink; + private String author; + private int chapterId; + private String chapterName; + private boolean collect; + private int courseId; + private String desc; + private String envelopePic; + private int id; + private int originId = -1;// 收藏文章列表里面的原始文章id + private String link; + private String niceDate; + private String origin; + private String projectLink; + private long publishTime; + private String title; + private int visible; + private int zan; + private boolean fresh; + private boolean isShowImage = true; + // 分类name + private String navigationName; + // 可能没有author 有 shareUser + private String shareUser; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getShareUser() { + return shareUser; + } + + public void setShareUser(String shareUser) { + this.shareUser = shareUser; + } + + public String getNavigationName() { + return navigationName; + } + + public void setNavigationName(String navigationName) { + this.navigationName = navigationName; + } + + public boolean isShowImage() { + return isShowImage; + } + + public void setShowImage(boolean showImage) { + isShowImage = showImage; + } + + public boolean isFresh() { + return fresh; + } + + public void setFresh(boolean fresh) { + this.fresh = fresh; + } + + public int getOriginId() { + return originId; + } + + public void setOriginId(int originId) { + this.originId = originId; + } + + public String getApkLink() { + return apkLink; + } + + public void setApkLink(String apkLink) { + this.apkLink = apkLink; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public int getChapterId() { + return chapterId; + } + + public void setChapterId(int chapterId) { + this.chapterId = chapterId; + } + + public String getChapterName() { + return chapterName; + } + + public void setChapterName(String chapterName) { + this.chapterName = chapterName; + } + + public boolean isCollect() { + return collect; + } + + public void setCollect(boolean collect) { + this.collect = collect; + } + + public int getCourseId() { + return courseId; + } + + public void setCourseId(int courseId) { + this.courseId = courseId; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public String getEnvelopePic() { + return envelopePic; + } + + public void setEnvelopePic(String envelopePic) { + this.envelopePic = envelopePic; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public String getNiceDate() { + return niceDate; + } + + public void setNiceDate(String niceDate) { + this.niceDate = niceDate; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getProjectLink() { + return projectLink; + } + + public void setProjectLink(String projectLink) { + this.projectLink = projectLink; + } + + public long getPublishTime() { + return publishTime; + } + + public void setPublishTime(long publishTime) { + this.publishTime = publishTime; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVisible() { + return visible; + } + + public void setVisible(int visible) { + this.visible = visible; + } + + public int getZan() { + return zan; + } + + public void setZan(int zan) { + this.zan = zan; + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/HomeListBean.java b/app/src/main/java/com/example/baseframe/bean/HomeListBean.java new file mode 100644 index 0000000..62f8703 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/HomeListBean.java @@ -0,0 +1,112 @@ +package com.example.baseframe.bean; + + +import java.util.List; + + +public class HomeListBean { + + + /** + * data : {"curPage":2,"datas":[{"apkLink":"http://www.wanandroid.com/blogimgs/e8faab6b-ecb1-4bc2-af96-f7e5039032b3.apk","author":"GcsSloop","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Diycode 社区客户端,可以更方便的在手机上查看社区信息。应用采用了数据多级缓存,并且实现了离线浏览(访问过一次的页面会被缓存下来,没有网络也可查看),相比于网页版,在一定程度上可以减少在手机上访问的流量消耗。由于目前功能尚未完善,还存在一些已知或未知的bug,所以当前版本仅为 beta 测试版。","envelopePic":"http://www.wanandroid.com/blogimgs/8876bcc1-7d12-4443-bf95-3f9a698685a6.png","id":2241,"link":"http://www.wanandroid.com/blog/show/2033","niceDate":"2018-01-29","origin":"","projectLink":"https://github.com/GcsSloop/diycode","publishTime":1517236491000,"title":"【开源完整项目】diycode客户端","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/13736f4b-6ab5-4223-a851-7354cd6d066e.apk","author":"Will-Ls","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一款新闻客户端, MVP + RxJava + Retrofit + Dagger2\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/5f1511d9-9d8b-41bd-b392-68dbe620f613.png","id":2239,"link":"http://www.wanandroid.com/blog/show/2031","niceDate":"2018-01-29","origin":"","projectLink":"https://github.com/Will-Ls/WeiYue","publishTime":1517232315000,"title":" 【开源完整项目】微阅客户端","visible":1,"zan":0},{"apkLink":"","author":"骑小猪看流星","chapterId":230,"chapterName":"打包","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2238,"link":"http://www.jianshu.com/p/332525b09a88 ","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517210113000,"title":"十分钟快速集成美团多渠道打包","visible":1,"zan":0},{"apkLink":"","author":"箫鉴哥","chapterId":97,"chapterName":"音视频","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2237,"link":"https://yq.aliyun.com/articles/8637","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517207717000,"title":"Android 音频技术开发总结","visible":1,"zan":0},{"apkLink":"","author":" 陈文超happylion","chapterId":185,"chapterName":"组件化","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2236,"link":"https://mp.weixin.qq.com/s/Tw-04it4_G4AgUmRO8imaw","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517192099000,"title":"美团猫眼电影android模块化实战","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/57a54bc0-5855-433c-8af6-59c0a68fc0c5.apk","author":"wangzailfm","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"使用Kotlin构建的wanandroid客户端\r\nKotlin + MVP + Kotlin-Coroutines + Retrofit2(GsonCallAdapterFactory + CoroutineCallAdapterFactory)\r\n\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/2f98fdd8-523f-48fc-a2a0-d20b90041b34.jpeg","id":2235,"link":"http://www.wanandroid.com/blog/show/2029","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/wangzailfm/WanAndroidClient","publishTime":1517150407000,"title":"【开源完整项目】wanandroid客户端","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/2840d80f-b099-417f-a00b-17e1910bd21a.apk","author":"DuanJiaNing","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"【我的音乐-Musicoco】 音乐播放器,功能:通过耳机和通知栏快捷控制音乐播放、创建歌单、本地歌曲搜索、记忆播放、自动切换到夜间模式、定时停止播放、应用主题自定义以及播放界面风格选择等功能。\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/fb84255b-697a-48f8-9cba-d785b22266fd.jpg","id":2234,"link":"http://www.wanandroid.com/blog/show/2027","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/DuanJiaNing/Musicoco","publishTime":1517149791000,"title":"【开源完整项目】Musicoco 管理本地音乐的app","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/538bddd9-eda7-4568-800c-2cd1bc77ab93.apk","author":"Kyson","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Android开发者在性能检测方面的工具一直比较匮乏,仅有的一些工具,比如Android Device Monitor,使用起来也有些繁琐,使用起来对开发者有一定的要求。而线上的App监控更无从谈起。所以需要有一个系统能够提供Debug和Release阶段全方位的监控,更深入地了解对App运行时的状态。\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/8483ff55-692b-4ac3-ae01-d7605b870d1f.png","id":2233,"link":"http://www.wanandroid.com/blog/show/2026","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/Kyson/AndroidGodEye/","publishTime":1517149661000,"title":"【开源完整项目】 AndroidGodEye 监控Android数据指标","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/e4d48142-8668-487d-8d37-83a6566555ba.apk","author":"Rayhahah","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一款资讯类应用~~~o(* ̄▽ ̄*)ブ,MVP+Retrofit+Rxjava\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/b8ed8741-75f9-47a8-8148-0540644f3f83.jpg","id":2232,"link":"http://www.wanandroid.com/blog/show/2024","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/Rayhahah/EasySports","publishTime":1517149531000,"title":"【开源完整项目】仿虎扑应用EasySport","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/be28932a-5946-4eed-89ee-9d919ba7ec75.apk","author":"maoruibin","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一个实现『划词翻译』功能的 Android 应用 ,可能是目前 Android 市场上翻译效率最高的一款应用。\r\n\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/9249453d-0578-410f-8237-3c6e204c0c4b.gif","id":2231,"link":"http://www.wanandroid.com/blog/show/2025","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/maoruibin/TranslateApp","publishTime":1517149256000,"title":"【开源完整项目】咕咚翻译App","visible":1,"zan":0},{"apkLink":"","author":"LRH1993","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Google在今年的IO大会上宣布,将Kotlin作为Android开发的一级语言。作为紧跟潮流的弄潮儿,对kotlin稍做了解后,发现其有优秀的特性,所以就开始了学习,而Eyepetizer-in-Kotlin便是对kotlin进行学习后的阶段性成果。","envelopePic":"http://www.wanandroid.com/blogimgs/d8e91478-5f79-460b-8e39-42fc166b5519.png","id":2230,"link":"http://www.wanandroid.com/blog/show/2028","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/LRH1993/Eyepetizer-in-Kotlin","publishTime":1517149004000,"title":"【开源完整项目】开眼视频学习项目","visible":1,"zan":0},{"apkLink":"","author":" JensenChen","chapterId":307,"chapterName":"Apk诞生记","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2229,"link":"https://juejin.im/post/5a69c0ccf265da3e2a0dc9aa","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516936185000,"title":"10分钟了解Android项目构建流程","visible":1,"zan":0},{"apkLink":"","author":"BryantPang","chapterId":254,"chapterName":"新闻资讯","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2228,"link":"https://github.com/BryantPang/ReadHub","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935679000,"title":"ReadHub 新闻资讯客户端","visible":1,"zan":0},{"apkLink":"","author":"DuanJiaNing","chapterId":256,"chapterName":"音乐、视频类","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2227,"link":"https://github.com/DuanJiaNing/Musicoco","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935645000,"title":"Musicoco 管理本地音乐的app","visible":1,"zan":0},{"apkLink":"","author":"Kyson","chapterId":255,"chapterName":"工具类","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2226,"link":"https://github.com/Kyson/AndroidGodEye/","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935614000,"title":"AndroidGodEye 监控Android数据指标","visible":1,"zan":0},{"apkLink":"","author":"小编","chapterId":292,"chapterName":"pdf电子书","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2225,"link":"http://wanandroid.com/blog/show/2022","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516872111000,"title":"1月24日 区块链线上分享 ppt下载","visible":1,"zan":0},{"apkLink":"","author":"看书的小蜗牛","chapterId":99,"chapterName":"具体案例","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2224,"link":"https://mp.weixin.qq.com/s/tHkltAvN1Ila8L3brdAYGQ","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516846304000,"title":"仿天猫、京东拖拽商品详情","visible":1,"zan":0},{"apkLink":"","author":"一口仨馍","chapterId":306,"chapterName":"多线程与并发","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2223,"link":"https://mp.weixin.qq.com/s/KuKROR8c4Bc1CdXE6AxB2g","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516846283000,"title":"应该了解的一些并发基础知识","visible":1,"zan":0},{"apkLink":"","author":"小编","chapterId":305,"chapterName":"各类工具","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2221,"link":"http://www.wanandroid.com/blog/show/2021","niceDate":"2018-01-24","origin":"","projectLink":"","publishTime":1516778328000,"title":"工具分享 :Jad 实现Class转Java文件","visible":1,"zan":0},{"apkLink":"","author":"MikanMu","chapterId":304,"chapterName":"基础源码","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2220,"link":"http://blog.csdn.net/mhmyqn/article/details/48087247","niceDate":"2018-01-24","origin":"","projectLink":"","publishTime":1516771306000,"title":"java枚举类型的实现原理","visible":1,"zan":0}],"offset":20,"over":false,"pageCount":53,"size":20,"total":1049} + * errorCode : 0 + * errorMsg : + */ + +// private DataBean data; +// private int errorCode; +// private String errorMsg; + + +// public DataBean getData() { +// return data; +// } +// +// public void setData(DataBean data) { +// this.data = data; +// } +// +// public int getErrorCode() { +// return errorCode; +// } +// +// public void setErrorCode(int errorCode) { +// this.errorCode = errorCode; +// } +// +// public String getErrorMsg() { +// return errorMsg; +// } +// +// public void setErrorMsg(String errorMsg) { +// this.errorMsg = errorMsg; +// } + + /** + * curPage : 2 + * datas : [{"apkLink":"http://www.wanandroid.com/blogimgs/e8faab6b-ecb1-4bc2-af96-f7e5039032b3.apk","author":"GcsSloop","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Diycode 社区客户端,可以更方便的在手机上查看社区信息。应用采用了数据多级缓存,并且实现了离线浏览(访问过一次的页面会被缓存下来,没有网络也可查看),相比于网页版,在一定程度上可以减少在手机上访问的流量消耗。由于目前功能尚未完善,还存在一些已知或未知的bug,所以当前版本仅为 beta 测试版。","envelopePic":"http://www.wanandroid.com/blogimgs/8876bcc1-7d12-4443-bf95-3f9a698685a6.png","id":2241,"link":"http://www.wanandroid.com/blog/show/2033","niceDate":"2018-01-29","origin":"","projectLink":"https://github.com/GcsSloop/diycode","publishTime":1517236491000,"title":"【开源完整项目】diycode客户端","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/13736f4b-6ab5-4223-a851-7354cd6d066e.apk","author":"Will-Ls","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一款新闻客户端, MVP + RxJava + Retrofit + Dagger2\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/5f1511d9-9d8b-41bd-b392-68dbe620f613.png","id":2239,"link":"http://www.wanandroid.com/blog/show/2031","niceDate":"2018-01-29","origin":"","projectLink":"https://github.com/Will-Ls/WeiYue","publishTime":1517232315000,"title":" 【开源完整项目】微阅客户端","visible":1,"zan":0},{"apkLink":"","author":"骑小猪看流星","chapterId":230,"chapterName":"打包","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2238,"link":"http://www.jianshu.com/p/332525b09a88 ","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517210113000,"title":"十分钟快速集成美团多渠道打包","visible":1,"zan":0},{"apkLink":"","author":"箫鉴哥","chapterId":97,"chapterName":"音视频","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2237,"link":"https://yq.aliyun.com/articles/8637","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517207717000,"title":"Android 音频技术开发总结","visible":1,"zan":0},{"apkLink":"","author":" 陈文超happylion","chapterId":185,"chapterName":"组件化","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2236,"link":"https://mp.weixin.qq.com/s/Tw-04it4_G4AgUmRO8imaw","niceDate":"2018-01-29","origin":"","projectLink":"","publishTime":1517192099000,"title":"美团猫眼电影android模块化实战","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/57a54bc0-5855-433c-8af6-59c0a68fc0c5.apk","author":"wangzailfm","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"使用Kotlin构建的wanandroid客户端\r\nKotlin + MVP + Kotlin-Coroutines + Retrofit2(GsonCallAdapterFactory + CoroutineCallAdapterFactory)\r\n\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/2f98fdd8-523f-48fc-a2a0-d20b90041b34.jpeg","id":2235,"link":"http://www.wanandroid.com/blog/show/2029","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/wangzailfm/WanAndroidClient","publishTime":1517150407000,"title":"【开源完整项目】wanandroid客户端","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/2840d80f-b099-417f-a00b-17e1910bd21a.apk","author":"DuanJiaNing","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"【我的音乐-Musicoco】 音乐播放器,功能:通过耳机和通知栏快捷控制音乐播放、创建歌单、本地歌曲搜索、记忆播放、自动切换到夜间模式、定时停止播放、应用主题自定义以及播放界面风格选择等功能。\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/fb84255b-697a-48f8-9cba-d785b22266fd.jpg","id":2234,"link":"http://www.wanandroid.com/blog/show/2027","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/DuanJiaNing/Musicoco","publishTime":1517149791000,"title":"【开源完整项目】Musicoco 管理本地音乐的app","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/538bddd9-eda7-4568-800c-2cd1bc77ab93.apk","author":"Kyson","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Android开发者在性能检测方面的工具一直比较匮乏,仅有的一些工具,比如Android Device Monitor,使用起来也有些繁琐,使用起来对开发者有一定的要求。而线上的App监控更无从谈起。所以需要有一个系统能够提供Debug和Release阶段全方位的监控,更深入地了解对App运行时的状态。\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/8483ff55-692b-4ac3-ae01-d7605b870d1f.png","id":2233,"link":"http://www.wanandroid.com/blog/show/2026","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/Kyson/AndroidGodEye/","publishTime":1517149661000,"title":"【开源完整项目】 AndroidGodEye 监控Android数据指标","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/e4d48142-8668-487d-8d37-83a6566555ba.apk","author":"Rayhahah","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一款资讯类应用~~~o(* ̄▽ ̄*)ブ,MVP+Retrofit+Rxjava\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/b8ed8741-75f9-47a8-8148-0540644f3f83.jpg","id":2232,"link":"http://www.wanandroid.com/blog/show/2024","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/Rayhahah/EasySports","publishTime":1517149531000,"title":"【开源完整项目】仿虎扑应用EasySport","visible":1,"zan":0},{"apkLink":"http://www.wanandroid.com/blogimgs/be28932a-5946-4eed-89ee-9d919ba7ec75.apk","author":"maoruibin","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"一个实现『划词翻译』功能的 Android 应用 ,可能是目前 Android 市场上翻译效率最高的一款应用。\r\n\r\n","envelopePic":"http://www.wanandroid.com/blogimgs/9249453d-0578-410f-8237-3c6e204c0c4b.gif","id":2231,"link":"http://www.wanandroid.com/blog/show/2025","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/maoruibin/TranslateApp","publishTime":1517149256000,"title":"【开源完整项目】咕咚翻译App","visible":1,"zan":0},{"apkLink":"","author":"LRH1993","chapterId":294,"chapterName":"完整项目","collect":false,"courseId":13,"desc":"Google在今年的IO大会上宣布,将Kotlin作为Android开发的一级语言。作为紧跟潮流的弄潮儿,对kotlin稍做了解后,发现其有优秀的特性,所以就开始了学习,而Eyepetizer-in-Kotlin便是对kotlin进行学习后的阶段性成果。","envelopePic":"http://www.wanandroid.com/blogimgs/d8e91478-5f79-460b-8e39-42fc166b5519.png","id":2230,"link":"http://www.wanandroid.com/blog/show/2028","niceDate":"2018-01-28","origin":"","projectLink":"https://github.com/LRH1993/Eyepetizer-in-Kotlin","publishTime":1517149004000,"title":"【开源完整项目】开眼视频学习项目","visible":1,"zan":0},{"apkLink":"","author":" JensenChen","chapterId":307,"chapterName":"Apk诞生记","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2229,"link":"https://juejin.im/post/5a69c0ccf265da3e2a0dc9aa","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516936185000,"title":"10分钟了解Android项目构建流程","visible":1,"zan":0},{"apkLink":"","author":"BryantPang","chapterId":254,"chapterName":"新闻资讯","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2228,"link":"https://github.com/BryantPang/ReadHub","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935679000,"title":"ReadHub 新闻资讯客户端","visible":1,"zan":0},{"apkLink":"","author":"DuanJiaNing","chapterId":256,"chapterName":"音乐、视频类","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2227,"link":"https://github.com/DuanJiaNing/Musicoco","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935645000,"title":"Musicoco 管理本地音乐的app","visible":1,"zan":0},{"apkLink":"","author":"Kyson","chapterId":255,"chapterName":"工具类","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2226,"link":"https://github.com/Kyson/AndroidGodEye/","niceDate":"2018-01-26","origin":"","projectLink":"","publishTime":1516935614000,"title":"AndroidGodEye 监控Android数据指标","visible":1,"zan":0},{"apkLink":"","author":"小编","chapterId":292,"chapterName":"pdf电子书","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2225,"link":"http://wanandroid.com/blog/show/2022","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516872111000,"title":"1月24日 区块链线上分享 ppt下载","visible":1,"zan":0},{"apkLink":"","author":"看书的小蜗牛","chapterId":99,"chapterName":"具体案例","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2224,"link":"https://mp.weixin.qq.com/s/tHkltAvN1Ila8L3brdAYGQ","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516846304000,"title":"仿天猫、京东拖拽商品详情","visible":1,"zan":0},{"apkLink":"","author":"一口仨馍","chapterId":306,"chapterName":"多线程与并发","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2223,"link":"https://mp.weixin.qq.com/s/KuKROR8c4Bc1CdXE6AxB2g","niceDate":"2018-01-25","origin":"","projectLink":"","publishTime":1516846283000,"title":"应该了解的一些并发基础知识","visible":1,"zan":0},{"apkLink":"","author":"小编","chapterId":305,"chapterName":"各类工具","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2221,"link":"http://www.wanandroid.com/blog/show/2021","niceDate":"2018-01-24","origin":"","projectLink":"","publishTime":1516778328000,"title":"工具分享 :Jad 实现Class转Java文件","visible":1,"zan":0},{"apkLink":"","author":"MikanMu","chapterId":304,"chapterName":"基础源码","collect":false,"courseId":13,"desc":"","envelopePic":"","id":2220,"link":"http://blog.csdn.net/mhmyqn/article/details/48087247","niceDate":"2018-01-24","origin":"","projectLink":"","publishTime":1516771306000,"title":"java枚举类型的实现原理","visible":1,"zan":0}] + * offset : 20 + * over : false + * pageCount : 53 + * size : 20 + * total : 1049 + */ + + private int curPage; + private int offset; + private int pageCount; + private int size; + private int total; + private List datas; + + + public int getCurPage() { + return curPage; + } + + public void setCurPage(int curPage) { + this.curPage = curPage; + } + + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getPageCount() { + return pageCount; + } + + public void setPageCount(int pageCount) { + this.pageCount = pageCount; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List getDatas() { + return datas; + } + + public void setDatas(List datas) { + this.datas = datas; + } + +} diff --git a/app/src/main/java/com/example/baseframe/bean/LoginBean.java b/app/src/main/java/com/example/baseframe/bean/LoginBean.java new file mode 100644 index 0000000..94050fc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/LoginBean.java @@ -0,0 +1,124 @@ +package com.example.baseframe.bean; + + +import java.util.List; + +/** + * @author jingbin + * @data 2018/5/7 + * @Description + */ + +public class LoginBean { + + + /** + * data : {"collectIds":[2317,2255,2324],"email":"","icon":"","id":1534,"password":"jingbin54770","type":0,"username":"jingbin"} + * errorCode : 0 + * errorMsg : + */ + +// private User data; +// private int errorCode; +// private String errorMsg; + +// public User getData() { +// return data; +// } +// +// public void setData(User data) { +// this.data = data; +// } +// +// public int getErrorCode() { +// return errorCode; +// } +// +// public void setErrorCode(int errorCode) { +// this.errorCode = errorCode; +// } +// +// public String getErrorMsg() { +// return errorMsg; +// } +// +// public void setErrorMsg(String errorMsg) { +// this.errorMsg = errorMsg; +// } + + public static class DataBean { + /** + * collectIds : [2317,2255,2324] + * email : + * icon : + * id : 1534 + * password : jingbin54770 + * type : 0 + * username : jingbin + */ + + private String email; + private String icon; + private int id; + private String password; + private int type; + private String username; + private List collectIds; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List getCollectIds() { + return collectIds; + } + + public void setCollectIds(List collectIds) { + this.collectIds = collectIds; + } + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/ResultBean.java b/app/src/main/java/com/example/baseframe/bean/ResultBean.java new file mode 100644 index 0000000..276c362 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/ResultBean.java @@ -0,0 +1,47 @@ +package com.example.baseframe.bean; + +import java.io.Serializable; + +/** + * 返回值 返回值 data节点为 object + */ +public class ResultBean implements Serializable { + // { +// "code": 5, +// "data": object, +// "msg": "未知司机" +// } + public ResultBean(int code, String msg) { + this.errorCode = code; + this.errorMsg = msg; + } + private int errorCode; + private T data; + + + private String errorMsg; + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/ResultBeans.java b/app/src/main/java/com/example/baseframe/bean/ResultBeans.java new file mode 100644 index 0000000..fc7e915 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/ResultBeans.java @@ -0,0 +1,48 @@ +package com.example.baseframe.bean; + +import java.io.Serializable; +import java.util.List; + +/** + * 返回值 data节点为 list + */ +public class ResultBeans implements Serializable { + // { +// "code": 5, +// "data": List, +// "msg": "未知司机" +// } + public ResultBeans(int code, String msg) { + this.errorCode = code; + this.errorMsg = msg; + } + private int errorCode; + private List data; + + + private String errorMsg; + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/User.java b/app/src/main/java/com/example/baseframe/bean/User.java new file mode 100644 index 0000000..ead1d1c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/User.java @@ -0,0 +1,112 @@ +package com.example.baseframe.bean; + + +public class User { + + + private int id; + + private String email; + + private String icon; + + private String password; + + private int type; + + private String username; + + private int coinCount; + + private int rank; + + + public int getId() { + return id; + } + + public void setId( int id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public User( int id, String email, String icon, String password, int type, String username, int coinCount, int rank) { + this.id = id; + this.email = email; + this.icon = icon; + this.password = password; + this.type = type; + this.username = username; + this.coinCount = coinCount; + this.rank = rank; + } + + public int getCoinCount() { + return coinCount; + } + + public void setCoinCount(int coinCount) { + this.coinCount = coinCount; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email='" + email + '\'' + + ", icon='" + icon + '\'' + + ", password='" + password + '\'' + + ", type=" + type + + ", username='" + username + '\'' + + ", coinCount=" + coinCount + + ", rank=" + rank + + '}'; + } +} diff --git a/app/src/main/java/com/example/baseframe/bean/WanAndroidBannerBean.java b/app/src/main/java/com/example/baseframe/bean/WanAndroidBannerBean.java new file mode 100644 index 0000000..523c4cf --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bean/WanAndroidBannerBean.java @@ -0,0 +1,109 @@ +package com.example.baseframe.bean; + +import androidx.databinding.ObservableInt; + +import java.io.Serializable; +import java.util.List; + +/** + * @author jingbin + * @data 2018/2/8 + * @Description + */ + +public class WanAndroidBannerBean implements Serializable { + + public ObservableInt progressValue=new ObservableInt(0);//用来演示更新下载进度 + + + /** + * data : [{"desc":"区块链养狗领取 百度莱茨狗","id":8,"imagePath":"http://www.wanandroid.com/blogimgs/a90cbfe5-b1e8-4354-8d45-e3fbf8445383.png","isVisible":1,"order":0,"title":"区块链养狗领取 百度莱茨狗","type":0,"url":"http://www.wanandroid.com/blog/show/2037"},{"desc":"","id":6,"imagePath":"http://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png","isVisible":1,"order":1,"title":"我们新增了一个常用导航Tab~","type":0,"url":"http://www.wanandroid.com/navi"},{"desc":"","id":7,"imagePath":"http://www.wanandroid.com/blogimgs/ffb61454-e0d2-46e7-bc9b-4f359061ae20.png","isVisible":1,"order":2,"title":"送你一个暖心的Mock API工具","type":0,"url":"http://www.wanandroid.com/blog/show/10"},{"desc":"","id":3,"imagePath":"http://www.wanandroid.com/blogimgs/fb0ea461-e00a-482b-814f-4faca5761427.png","isVisible":1,"order":3,"title":"兄弟,要不要挑个项目学习下?","type":0,"url":"http://www.wanandroid.com/article/list/0?cid=254"},{"desc":"","id":4,"imagePath":"http://www.wanandroid.com/blogimgs/ab17e8f9-6b79-450b-8079-0f2287eb6f0f.png","isVisible":1,"order":3,"title":"看看别人的面经,搞定面试~","type":0,"url":"http://www.wanandroid.com/article/list/0?cid=73"},{"desc":"","id":2,"imagePath":"http://www.wanandroid.com/blogimgs/90cf8c40-9489-4f9d-8936-02c9ebae31f0.png","isVisible":1,"order":2,"title":"JSON工具","type":1,"url":"http://www.wanandroid.com/tools/bejson"},{"desc":"","id":5,"imagePath":"http://www.wanandroid.com/blogimgs/acc23063-1884-4925-bdf8-0b0364a7243e.png","isVisible":1,"order":3,"title":"微信文章合集","type":1,"url":"http://www.wanandroid.com/blog/show/6"}] + * errorCode : 0 + * errorMsg : + */ + +// private int errorCode; +// private String errorMsg; +// private List data; //这几个字段是每个接口公共的字段 已经放到ResultBean里了 + + /** + * desc : 区块链养狗领取 百度莱茨狗 + * id : 8 + * imagePath : http://www.wanandroid.com/blogimgs/a90cbfe5-b1e8-4354-8d45-e3fbf8445383.png + * isVisible : 1 + * order : 0 + * title : 区块链养狗领取 百度莱茨狗 + * type : 0 + * url : http://www.wanandroid.com/blog/show/2037 + */ + + private String desc; + private String imagePath; + private String title; + private String url; + + private List images; + private List titles; + + public List getImages() { + return images; + } + + public void setImages(List images) { + this.images = images; + } + + public List getTitles() { + return titles; + } + + public void setTitles(List titles) { + this.titles = titles; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public String getImagePath() { + return imagePath; + } + + public void setImagePath(String imagePath) { + this.imagePath = imagePath; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + + @Override + public String toString() { + return "WanAndroidBannerBean{" + + "progressValue=" + progressValue + + ", desc='" + desc + '\'' + + ", imagePath='" + imagePath + '\'' + + ", title='" + title + '\'' + + ", url='" + url + '\'' + + ", images=" + images + + ", titles=" + titles + + '}'; + } +} diff --git a/app/src/main/java/com/example/baseframe/bindingadapter/DataBindingAdapter.java b/app/src/main/java/com/example/baseframe/bindingadapter/DataBindingAdapter.java new file mode 100644 index 0000000..568cefb --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bindingadapter/DataBindingAdapter.java @@ -0,0 +1,100 @@ +package com.example.baseframe.bindingadapter; + +import android.text.TextUtils; +import android.widget.ImageView; + +import androidx.databinding.BindingAdapter; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.bumptech.glide.request.RequestOptions; +import com.example.baseframe.base.BaseMvvmRecyclerAdapter; +import com.example.baseframe.weight.recyclerview.DividerLine; + +import static com.example.baseframe.weight.recyclerview.DividerLine.LineDrawMode.BOTH; +import static com.example.baseframe.weight.recyclerview.DividerLine.LineDrawMode.HORIZONTAL; +import static com.example.baseframe.weight.recyclerview.DividerLine.LineDrawMode.VERTICAL; + + +public class DataBindingAdapter { + private static final String TAG = DataBindingAdapter.class.getSimpleName(); + + /** + * 圆形图片 + * + * @param img + * @param path + */ + @BindingAdapter("circleImg") + public static void setCircleImg(ImageView img, String path) { + if (path == null || path.isEmpty()) { + return; + } + Glide + .with(img) + .load(TextUtils.isDigitsOnly(path) ? Integer.valueOf(path) : path) + .apply(RequestOptions.bitmapTransform(new CircleCrop())) + .into(img); + } + + /** + * 加载网络或者本地资源 + * + * @param img + * @param url + */ + @BindingAdapter("imgUrl") + public static void setImgUrl(ImageView img, String url) { + if (url == null || url.isEmpty()) { + return; + } + //如果是-1不设置图片 + if (TextUtils.isDigitsOnly(url)) { + int resId = Integer.valueOf(url); + if (resId <= 0) { + return; + } + } + Glide + .with(img) + .load(TextUtils.isDigitsOnly(url) ? Integer.valueOf(url) : url) + .into(img); + } + + + + + + @BindingAdapter({"android:itemDecoration"}) + public static void addItemDecoration(RecyclerView mRecyclerView, int type) { + switch (type){ + case 0: + mRecyclerView.addItemDecoration(new DividerLine(mRecyclerView.getContext(),HORIZONTAL)); + break; + case 1: + mRecyclerView.addItemDecoration(new DividerLine(mRecyclerView.getContext(),VERTICAL)); + break; + case 2: + mRecyclerView.addItemDecoration(new DividerLine(mRecyclerView.getContext(),BOTH)); + break; + } + + mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(), DividerItemDecoration.VERTICAL)); + } + + + @BindingAdapter(value ={"adapter", "bindAdapterAnimation"}, requireAll = false) + public static void bindAdapter(RecyclerView recyclerView, BaseMvvmRecyclerAdapter adapter, int animation) { + recyclerView.setAdapter(adapter); + //设置动画 + if (animation != 0) { + adapter.openLoadAnimation(animation); + } + //adapter.notifyDataSetChanged(); + // recyclerView.setPageFooter(R.layout.layout_loading_footer); + } + + +} diff --git a/app/src/main/java/com/example/baseframe/bindingadapter/ViewAdapters.java b/app/src/main/java/com/example/baseframe/bindingadapter/ViewAdapters.java new file mode 100644 index 0000000..53ff67f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bindingadapter/ViewAdapters.java @@ -0,0 +1,85 @@ +package com.example.baseframe.bindingadapter; + +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.databinding.BindingAdapter; + +import com.blankj.ALog; +import com.example.baseframe.bus.event.SingleLiveEvent; +import com.example.baseframe.listener.ClickListener; +import com.example.baseframe.utils.ButtonUtils; +import com.example.baseframe.utils.CommUtils; + + +/** + * @Description: ViewAdapter类作用描述 + * @Author: yzh + * @CreateDate: 2019/11/15 14:37 + */ +public class ViewAdapters { + + /** + * requireAll 是意思是是否需要绑定全部参数, false为否 + */ + @BindingAdapter(value = {"textIsNullGone"}, requireAll = false) + public static void setTexts(TextView view, String string) { + //如果是空的话就设置为Gone + if (TextUtils.isEmpty(string)) { + if (view.getVisibility() != View.GONE) { + view.setVisibility(View.GONE); + } + + } else { + if (view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + } + view.setText(string); + } + } + + + @BindingAdapter(value = {"onBindingClick"}, requireAll = false) + public static void onClicks(View view, ClickListener listener) { + if(listener!=null){ + //view.setOnClickListener(listener::onResult); + view.setOnClickListener(v -> { + if(!ButtonUtils.isFastDoubleClick()){ + listener.onResult(v); + } + }); + } + } + + @BindingAdapter(value = {"onBindingClick"}, requireAll = false) + public static void onClick(View view, SingleLiveEvent event) { + if(event!=null){ + view.setOnClickListener(v -> { + if(!ButtonUtils.isFastDoubleClick()){ + event.call(); + } + }); + + } + } + + /** + * 设置宽高 + * @param imageView + * @param width + * @param height + */ + @BindingAdapter(value = {"setWidth","setHeight"}, requireAll = false) + public static void setHW(View imageView, int width,int height) { + if(width>0){ + ALog.w(width+"---------"+ CommUtils.dip2px(width)); + imageView.getLayoutParams().width= CommUtils.dip2px(width); + } + if(height>0){ + imageView.getLayoutParams().height=CommUtils.dip2px(height); + } + } + + +} diff --git a/app/src/main/java/com/example/baseframe/bus/RxBus.java b/app/src/main/java/com/example/baseframe/bus/RxBus.java new file mode 100644 index 0000000..0b8d329 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/RxBus.java @@ -0,0 +1,112 @@ +package com.example.baseframe.bus; + + +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.functions.Predicate; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.Subject; + +/** + * Created by yzh on 19/11/11. + */ +public class RxBus { + /** + * 参考: http://hanhailong.com/2015/10/09/RxBus%E2%80%94%E9%80%9A%E8%BF%87RxJava%E6%9D%A5%E6%9B%BF%E6%8D%A2EventBus/ + * http://www.loongwind.com/archives/264.html + * https://theseyears.gitbooks.io/android-architecture-journey/content/rxbus.html + */ + + //RxBus=用RxJava模拟实现的EventBus的功能,不需要再额外引入EventBus库增加app代码量 +// RxBus.getDefault().post(1,"呵呵呵哒"); +// +// RxBus.getDefault().toObservable(String.class).subscribe(new Consumer() { +// @Override +// public void accept(String s) throws Exception { +// +// } +// }); +// } + + + private static volatile RxBus mDefaultInstance; + + private RxBus() { + } + + public static RxBus getDefault() { + if (mDefaultInstance == null) { + synchronized (RxBus.class) { + if (mDefaultInstance == null) { + mDefaultInstance = new RxBus(); + } + } + } + return mDefaultInstance; + } + + private final Subject _bus = PublishSubject.create().toSerialized(); + + public void send(Object o) { + _bus.onNext(o); + } + + public Observable toObservable() { + return _bus; + } + + /** + * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者 + * + * @param eventType 事件类型 + * @param + * @return + */ + public Observable toObservable(Class eventType) { + return _bus.ofType(eventType); + } + + /** + * 提供了一个新的事件,根据code进行分发 + * + * @param code 事件code + * @param o + */ + public void post(int code, Object o) { + _bus.onNext(new RxBusMessage(code, o)); + + } + + /** + * 根据传递的code和 eventType 类型返回特定类型(eventType)的 被观察者 + * 对于注册了code为0,class为voidMessage的观察者,那么就接收不到code为0之外的voidMessage。 + * + * @param code 事件code + * @param eventType 事件类型 + * @param + * @return + */ + public Observable toObservable(final int code, final Class eventType) { + return _bus.ofType(RxBusMessage.class) + .filter(new Predicate() { + @Override + public boolean test(RxBusMessage rxBusMessage) throws Exception { + //过滤code和eventType都相同的事件 + return rxBusMessage.getCode() == code && eventType.isInstance(rxBusMessage.getObject()); + } + }).map(new Function() { + @Override + public Object apply(RxBusMessage rxBusMessage) throws Exception { + return rxBusMessage.getObject(); + } + }).cast(eventType); + } + + /** + * 判断是否有订阅者 + */ + public boolean hasObservers() { + return _bus.hasObservers(); + } + +} diff --git a/app/src/main/java/com/example/baseframe/bus/RxBusCode.java b/app/src/main/java/com/example/baseframe/bus/RxBusCode.java new file mode 100644 index 0000000..213035f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/RxBusCode.java @@ -0,0 +1,12 @@ +package com.example.baseframe.bus; + + +public class RxBusCode { + + // 对应type + public static final int TYPE_0 = 0; + // 首页跳转到详情 + public static final int TYPE_1 = 1; + // 下载 + public static final int TYPE_2 = 2; +} diff --git a/app/src/main/java/com/example/baseframe/bus/RxBusMessage.java b/app/src/main/java/com/example/baseframe/bus/RxBusMessage.java new file mode 100644 index 0000000..345d164 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/RxBusMessage.java @@ -0,0 +1,19 @@ +package com.example.baseframe.bus; + +public class RxBusMessage { + private int code; + private Object object; + public RxBusMessage(int code, Object object){ + this.code=code; + this.object=object; + } + public RxBusMessage(){} + + public int getCode() { + return code; + } + + public Object getObject() { + return object; + } +} diff --git a/app/src/main/java/com/example/baseframe/bus/RxSubscriptions.java b/app/src/main/java/com/example/baseframe/bus/RxSubscriptions.java new file mode 100644 index 0000000..5a97637 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/RxSubscriptions.java @@ -0,0 +1,36 @@ +package com.example.baseframe.bus; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * 管理 CompositeSubscription + */ +public class RxSubscriptions { + private static CompositeDisposable mSubscriptions = new CompositeDisposable(); + + public static boolean isDisposed() { + return mSubscriptions.isDisposed(); + } + + public static void add(Disposable s) { + if (s != null) { + mSubscriptions.add(s); + } + } + + public static void remove(Disposable s) { + if (s != null) { + mSubscriptions.remove(s); + } + } + + public static void clear() { + mSubscriptions.clear(); + } + + public static void dispose() { + mSubscriptions.dispose(); + } + +} diff --git a/app/src/main/java/com/example/baseframe/bus/RxTimerUtil.java b/app/src/main/java/com/example/baseframe/bus/RxTimerUtil.java new file mode 100644 index 0000000..7f89418 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/RxTimerUtil.java @@ -0,0 +1,280 @@ +package com.example.baseframe.bus; + +import android.graphics.Color; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.utils.CommUtils; + + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; + +/** + * Rxjava2.x实现轮询定时器. + * 2019年04月15日09:07:59 + * @author yzh + */ +public class RxTimerUtil { + + /** + * 作用1、避免重复执行相同name定时器2,计时结束后取消订阅 + */ + private static Map mDisposableMap=new HashMap(); + /** + * x秒后执行next操作 + */ + public static void timer(long seconds, final String name, final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.timer(seconds, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + //取消订阅 + cancel(name); + } + + @Override + public void onComplete() { + //取消订阅 + cancel(name); + } + }); + } + + + /** + * 每隔milliseconds秒后执行next操作 + * @param milliseconds + * @param name 给当前定时器命名 + * @param next + */ + public static void interval(long milliseconds, final String name , final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(milliseconds, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + cancel(name); + ALog.e("---onError---"); + } + + @Override + public void onComplete() { + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void interval(long milliseconds, TimeUnit unit, final String name, final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(milliseconds,unit) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + cancel(name); + } + + @Override + public void onComplete() { + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void countDownTimer(final long seconds, final String name, final TextView tv) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(0, 1, TimeUnit.SECONDS) + .take(seconds + 1) + .map(new Function() { + @Override + public Long apply(Long aLong) throws Exception { + return seconds - aLong; + } + }) + .observeOn(AndroidSchedulers.mainThread())//ui线程中进行控件更新 + .doOnSubscribe(new Consumer() { + @Override + public void accept(Disposable disposable) throws Exception { + tv.setEnabled(false); + tv.setTextColor(Color.BLACK); + } + }).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) {mDisposableMap.put(name,disposable); } + @Override + public void onNext(Long num) { + tv.setText("剩余" + num + "秒"); + } + @Override + public void onError(Throwable e) {cancel(name);} + + @Override + public void onComplete() { + //回复原来初始状态 + tv.setEnabled(true); + tv.setText("发送验证码"); + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void countDownTimer(final long seconds, final String name,TextView tv,ITimer iTimer) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(0, 1, TimeUnit.SECONDS) + .take(seconds + 1) + .map(new Function() { + @Override + public Long apply(Long aLong) throws Exception { + return seconds - aLong; + } + }) + .observeOn(AndroidSchedulers.mainThread())//ui线程中进行控件更新 + .doOnSubscribe(new Consumer() { + @Override + public void accept(Disposable disposable) throws Exception { + if(tv!=null){ + tv.setVisibility(View.VISIBLE); + CommUtils.setTextColor(tv, R.color.color_write); + } + + } + }) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) {mDisposableMap.put(name,disposable); } + @Override + public void onNext(Long num) { + //tv.setText("剩余" + num + "秒"); + + if(tv!=null){ + if(num<=3){ + CommUtils.setTextColor(tv, R.color.color_red); + } + tv.setText(String.valueOf(num)); + } + + iTimer.doNext(num,num==0); + } + @Override + public void onError(Throwable e) {cancel(name);} + + @Override + public void onComplete() { + //回复原来初始状态 + // tv.setEnabled(true); + // tv.setText("发送验证码"); + cancel(name); + if(tv!=null){ + tv.setVisibility(View.GONE); + } + + } + }); + } + + + /** + * 取消订阅 + */ + public static void cancel(String timerName) { + Disposable mDisposable= (Disposable) mDisposableMap.get(timerName); + if (mDisposable != null) { + mDisposableMap.remove(timerName); + + if(!mDisposable.isDisposed()){ + mDisposable.dispose(); + Log.i("RxTimerUtil","---Rx定时器【"+timerName+"】取消---"); + } + + } + } + + public interface IRxNext { + void doNext(long number, String timerName); + } + public interface ITimer { + void doNext(long number, boolean complete); + } +} diff --git a/app/src/main/java/com/example/baseframe/bus/event/SingleLiveEvent.java b/app/src/main/java/com/example/baseframe/bus/event/SingleLiveEvent.java new file mode 100644 index 0000000..5bd6028 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/event/SingleLiveEvent.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.bus.event; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + */ +public class SingleLiveEvent extends MutableLiveData { + + private static final String TAG = "SingleLiveEvent"; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(LifecycleOwner owner, final Observer observer) { + + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + } + + // Observe the internal MutableLiveData + super.observe(owner, new Observer() { + @Override + public void onChanged(@Nullable T t) { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t); + } + } + }); + } + + @MainThread + public void setValue(@Nullable T t) { + mPending.set(true); + super.setValue(t); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } +} diff --git a/app/src/main/java/com/example/baseframe/bus/event/SnackbarMessage.java b/app/src/main/java/com/example/baseframe/bus/event/SnackbarMessage.java new file mode 100644 index 0000000..2737c97 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/bus/event/SnackbarMessage.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.bus.event; + +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +/** + * A SingleLiveEvent used for Snackbar messages. Like a {@link SingleLiveEvent} but also prevents + * null messages and uses a custom observer. + *

+ * Note that only one observer is going to be notified of changes. + */ +public class SnackbarMessage extends SingleLiveEvent { + + public void observe(LifecycleOwner owner, final SnackbarObserver observer) { + super.observe(owner, new Observer() { + @Override + public void onChanged(@Nullable Integer t) { + if (t == null) { + return; + } + observer.onNewMessage(t); + } + }); + } + + public interface SnackbarObserver { + /** + * Called when there is a new message to be shown. + * @param snackbarMessageResourceId The new message, non-null. + */ + void onNewMessage(@StringRes int snackbarMessageResourceId); + } + +} diff --git a/app/src/main/java/com/example/baseframe/crash/CaocConfig.java b/app/src/main/java/com/example/baseframe/crash/CaocConfig.java new file mode 100644 index 0000000..de2cb3f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/crash/CaocConfig.java @@ -0,0 +1,300 @@ +/* + * Copyright 2014-2017 Eduard Ereza Martínez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.crash; + +import android.app.Activity; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Modifier; + + +public class CaocConfig implements Serializable { + + @IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT}) + @Retention(RetentionPolicy.SOURCE) + private @interface BackgroundMode { + //I hate empty blocks + } + + public static final int BACKGROUND_MODE_SILENT = 0; + public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1; + public static final int BACKGROUND_MODE_CRASH = 2; + + private int backgroundMode = BACKGROUND_MODE_SHOW_CUSTOM; + private boolean enabled = true; + private boolean showErrorDetails = true; + private boolean showRestartButton = true; + private boolean trackActivities = false; + private int minTimeBetweenCrashesMs = 3000; + private Integer errorDrawable = null; + private Class errorActivityClass = null; + private Class restartActivityClass = null; + private CustomActivityOnCrash.EventListener eventListener = null; + + @BackgroundMode + public int getBackgroundMode() { + return backgroundMode; + } + + public void setBackgroundMode(@BackgroundMode int backgroundMode) { + this.backgroundMode = backgroundMode; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isShowErrorDetails() { + return showErrorDetails; + } + + public void setShowErrorDetails(boolean showErrorDetails) { + this.showErrorDetails = showErrorDetails; + } + + public boolean isShowRestartButton() { + return showRestartButton; + } + + public void setShowRestartButton(boolean showRestartButton) { + this.showRestartButton = showRestartButton; + } + + public boolean isTrackActivities() { + return trackActivities; + } + + public void setTrackActivities(boolean trackActivities) { + this.trackActivities = trackActivities; + } + + public int getMinTimeBetweenCrashesMs() { + return minTimeBetweenCrashesMs; + } + + public void setMinTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) { + this.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs; + } + + @Nullable + @DrawableRes + public Integer getErrorDrawable() { + return errorDrawable; + } + + public void setErrorDrawable(@Nullable @DrawableRes Integer errorDrawable) { + this.errorDrawable = errorDrawable; + } + + @Nullable + public Class getErrorActivityClass() { + return errorActivityClass; + } + + public void setErrorActivityClass(@Nullable Class errorActivityClass) { + this.errorActivityClass = errorActivityClass; + } + + @Nullable + public Class getRestartActivityClass() { + return restartActivityClass; + } + + public void setRestartActivityClass(@Nullable Class restartActivityClass) { + this.restartActivityClass = restartActivityClass; + } + + @Nullable + public CustomActivityOnCrash.EventListener getEventListener() { + return eventListener; + } + + public void setEventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) { + this.eventListener = eventListener; + } + + public static class Builder { + private CaocConfig config; + + @NonNull + public static Builder create() { + Builder builder = new Builder(); + CaocConfig currentConfig = CustomActivityOnCrash.getConfig(); + + CaocConfig config = new CaocConfig(); + config.backgroundMode = currentConfig.backgroundMode; + config.enabled = currentConfig.enabled; + config.showErrorDetails = currentConfig.showErrorDetails; + config.showRestartButton = currentConfig.showRestartButton; + config.trackActivities = currentConfig.trackActivities; + config.minTimeBetweenCrashesMs = currentConfig.minTimeBetweenCrashesMs; + config.errorDrawable = currentConfig.errorDrawable; + config.errorActivityClass = currentConfig.errorActivityClass; + config.restartActivityClass = currentConfig.restartActivityClass; + config.eventListener = currentConfig.eventListener; + + builder.config = config; + + return builder; + } + + /** + * Defines if the error activity must be launched when the app is on background. + * BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM: launch the error activity when the app is in background, + * BackgroundMode.BACKGROUND_MODE_CRASH: launch the default system error when the app is in background, + * BackgroundMode.BACKGROUND_MODE_SILENT: crash silently when the app is in background, + * The default is BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM (the app will be brought to front when a crash occurs). + */ + @NonNull + public Builder backgroundMode(@BackgroundMode int backgroundMode) { + config.backgroundMode = backgroundMode; + return this; + } + + /** + * Defines if CustomActivityOnCrash crash interception mechanism is enabled. + * Set it to true if you want CustomActivityOnCrash to intercept crashes, + * false if you want them to be treated as if the library was not installed. + * The default is true. + */ + @NonNull + public Builder enabled(boolean enabled) { + config.enabled = enabled; + return this; + } + + /** + * Defines if the error activity must shown the error details button. + * Set it to true if you want to show the full stack trace and device info, + * false if you want it to be hidden. + * The default is true. + */ + @NonNull + public Builder showErrorDetails(boolean showErrorDetails) { + config.showErrorDetails = showErrorDetails; + return this; + } + + /** + * Defines if the error activity should show a restart button. + * Set it to true if you want to show a restart button, + * false if you want to show a close button. + * Note that even if restart is enabled but you app does not have any launcher activities, + * a close button will still be used by the default error activity. + * The default is true. + */ + @NonNull + public Builder showRestartButton(boolean showRestartButton) { + config.showRestartButton = showRestartButton; + return this; + } + + /** + * Defines if the activities visited by the user should be tracked + * so they are reported when an error occurs. + * The default is false. + */ + @NonNull + public Builder trackActivities(boolean trackActivities) { + config.trackActivities = trackActivities; + return this; + } + + /** + * Defines the time that must pass between app crashes to determine that we are not + * in a crash loop. If a crash has occurred less that this time ago, + * the error activity will not be launched and the system crash screen will be invoked. + * The default is 3000. + */ + @NonNull + public Builder minTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) { + config.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs; + return this; + } + + /** + * Defines which drawable to use in the default error activity image. + * Set this if you want to use an image other than the default one. + * The default is R.drawable.customactivityoncrash_error_image (a cute upside-down bug). + */ + @NonNull + public Builder errorDrawable(@Nullable @DrawableRes Integer errorDrawable) { + config.errorDrawable = errorDrawable; + return this; + } + + /** + * Sets the error activity class to launch when a crash occurs. + * If null, the default error activity will be used. + */ + @NonNull + public Builder errorActivity(@Nullable Class errorActivityClass) { + config.errorActivityClass = errorActivityClass; + return this; + } + + /** + * Sets the main activity class that the error activity must launch when a crash occurs. + * If not set or set to null, the default launch activity will be used. + * If your app has no launch activities and this is not set, the default error activity will close instead. + */ + @NonNull + public Builder restartActivity(@Nullable Class restartActivityClass) { + config.restartActivityClass = restartActivityClass; + return this; + } + + /** + * Sets an event listener to be called when events occur, so they can be reported + * by the app as, for example, Google Analytics events. + * If not set or set to null, no events will be reported. + * + * @param eventListener The event listener. + * @throws IllegalArgumentException if the eventListener is an inner or anonymous class + */ + @NonNull + public Builder eventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) { + if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) { + throw new IllegalArgumentException("The event listener cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class."); + } else { + config.eventListener = eventListener; + } + return this; + } + + @NonNull + public CaocConfig get() { + return config; + } + + public void apply() { + CustomActivityOnCrash.setConfig(config); + } + } + + +} diff --git a/app/src/main/java/com/example/baseframe/crash/CaocInitProvider.java b/app/src/main/java/com/example/baseframe/crash/CaocInitProvider.java new file mode 100644 index 0000000..fccdcda --- /dev/null +++ b/app/src/main/java/com/example/baseframe/crash/CaocInitProvider.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2017 Eduard Ereza Martínez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.crash; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + + +public class CaocInitProvider extends ContentProvider { + + public boolean onCreate() { + CustomActivityOnCrash.install(getContext()); + return false; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { + return null; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return null; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + +} diff --git a/app/src/main/java/com/example/baseframe/crash/CustomActivityOnCrash.java b/app/src/main/java/com/example/baseframe/crash/CustomActivityOnCrash.java new file mode 100644 index 0000000..3f24c91 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/crash/CustomActivityOnCrash.java @@ -0,0 +1,690 @@ +/* + * Copyright 2014-2017 Eduard Ereza Martínez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.crash; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import android.util.Log; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.lang.ref.WeakReference; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.Date; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + + +public final class CustomActivityOnCrash { + + private final static String TAG = "CustomActivityOnCrash"; + + //Extras passed to the error activity + private static final String EXTRA_CONFIG = "cat.ereza.customactivityoncrash.EXTRA_CONFIG"; + private static final String EXTRA_STACK_TRACE = "cat.ereza.customactivityoncrash.EXTRA_STACK_TRACE"; + private static final String EXTRA_ACTIVITY_LOG = "cat.ereza.customactivityoncrash.EXTRA_ACTIVITY_LOG"; + + //General constants + private static final String INTENT_ACTION_ERROR_ACTIVITY = "cat.ereza.customactivityoncrash.ERROR"; + private static final String INTENT_ACTION_RESTART_ACTIVITY = "cat.ereza.customactivityoncrash.RESTART"; + private static final String CAOC_HANDLER_PACKAGE_NAME = "cat.ereza.customactivityoncrash"; + private static final String DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os"; + private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1 + private static final int MAX_ACTIVITIES_IN_LOG = 50; + + //Shared preferences + private static final String SHARED_PREFERENCES_FILE = "custom_activity_on_crash"; + private static final String SHARED_PREFERENCES_FIELD_TIMESTAMP = "last_crash_timestamp"; + + //Internal variables + @SuppressLint("StaticFieldLeak") //This is an application-wide component + private static Application application; + private static CaocConfig config = new CaocConfig(); + private static Deque activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG); + private static WeakReference lastActivityCreated = new WeakReference<>(null); + private static boolean isInBackground = true; + + + /** + * Installs CustomActivityOnCrash on the application using the default error activity. + * + * @param context Context to use for obtaining the ApplicationContext. Must not be null. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + public static void install(@Nullable final Context context) { + try { + if (context == null) { + Log.e(TAG, "Install failed: context is null!"); + } else { + //INSTALL! + final Thread.UncaughtExceptionHandler oldHandler = Thread.getDefaultUncaughtExceptionHandler(); + + if (oldHandler != null && oldHandler.getClass().getName().startsWith(CAOC_HANDLER_PACKAGE_NAME)) { + Log.e(TAG, "CustomActivityOnCrash was already installed, doing nothing!"); + } else { + if (oldHandler != null && !oldHandler.getClass().getName().startsWith(DEFAULT_HANDLER_PACKAGE_NAME)) { + Log.e(TAG, "IMPORTANT WARNING! You already have an UncaughtExceptionHandler, are you sure this is correct? If you use a custom UncaughtExceptionHandler, you must initialize it AFTER CustomActivityOnCrash! Installing anyway, but your original handler will not be called."); + } + + application = (Application) context.getApplicationContext(); + + //We define a default exception handler that does what we want so it can be called from Crashlytics/ACRA + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, final Throwable throwable) { + if (config.isEnabled()) { + Log.e(TAG, "App has crashed, executing CustomActivityOnCrash's UncaughtExceptionHandler", throwable); + + if (hasCrashedInTheLastSeconds(application)) { + Log.e(TAG, "App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?", throwable); + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); + return; + } + } else { + setLastCrashTimestamp(application, new Date().getTime()); + + Class errorActivityClass = config.getErrorActivityClass(); + + if (errorActivityClass == null) { + errorActivityClass = guessErrorActivityClass(application); + } + + if (isStackTraceLikelyConflictive(throwable, errorActivityClass)) { + Log.e(TAG, "Your application class or your error activity have crashed, the custom activity will not be launched!"); + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); + return; + } + } else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground) { + + final Intent intent = new Intent(application, errorActivityClass); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + String stackTraceString = sw.toString(); + + //Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent. + //The limit is 1MB on Android but some devices seem to have it lower. + //See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html + //And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171 + if (stackTraceString.length() > MAX_STACK_TRACE_SIZE) { + String disclaimer = " [stack trace too large]"; + stackTraceString = stackTraceString.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer; + } + intent.putExtra(EXTRA_STACK_TRACE, stackTraceString); + + if (config.isTrackActivities()) { + String activityLogString = ""; + while (!activityLog.isEmpty()) { + activityLogString += activityLog.poll(); + } + intent.putExtra(EXTRA_ACTIVITY_LOG, activityLogString); + } + + if (config.isShowRestartButton() && config.getRestartActivityClass() == null) { + //We can set the restartActivityClass because the app will terminate right now, + //and when relaunched, will be null again by default. + config.setRestartActivityClass(guessRestartActivityClass(application)); + } + + intent.putExtra(EXTRA_CONFIG, config); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + if (config.getEventListener() != null) { + config.getEventListener().onLaunchErrorActivity(); + } + application.startActivity(intent); + } else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_CRASH) { + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); + return; + } + //If it is null (should not be), we let it continue and kill the process or it will be stuck + } + //Else (BACKGROUND_MODE_SILENT): do nothing and let the following code kill the process + } + final Activity lastActivity = lastActivityCreated.get(); + if (lastActivity != null) { + //We finish the activity, this solves a bug which causes infinite recursion. + //See: https://github.com/ACRA/acra/issues/42 + lastActivity.finish(); + lastActivityCreated.clear(); + } + killCurrentProcess(); + } else if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); + } + } + }); + application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { + int currentlyStartedActivities = 0; + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activity.getClass() != config.getErrorActivityClass()) { + // Copied from ACRA: + // Ignore activityClass because we want the last + // application Activity that was started so that we can + // explicitly kill it off. + lastActivityCreated = new WeakReference<>(activity); + } + if (config.isTrackActivities()) { + activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " created\n"); + } + } + + @Override + public void onActivityStarted(Activity activity) { + currentlyStartedActivities++; + isInBackground = (currentlyStartedActivities == 0); + //Do nothing + } + + @Override + public void onActivityResumed(Activity activity) { + if (config.isTrackActivities()) { + activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " resumed\n"); + } + } + + @Override + public void onActivityPaused(Activity activity) { + if (config.isTrackActivities()) { + activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " paused\n"); + } + } + + @Override + public void onActivityStopped(Activity activity) { + //Do nothing + currentlyStartedActivities--; + isInBackground = (currentlyStartedActivities == 0); + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + //Do nothing + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (config.isTrackActivities()) { + activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " destroyed\n"); + } + } + }); + } + + Log.i(TAG, "CustomActivityOnCrash has been installed."); + } + } catch (Throwable t) { + Log.e(TAG, "An unknown error occurred while installing CustomActivityOnCrash, it may not have been properly initialized. Please report this as a bug if needed.", t); + } + } + + /** + * Given an Intent, returns the stack trace extra from it. + * + * @param intent The Intent. Must not be null. + * @return The stacktrace, or null if not provided. + */ + @NonNull + public static String getStackTraceFromIntent(@NonNull Intent intent) { + return intent.getStringExtra(CustomActivityOnCrash.EXTRA_STACK_TRACE); + } + + /** + * Given an Intent, returns the config extra from it. + * + * @param intent The Intent. Must not be null. + * @return The config, or null if not provided. + */ + @NonNull + public static CaocConfig getConfigFromIntent(@NonNull Intent intent) { + return (CaocConfig) intent.getSerializableExtra(CustomActivityOnCrash.EXTRA_CONFIG); + } + + /** + * Given an Intent, returns the activity log extra from it. + * + * @param intent The Intent. Must not be null. + * @return The activity log, or null if not provided. + */ + @Nullable + public static String getActivityLogFromIntent(@NonNull Intent intent) { + return intent.getStringExtra(CustomActivityOnCrash.EXTRA_ACTIVITY_LOG); + } + + /** + * Given an Intent, returns several error details including the stack trace extra from the intent. + * + * @param context A valid context. Must not be null. + * @param intent The Intent. Must not be null. + * @return The full error details. + */ + @NonNull + public static String getAllErrorDetailsFromIntent(@NonNull Context context, @NonNull Intent intent) { + //I don't think that this needs localization because it's a development string... + + Date currentDate = new Date(); + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); + + //Get build date + String buildDateAsString = getBuildDateAsString(context, dateFormat); + + //Get app version + String versionName = getVersionName(context); + + String errorDetails = ""; + + errorDetails += "Build version: " + versionName + " \n"; + if (buildDateAsString != null) { + errorDetails += "Build date: " + buildDateAsString + " \n"; + } + errorDetails += "Current date: " + dateFormat.format(currentDate) + " \n"; + //Added a space between line feeds to fix #18. + //Ideally, we should not use this method at all... It is only formatted this way because of coupling with the default error activity. + //We should move it to a method that returns a bean, and let anyone format it as they wish. + errorDetails += "Device: " + getDeviceModelName() + " \n \n"; + errorDetails += "Stack trace: \n"; + errorDetails += getStackTraceFromIntent(intent); + + String activityLog = getActivityLogFromIntent(intent); + + if (activityLog != null) { + errorDetails += "\nUser actions: \n"; + errorDetails += activityLog; + } + return errorDetails; + } + + /** + * Given an Intent, restarts the app and launches a startActivity to that intent. + * The flags NEW_TASK and CLEAR_TASK are set if the Intent does not have them, to ensure + * the app stack is fully cleared. + * If an event listener is provided, the restart app event is invoked. + * Must only be used from your error activity. + * + * @param activity The current error activity. Must not be null. + * @param intent The Intent. Must not be null. + * @param config The config object as obtained by calling getConfigFromIntent. + */ + public static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CaocConfig config) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + if (intent.getComponent() != null) { + //If the class name has been set, we force it to simulate a Launcher launch. + //If we don't do this, if you restart from the error activity, then press home, + //and then launch the activity from the launcher, the main activity appears twice on the backstack. + //This will most likely not have any detrimental effect because if you set the Intent component, + //if will always be launched regardless of the actions specified here. + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + } + if (config.getEventListener() != null) { + config.getEventListener().onRestartAppFromErrorActivity(); + } + activity.finish(); + activity.startActivity(intent); + killCurrentProcess(); + } + + public static void restartApplication(@NonNull Activity activity, @NonNull CaocConfig config) { + Intent intent = new Intent(activity, config.getRestartActivityClass()); + restartApplicationWithIntent(activity, intent, config); + } + + /** + * Closes the app. + * If an event listener is provided, the close app event is invoked. + * Must only be used from your error activity. + * + * @param activity The current error activity. Must not be null. + * @param config The config object as obtained by calling getConfigFromIntent. + */ + public static void closeApplication(@NonNull Activity activity, @NonNull CaocConfig config) { + if (config.getEventListener() != null) { + config.getEventListener().onCloseAppFromErrorActivity(); + } + activity.finish(); + killCurrentProcess(); + } + + /// INTERNAL METHODS NOT TO BE USED BY THIRD PARTIES + + /** + * INTERNAL method that returns the current configuration of the library. + * If you want to check the config, use CaocConfig.Builder.get(); + * + * @return the current configuration + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public static CaocConfig getConfig() { + return config; + } + + /** + * INTERNAL method that sets the configuration of the library. + * You must not use this, use CaocConfig.Builder.apply() + * + * @param config the configuration to use + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + public static void setConfig(@NonNull CaocConfig config) { + CustomActivityOnCrash.config = config; + } + + /** + * INTERNAL method that checks if the stack trace that just crashed is conflictive. This is true in the following scenarios: + * - The application has crashed while initializing (handleBindApplication is in the stack) + * - The error activity has crashed (activityClass is in the stack) + * + * @param throwable The throwable from which the stack trace will be checked + * @param activityClass The activity class to launch when the app crashes + * @return true if this stack trace is conflictive and the activity must not be launched, false otherwise + */ + private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwable, @NonNull Class activityClass) { + do { + StackTraceElement[] stackTrace = throwable.getStackTrace(); + for (StackTraceElement element : stackTrace) { + if ((element.getClassName().equals("android.app.ActivityThread") && element.getMethodName().equals("handleBindApplication")) || element.getClassName().equals(activityClass.getName())) { + return true; + } + } + } while ((throwable = throwable.getCause()) != null); + return false; + } + + /** + * INTERNAL method that returns the build date of the current APK as a string, or null if unable to determine it. + * + * @param context A valid context. Must not be null. + * @param dateFormat DateFormat to use to convert from Date to String + * @return The formatted date, or "Unknown" if unable to determine it. + */ + @Nullable + private static String getBuildDateAsString(@NonNull Context context, @NonNull DateFormat dateFormat) { + long buildDate; + try { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + ZipFile zf = new ZipFile(ai.sourceDir); + + //If this failed, try with the old zip method + ZipEntry ze = zf.getEntry("classes.dex"); + buildDate = ze.getTime(); + + + zf.close(); + } catch (Exception e) { + buildDate = 0; + } + + if (buildDate > 312764400000L) { + return dateFormat.format(new Date(buildDate)); + } else { + return null; + } + } + + /** + * INTERNAL method that returns the version name of the current app, or null if unable to determine it. + * + * @param context A valid context. Must not be null. + * @return The version name, or "Unknown if unable to determine it. + */ + @NonNull + private static String getVersionName(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionName; + } catch (Exception e) { + return "Unknown"; + } + } + + /** + * INTERNAL method that returns the device model name with correct capitalization. + * Taken from: http://stackoverflow.com/a/12707479/1254846 + * + * @return The device model name (i.e., "LGE Nexus 5") + */ + @NonNull + private static String getDeviceModelName() { + String manufacturer = Build.MANUFACTURER; + String model = Build.MODEL; + if (model.startsWith(manufacturer)) { + return capitalize(model); + } else { + return capitalize(manufacturer) + " " + model; + } + } + + /** + * INTERNAL method that capitalizes the first character of a string + * + * @param s The string to capitalize + * @return The capitalized string + */ + @NonNull + private static String capitalize(@Nullable String s) { + if (s == null || s.length() == 0) { + return ""; + } + char first = s.charAt(0); + if (Character.isUpperCase(first)) { + return s; + } else { + return Character.toUpperCase(first) + s.substring(1); + } + } + + /** + * INTERNAL method used to guess which activity must be called from the error activity to restart the app. + * It will first get activities from the AndroidManifest with intent filter , + * if it cannot find them, then it will get the default launcher. + * If there is no default launcher, this returns null. + * + * @param context A valid context. Must not be null. + * @return The guessed restart activity class, or null if no suitable one is found + */ + @Nullable + private static Class guessRestartActivityClass(@NonNull Context context) { + Class resolvedActivityClass; + + //If action is defined, use that + resolvedActivityClass = getRestartActivityClassWithIntentFilter(context); + + //Else, get the default launcher activity + if (resolvedActivityClass == null) { + resolvedActivityClass = getLauncherActivity(context); + } + + return resolvedActivityClass; + } + + /** + * INTERNAL method used to get the first activity with an intent-filter , + * If there is no activity with that intent filter, this returns null. + * + * @param context A valid context. Must not be null. + * @return A valid activity class, or null if no suitable one is found + */ + @SuppressWarnings("unchecked") + @Nullable + private static Class getRestartActivityClassWithIntentFilter(@NonNull Context context) { + Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName()); + List resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent, + PackageManager.GET_RESOLVED_FILTER); + + if (resolveInfos != null && resolveInfos.size() > 0) { + ResolveInfo resolveInfo = resolveInfos.get(0); + try { + return (Class) Class.forName(resolveInfo.activityInfo.name); + } catch (ClassNotFoundException e) { + //Should not happen, print it to the log! + Log.e(TAG, "Failed when resolving the restart activity class via intent filter, stack trace follows!", e); + } + } + + return null; + } + + /** + * INTERNAL method used to get the default launcher activity for the app. + * If there is no launchable activity, this returns null. + * + * @param context A valid context. Must not be null. + * @return A valid activity class, or null if no suitable one is found + */ + @SuppressWarnings("unchecked") + @Nullable + private static Class getLauncherActivity(@NonNull Context context) { + Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + if (intent != null) { + try { + return (Class) Class.forName(intent.getComponent().getClassName()); + } catch (ClassNotFoundException e) { + //Should not happen, print it to the log! + Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e); + } + } + + return null; + } + + /** + * INTERNAL method used to guess which error activity must be called when the app crashes. + * It will first get activities from the AndroidManifest with intent filter , + * if it cannot find them, then it will use the default error activity. + * + * @param context A valid context. Must not be null. + * @return The guessed error activity class, or the default error activity if not found + */ + @NonNull + private static Class guessErrorActivityClass(@NonNull Context context) { + Class resolvedActivityClass; + + //If action is defined, use that + resolvedActivityClass = getErrorActivityClassWithIntentFilter(context); + + //Else, get the default error activity + if (resolvedActivityClass == null) { + resolvedActivityClass = DefaultErrorActivity.class; + } + + return resolvedActivityClass; + } + + /** + * INTERNAL method used to get the first activity with an intent-filter , + * If there is no activity with that intent filter, this returns null. + * + * @param context A valid context. Must not be null. + * @return A valid activity class, or null if no suitable one is found + */ + @SuppressWarnings("unchecked") + @Nullable + private static Class getErrorActivityClassWithIntentFilter(@NonNull Context context) { + Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName()); + List resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent, + PackageManager.GET_RESOLVED_FILTER); + + if (resolveInfos != null && resolveInfos.size() > 0) { + ResolveInfo resolveInfo = resolveInfos.get(0); + try { + return (Class) Class.forName(resolveInfo.activityInfo.name); + } catch (ClassNotFoundException e) { + //Should not happen, print it to the log! + Log.e(TAG, "Failed when resolving the error activity class via intent filter, stack trace follows!", e); + } + } + + return null; + } + + /** + * INTERNAL method that kills the current process. + * It is used after restarting or killing the app. + */ + private static void killCurrentProcess() { + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(10); + } + + /** + * INTERNAL method that stores the last crash timestamp + * + * @param timestamp The current timestamp. + */ + @SuppressLint("ApplySharedPref") //This must be done immediately since we are killing the app + private static void setLastCrashTimestamp(@NonNull Context context, long timestamp) { + context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit().putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit(); + } + + /** + * INTERNAL method that gets the last crash timestamp + * + * @return The last crash timestamp, or -1 if not set. + */ + private static long getLastCrashTimestamp(@NonNull Context context) { + return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1); + } + + /** + * INTERNAL method that tells if the app has crashed in the last seconds. + * This is used to avoid restart loops. + * + * @return true if the app has crashed in the last seconds, false otherwise. + */ + private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) { + long lastTimestamp = getLastCrashTimestamp(context); + long currentTimestamp = new Date().getTime(); + + return (lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < config.getMinTimeBetweenCrashesMs()); + } + + /** + * Interface to be called when events occur, so they can be reported + * by the app as, for example, Google Analytics events. + */ + public interface EventListener extends Serializable { + void onLaunchErrorActivity(); + + void onRestartAppFromErrorActivity(); + + void onCloseAppFromErrorActivity(); + } +} diff --git a/app/src/main/java/com/example/baseframe/crash/DefaultErrorActivity.java b/app/src/main/java/com/example/baseframe/crash/DefaultErrorActivity.java new file mode 100644 index 0000000..da65c29 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/crash/DefaultErrorActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014-2017 Eduard Ereza Martínez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.crash; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.core.content.res.ResourcesCompat; +import androidx.appcompat.app.AppCompatActivity; +import android.util.TypedValue; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.baseframe.R; + + +public final class DefaultErrorActivity extends AppCompatActivity { + + @SuppressLint("PrivateResource") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //This is needed to avoid a crash if the developer has not specified + //an app-level theme that extends Theme.AppCompat + TypedArray a = obtainStyledAttributes(R.styleable.AppCompatTheme); + if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) { + setTheme(R.style.Theme_AppCompat_Light_DarkActionBar); + } + a.recycle(); + + setContentView(R.layout.customactivityoncrash_default_error_activity); + + //Close/restart button logic: + //If a class if set, use restart. + //Else, use close and just finish the app. + //It is recommended that you follow this logic if implementing a custom error activity. + Button restartButton = (Button) findViewById(R.id.customactivityoncrash_error_activity_restart_button); + + final CaocConfig config = CustomActivityOnCrash.getConfigFromIntent(getIntent()); + + if (config.isShowRestartButton() && config.getRestartActivityClass()!=null) { + restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app); + restartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + CustomActivityOnCrash.restartApplication(DefaultErrorActivity.this, config); + } + }); + } else { + restartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + CustomActivityOnCrash.closeApplication(DefaultErrorActivity.this, config); + } + }); + } + + Button moreInfoButton = (Button) findViewById(R.id.customactivityoncrash_error_activity_more_info_button); + + if (config.isShowErrorDetails()) { + moreInfoButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //We retrieve all the error data and show it + + AlertDialog dialog = new AlertDialog.Builder(DefaultErrorActivity.this) + .setTitle(R.string.customactivityoncrash_error_activity_error_details_title) + .setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent())) + .setPositiveButton(R.string.customactivityoncrash_error_activity_error_details_close, null) + .setNeutralButton(R.string.customactivityoncrash_error_activity_error_details_copy, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + copyErrorToClipboard(); + Toast.makeText(DefaultErrorActivity.this, R.string.customactivityoncrash_error_activity_error_details_copied, Toast.LENGTH_SHORT).show(); + } + }) + .show(); + TextView textView = (TextView) dialog.findViewById(android.R.id.message); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.customactivityoncrash_error_activity_error_details_text_size)); + } + }); + } else { + moreInfoButton.setVisibility(View.GONE); + } + + Integer defaultErrorActivityDrawableId = config.getErrorDrawable(); + ImageView errorImageView = ((ImageView) findViewById(R.id.customactivityoncrash_error_activity_image)); + + if (defaultErrorActivityDrawableId != null) { + errorImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), defaultErrorActivityDrawableId, getTheme())); + } + } + + private void copyErrorToClipboard() { + String errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent()); + + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation); + clipboard.setPrimaryClip(clip); + } +} diff --git a/app/src/main/java/com/example/baseframe/download/DownLoadManager.java b/app/src/main/java/com/example/baseframe/download/DownLoadManager.java new file mode 100644 index 0000000..35f5ebb --- /dev/null +++ b/app/src/main/java/com/example/baseframe/download/DownLoadManager.java @@ -0,0 +1,92 @@ +package com.example.baseframe.download; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.ResponseBody; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.http.GET; +import retrofit2.http.Streaming; +import retrofit2.http.Url; + +/** + * Created by goldze + * 文件下载管理,封装一行代码实现下载 + */ + +public class DownLoadManager { + private static DownLoadManager instance; + + private static Retrofit retrofit; + + private DownLoadManager() { + buildNetWork(); + } + + + public static DownLoadManager getInstance() { + if (instance == null) { + synchronized (DownLoadManager.class){ + if(instance==null){ + instance = new DownLoadManager(); + } + } + } + return instance; + } + + //下载 + public void load(String downUrl, final ProgressCallBack callBack) { + retrofit.create(ApiService.class) + .download(downUrl) + .subscribeOn(Schedulers.io())//请求网络 在调度者的io线程 + .observeOn(Schedulers.io()) //指定线程保存文件 + .doOnNext(new Consumer() { + @Override + public void accept(ResponseBody responseBody) throws Exception { + callBack.saveFile(responseBody); + } + }) + .observeOn(AndroidSchedulers.mainThread()) //在主线程中更新ui + .subscribe(new DownLoadSubscriber(callBack)); + } + + private void buildNetWork() { + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new ProgressInterceptor()) + .connectTimeout(20, TimeUnit.SECONDS) + .build(); + + retrofit = new Retrofit.Builder() + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .baseUrl("http://www.baidu.com") + .build(); + } + + private interface ApiService { + @Streaming + @GET + Observable download(@Url String url); + } + + public class ProgressInterceptor implements Interceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .body(new ProgressResponseBody(originalResponse.body())) + .build(); + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/download/DownLoadStateBean.java b/app/src/main/java/com/example/baseframe/download/DownLoadStateBean.java new file mode 100644 index 0000000..7ff18d0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/download/DownLoadStateBean.java @@ -0,0 +1,77 @@ +package com.example.baseframe.download; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; + +public class DownLoadStateBean implements Serializable, Parcelable { + long total; // 文件总大小 + long bytesLoaded; //已加载文件的大小 + String tag; // 多任务下载时的一个标记 + + public DownLoadStateBean(long total, long bytesLoaded) { + this.total = total; + this.bytesLoaded = bytesLoaded; + } + + public DownLoadStateBean(long total, long bytesLoaded, String tag) { + this.total = total; + this.bytesLoaded = bytesLoaded; + this.tag = tag; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public long getBytesLoaded() { + return bytesLoaded; + } + + public void setBytesLoaded(long bytesLoaded) { + this.bytesLoaded = bytesLoaded; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(this.total); + dest.writeLong(this.bytesLoaded); + dest.writeString(this.tag); + } + + protected DownLoadStateBean(Parcel in) { + this.total = in.readLong(); + this.bytesLoaded = in.readLong(); + this.tag = in.readString(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public DownLoadStateBean createFromParcel(Parcel source) { + return new DownLoadStateBean(source); + } + + @Override + public DownLoadStateBean[] newArray(int size) { + return new DownLoadStateBean[size]; + } + }; +} diff --git a/app/src/main/java/com/example/baseframe/download/DownLoadSubscriber.java b/app/src/main/java/com/example/baseframe/download/DownLoadSubscriber.java new file mode 100644 index 0000000..e5aaf11 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/download/DownLoadSubscriber.java @@ -0,0 +1,37 @@ +package com.example.baseframe.download; + +import io.reactivex.observers.DisposableObserver; + + +public class DownLoadSubscriber extends DisposableObserver { + private ProgressCallBack fileCallBack; + + public DownLoadSubscriber(ProgressCallBack fileCallBack) { + this.fileCallBack = fileCallBack; + } + + @Override + public void onStart() { + super.onStart(); + if (fileCallBack != null) + fileCallBack.onStart(); + } + + @Override + public void onComplete() { + if (fileCallBack != null) + fileCallBack.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (fileCallBack != null) + fileCallBack.onError(e); + } + + @Override + public void onNext(T t) { + if (fileCallBack != null) + fileCallBack.onSuccess(t); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/download/ProgressCallBack.java b/app/src/main/java/com/example/baseframe/download/ProgressCallBack.java new file mode 100644 index 0000000..194773d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/download/ProgressCallBack.java @@ -0,0 +1,101 @@ +package com.example.baseframe.download; + +import android.util.Log; + + +import com.example.baseframe.rx.RxBus; +import com.example.baseframe.rx.RxBusCode; +import com.example.baseframe.rx.RxSubscriptions; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import okhttp3.ResponseBody; + + +public abstract class ProgressCallBack { + + private String destFileDir; // 本地文件存放路径 + private String destFileName; // 文件名 + private Disposable mSubscription; + + public ProgressCallBack(String destFileDir, String destFileName) { + this.destFileDir = destFileDir; + this.destFileName = destFileName; + subscribeLoadProgress(); + } + + public abstract void onSuccess(T t); + + public abstract void progress(long progress, long total); + + public void onStart() { + } + + public void onCompleted() { + } + + public abstract void onError(Throwable e); + + public void saveFile(ResponseBody body) { + InputStream is = null; + byte[] buf = new byte[2048]; + int len; + FileOutputStream fos = null; + try { + is = body.byteStream(); + File dir = new File(destFileDir); + if (!dir.exists()) { + dir.mkdirs(); + } + File file = new File(dir, destFileName); + fos = new FileOutputStream(file); + while ((len = is.read(buf)) != -1) { + fos.write(buf, 0, len); + } + fos.flush(); + unsubscribe(); + //onCompleted(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) is.close(); + if (fos != null) fos.close(); + } catch (IOException e) { + Log.e("saveFile", e.getMessage()); + } + } + } + + /** + * 订阅加载的进度条 + */ + public void subscribeLoadProgress() { + mSubscription = RxBus.getDefault().toObservable(RxBusCode.TYPE_2,DownLoadStateBean.class) + .observeOn(AndroidSchedulers.mainThread()) //回调到主线程更新UI + .subscribe(new Consumer() { + @Override + public void accept(final DownLoadStateBean progressLoadBean) throws Exception { + progress(progressLoadBean.getBytesLoaded(), progressLoadBean.getTotal()); + } + }); + //将订阅者加入管理站 + RxSubscriptions.add(mSubscription); + } + + /** + * 取消订阅,防止内存泄漏 + */ + public void unsubscribe() { + RxSubscriptions.remove(mSubscription); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/download/ProgressResponseBody.java b/app/src/main/java/com/example/baseframe/download/ProgressResponseBody.java new file mode 100644 index 0000000..6f3aefa --- /dev/null +++ b/app/src/main/java/com/example/baseframe/download/ProgressResponseBody.java @@ -0,0 +1,63 @@ +package com.example.baseframe.download; + +import com.example.baseframe.rx.RxBus; +import com.example.baseframe.rx.RxBusCode; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +public class ProgressResponseBody extends ResponseBody { + private ResponseBody responseBody; + + private BufferedSource bufferedSource; + private String tag; + + public ProgressResponseBody(ResponseBody responseBody) { + this.responseBody = responseBody; + } + + public ProgressResponseBody(ResponseBody responseBody, String tag) { + this.responseBody = responseBody; + this.tag = tag; + } + + @Override + public MediaType contentType() { + return responseBody.contentType(); + } + + @Override + public long contentLength() { + return responseBody.contentLength(); + } + + @Override + public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long bytesReaded = 0; + + @Override + public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + bytesReaded += bytesRead == -1 ? 0 : bytesRead; + //使用RxBus的方式,实时发送当前已读取(上传/下载)的字节数据 + RxBus.getDefault().post(RxBusCode.TYPE_2,new DownLoadStateBean(contentLength(), bytesReaded, tag)); + return bytesRead; + } + }; + } +} diff --git a/app/src/main/java/com/example/baseframe/downloadapk/DownloadAPk.java b/app/src/main/java/com/example/baseframe/downloadapk/DownloadAPk.java new file mode 100644 index 0000000..46049db --- /dev/null +++ b/app/src/main/java/com/example/baseframe/downloadapk/DownloadAPk.java @@ -0,0 +1,230 @@ +package com.example.baseframe.downloadapk; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.RequiresApi; +import androidx.core.content.FileProvider; + +import com.blankj.ALog; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; + + +/** + * 下载工具类(开发中一般用于APK应用升级) + * 需要断点续传的可使用这个库 :https://github.com/ssseasonnn/RxDownload + */ +public class DownloadAPk { + private static int FILE_LEN = 0; + public static String APK_UPGRADE = Environment.getExternalStorageDirectory() + "/DownLoad/apk/downloadApp.apk"; + private static Context mContext; + public static volatile DownloadAPk downloadAPk; + private static DownLoadListener mListener; + public static DownloadAPk getInstance(){ + if(downloadAPk==null){ + synchronized (DownloadAPk.class){ + if(downloadAPk==null){ + downloadAPk=new DownloadAPk(); + } + } + } + return downloadAPk; + } + /** + * 判断8.0 安装权限 + */ + public void downApk(Context context, String url,DownLoadListener mListener) { + mContext = context; + this.mListener=mListener; + if (Build.VERSION.SDK_INT >= 26) { + boolean b = context.getPackageManager().canRequestPackageInstalls(); + if (b) { + downloadAPK(url, null); + } else { + //请求安装未知应用来源的权限 + startInstallPermissionSettingActivity(); + } + } else { + downloadAPK(url, null); + } + } + + + /** + * 开启安装APK权限(适配8.0) + */ + @RequiresApi(api = Build.VERSION_CODES.O) + private void startInstallPermissionSettingActivity() { + Uri packageURI = Uri.parse("package:" + mContext.getPackageName()); + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI); + mContext.startActivity(intent); + } + + /** + * 下载APK文件 + */ + private void downloadAPK(String url, String localAddress) { + // 下载 + if (localAddress != null) { + APK_UPGRADE = localAddress; + } + new UpgradeTask().execute(url); + + } + + static class UpgradeTask extends AsyncTask { + + @Override + protected void onPreExecute() { } + + @Override + protected Void doInBackground(String... params) { + + String apkUrl = params[0]; + InputStream is = null; + FileOutputStream fos = null; + try { + URL url = new URL(apkUrl); + HttpURLConnection conn = (HttpURLConnection) url + .openConnection(); + // 设置连接超时时间 + conn.setConnectTimeout(20000); + // 设置下载数据超时时间 + conn.setReadTimeout(25000); + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + return null;// 服务端错误响应 + } + is = conn.getInputStream(); + FILE_LEN = conn.getContentLength(); + File apkFile = new File(APK_UPGRADE); + // 如果文件夹不存在则创建 + if (!apkFile.getParentFile().exists()) { + apkFile.getParentFile().mkdirs(); + } + fos = new FileOutputStream(apkFile); + byte[] buffer = new byte[8024]; + int len = 0; + int loadedLen = 0;// 当前已下载文件大小 + // 更新100次onProgressUpdate 回調次數 + int updateSize = FILE_LEN / 100; + int num = 0; + while (-1 != (len = is.read(buffer))) { + loadedLen += len; + fos.write(buffer, 0, len); + if (loadedLen > updateSize * num) { + num++; + publishProgress(loadedLen); + } + } + fos.flush(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (SocketTimeoutException e) { + // 处理超时异常,提示用户在网络良好情况下重试 + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return null; + } + + @Override + protected void onProgressUpdate(Integer... values) { + + int progress; + if(values[0] ==FILE_LEN){ + progress=100; + }else{ + progress = values[0] * 100 / FILE_LEN; + //进度显示2位小数: + // double progress= ArithUtils.round((values[0] * 100 / (double) FILE_LEN),2); + } + + Log.w("DownloadAPk",progress + "% 总大小:" + FILE_LEN+"已下载大小:"+values[0]); + mListener.onProgressUpdate(progress); + } + + @Override + protected void onPostExecute(Void result) { + Log.v("DownloadAPk","下载完成"); + mListener.finish(APK_UPGRADE); + + mContext.startActivity(getInstallAppIntent(APK_UPGRADE)); + } + + + } + + + /** + * 调往系统APK安装界面(适配7.0) + * + * @return + */ + public static Intent getInstallAppIntent(String filePath) { + //apk文件的本地路径 + File apkfile = new File(filePath); + if (!apkfile.exists()) { + return null; + } + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri contentUri = getUriForFile(apkfile); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); + return intent; + } + + /** + * 将文件转换成uri + * + * @return + */ + public static Uri getUriForFile(File file) { + ALog.v(file.getPath()); + Uri fileUri = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + fileUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", file); + } else { + fileUri = Uri.fromFile(file); + } + ALog.v(fileUri.toString()); + return fileUri; + } + + public interface DownLoadListener{ + void onProgressUpdate(int progress); + void finish(String filePath); + } +} diff --git a/app/src/main/java/com/example/baseframe/entity/DemoEntity.java b/app/src/main/java/com/example/baseframe/entity/DemoEntity.java new file mode 100644 index 0000000..5e582bd --- /dev/null +++ b/app/src/main/java/com/example/baseframe/entity/DemoEntity.java @@ -0,0 +1,174 @@ +package com.example.baseframe.entity; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * Created by goldze on 2017/7/17. + */ + +public class DemoEntity { + private String nextPageToken; + private String prevPageToken; + private int requestCount; + private int responseCount; + private int totalResults; + private List items; + + public String getNextPageToken() { + return nextPageToken; + } + + public void setNextPageToken(String nextPageToken) { + this.nextPageToken = nextPageToken; + } + + public String getPrevPageToken() { + return prevPageToken; + } + + public void setPrevPageToken(String prevPageToken) { + this.prevPageToken = prevPageToken; + } + + public int getRequestCount() { + return requestCount; + } + + public void setRequestCount(int requestCount) { + this.requestCount = requestCount; + } + + public int getResponseCount() { + return responseCount; + } + + public void setResponseCount(int responseCount) { + this.responseCount = responseCount; + } + + public int getTotalResults() { + return totalResults; + } + + public void setTotalResults(int totalResults) { + this.totalResults = totalResults; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public static class ItemsEntity implements Parcelable { + private String detail; + private String href; + private int id; + private String img; + private String name; + private String pubDate; + private int type; + + public String getDetail() { + return detail; + } + + public void setDetail(String detail) { + this.detail = detail; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getImg() { + return img; + } + + public void setImg(String img) { + this.img = img; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPubDate() { + return pubDate; + } + + public void setPubDate(String pubDate) { + this.pubDate = pubDate; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.detail); + dest.writeString(this.href); + dest.writeInt(this.id); + dest.writeString(this.img); + dest.writeString(this.name); + dest.writeString(this.pubDate); + dest.writeInt(this.type); + } + + public ItemsEntity() { + } + + protected ItemsEntity(Parcel in) { + this.detail = in.readString(); + this.href = in.readString(); + this.id = in.readInt(); + this.img = in.readString(); + this.name = in.readString(); + this.pubDate = in.readString(); + this.type = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ItemsEntity createFromParcel(Parcel source) { + return new ItemsEntity(source); + } + + @Override + public ItemsEntity[] newArray(int size) { + return new ItemsEntity[size]; + } + }; + } +} diff --git a/app/src/main/java/com/example/baseframe/http/BaseResponse.java b/app/src/main/java/com/example/baseframe/http/BaseResponse.java new file mode 100644 index 0000000..858acde --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/BaseResponse.java @@ -0,0 +1,39 @@ +package com.example.baseframe.http; + +/** + * Created by goldze on 2017/5/10. + * 该类仅供参考,实际业务返回的固定字段, 根据需求来定义, + */ +public class BaseResponse { + private int code; + private String message; + private T result; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public T getResult() { + return result; + } + + public void setResult(T result) { + this.result = result; + } + + public boolean isOk() { + return code == 0; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/app/src/main/java/com/example/baseframe/http/ExceptionHandle.java b/app/src/main/java/com/example/baseframe/http/ExceptionHandle.java new file mode 100644 index 0000000..2363872 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/ExceptionHandle.java @@ -0,0 +1,124 @@ +package com.example.baseframe.http; + +import android.net.ParseException; + +import com.google.gson.JsonParseException; +import com.google.gson.stream.MalformedJsonException; + +import org.apache.http.conn.ConnectTimeoutException; +import org.json.JSONException; + +import java.net.ConnectException; + +import retrofit2.HttpException; + + +/** + * Created by goldze on 2017/5/11. + */ +public class ExceptionHandle { + + private static final int UNAUTHORIZED = 401; + private static final int FORBIDDEN = 403; + private static final int NOT_FOUND = 404; + private static final int REQUEST_TIMEOUT = 408; + private static final int INTERNAL_SERVER_ERROR = 500; + private static final int SERVICE_UNAVAILABLE = 503; + + public static ResponseThrowable handleException(Throwable e) { + ResponseThrowable ex; + if (e instanceof HttpException) { + HttpException httpException = (HttpException) e; + ex = new ResponseThrowable(e, ERROR.HTTP_ERROR); + switch (httpException.code()) { + case UNAUTHORIZED: + ex.message = "操作未授权"; + break; + case FORBIDDEN: + ex.message = "请求被拒绝"; + break; + case NOT_FOUND: + ex.message = "资源不存在"; + break; + case REQUEST_TIMEOUT: + ex.message = "服务器执行超时"; + break; + case INTERNAL_SERVER_ERROR: + ex.message = "服务器内部错误"; + break; + case SERVICE_UNAVAILABLE: + ex.message = "服务器不可用"; + break; + default: + ex.message = "网络错误"; + break; + } + return ex; + } else if (e instanceof JsonParseException + || e instanceof JSONException + || e instanceof ParseException || e instanceof MalformedJsonException) { + ex = new ResponseThrowable(e, ERROR.PARSE_ERROR); + ex.message = "解析错误"; + return ex; + } else if (e instanceof ConnectException) { + ex = new ResponseThrowable(e, ERROR.NETWORD_ERROR); + ex.message = "连接失败"; + return ex; + } else if (e instanceof javax.net.ssl.SSLException) { + ex = new ResponseThrowable(e, ERROR.SSL_ERROR); + ex.message = "证书验证失败"; + return ex; + } else if (e instanceof ConnectTimeoutException) { + ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR); + ex.message = "连接超时"; + return ex; + } else if (e instanceof java.net.SocketTimeoutException) { + ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR); + ex.message = "连接超时"; + return ex; + } else if (e instanceof java.net.UnknownHostException) { + ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR); + ex.message = "主机地址未知"; + return ex; + } else { + ex = new ResponseThrowable(e, ERROR.UNKNOWN); + ex.message = "未知错误"; + return ex; + } + } + + + /** + * 约定异常 这个具体规则需要与服务端或者领导商讨定义 + */ + class ERROR { + /** + * 未知错误 + */ + public static final int UNKNOWN = 1000; + /** + * 解析错误 + */ + public static final int PARSE_ERROR = 1001; + /** + * 网络错误 + */ + public static final int NETWORD_ERROR = 1002; + /** + * 协议出错 + */ + public static final int HTTP_ERROR = 1003; + + /** + * 证书出错 + */ + public static final int SSL_ERROR = 1005; + + /** + * 连接超时 + */ + public static final int TIMEOUT_ERROR = 1006; + } + +} + diff --git a/app/src/main/java/com/example/baseframe/http/NetworkUtil.java b/app/src/main/java/com/example/baseframe/http/NetworkUtil.java new file mode 100644 index 0000000..31b1666 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/NetworkUtil.java @@ -0,0 +1,178 @@ +package com.example.baseframe.http; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.util.Enumeration; + +public class NetworkUtil { + public static String url = "http://www.baidu.com"; + public static int NET_CNNT_BAIDU_OK = 1; // NetworkAvailable + public static int NET_CNNT_BAIDU_TIMEOUT = 2; // no NetworkAvailable + public static int NET_NOT_PREPARE = 3; // Net no ready + public static int NET_ERROR = 4; //net error + private static int TIMEOUT = 3000; // TIMEOUT + + + /** + * check NetworkAvailable + * @param context + * @return + */ + public static boolean isNetworkAvailable(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getApplicationContext().getSystemService( + Context.CONNECTIVITY_SERVICE); + if (null == manager) + return false; + NetworkInfo info = manager.getActiveNetworkInfo(); + if (null == info || !info.isAvailable()) + return false; + return true; + } + + /** + * getLocalIpAddress + * @return + */ + public static String getLocalIpAddress() { + String ret = ""; + try { + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { + NetworkInterface intf = en.nextElement(); + for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress()) { + ret = inetAddress.getHostAddress().toString(); + } + } + } + } catch (SocketException ex) { + ex.printStackTrace(); + } + return ret; + } + + /** + * 返回当前网络状态 + * + * @param context + * @return + */ + public static int getNetState(Context context) { + try { + ConnectivityManager connectivity = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity != null) { + NetworkInfo networkinfo = connectivity.getActiveNetworkInfo(); + if (networkinfo != null) { + if (networkinfo.isAvailable() && networkinfo.isConnected()) { + if (!connectionNetwork()) + return NET_CNNT_BAIDU_TIMEOUT; + else + return NET_CNNT_BAIDU_OK; + } else { + return NET_NOT_PREPARE; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return NET_ERROR; + } + + /** + *ping "http://www.baidu.com" + * @return + */ + static private boolean connectionNetwork() { + boolean result = false; + HttpURLConnection httpUrl = null; + try { + httpUrl = (HttpURLConnection) new URL(url) + .openConnection(); + httpUrl.setConnectTimeout(TIMEOUT); + httpUrl.connect(); + result = true; + } catch (IOException e) { + } finally { + if (null != httpUrl) { + httpUrl.disconnect(); + } + httpUrl = null; + } + return result; + } + + /** + * check is3G + * @param context + * @return boolean + */ + public static boolean is3G(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); + if (activeNetInfo != null + && activeNetInfo.getType() == ConnectivityManager.TYPE_MOBILE) { + return true; + } + return false; + } + + /** + * isWifi + * @param context + * @return boolean + */ + public static boolean isWifi(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); + if (activeNetInfo != null + && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return false; + } + + /** + * is2G + * @param context + * @return boolean + */ + public static boolean is2G(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); + if (activeNetInfo != null + && (activeNetInfo.getSubtype() == TelephonyManager.NETWORK_TYPE_EDGE + || activeNetInfo.getSubtype() == TelephonyManager.NETWORK_TYPE_GPRS || activeNetInfo + .getSubtype() == TelephonyManager.NETWORK_TYPE_CDMA)) { + return true; + } + return false; + } + + /** + * is wifi on + */ + public static boolean isWifiEnabled(Context context) { + ConnectivityManager mgrConn = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + TelephonyManager mgrTel = (TelephonyManager) context + .getSystemService(Context.TELEPHONY_SERVICE); + return ((mgrConn.getActiveNetworkInfo() != null && mgrConn + .getActiveNetworkInfo().getState() == NetworkInfo.State.CONNECTED) || mgrTel + .getNetworkType() == TelephonyManager.NETWORK_TYPE_UMTS); + } + +} diff --git a/app/src/main/java/com/example/baseframe/http/ResponseThrowable.java b/app/src/main/java/com/example/baseframe/http/ResponseThrowable.java new file mode 100644 index 0000000..bdceaaa --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/ResponseThrowable.java @@ -0,0 +1,15 @@ +package com.example.baseframe.http; + +/** + * Created by goldze on 2017/5/11. + */ + +public class ResponseThrowable extends Exception { + public int code; + public String message; + + public ResponseThrowable(Throwable throwable, int code) { + super(throwable); + this.code = code; + } +} diff --git a/app/src/main/java/com/example/baseframe/http/cookie/CookieJarImpl.java b/app/src/main/java/com/example/baseframe/http/cookie/CookieJarImpl.java new file mode 100644 index 0000000..a55f3f3 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/cookie/CookieJarImpl.java @@ -0,0 +1,38 @@ +package com.example.baseframe.http.cookie; + + +import java.util.List; + +import com.example.baseframe.http.cookie.store.CookieStore; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +/** + * Created by goldze on 2017/5/13. + */ +public class CookieJarImpl implements CookieJar { + + private CookieStore cookieStore; + + public CookieJarImpl(CookieStore cookieStore) { + if (cookieStore == null) { + throw new IllegalArgumentException("cookieStore can not be null!"); + } + this.cookieStore = cookieStore; + } + + @Override + public synchronized void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.saveCookie(url, cookies); + } + + @Override + public synchronized List loadForRequest(HttpUrl url) { + return cookieStore.loadCookie(url); + } + + public CookieStore getCookieStore() { + return cookieStore; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/http/cookie/store/CookieStore.java b/app/src/main/java/com/example/baseframe/http/cookie/store/CookieStore.java new file mode 100644 index 0000000..7afda69 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/cookie/store/CookieStore.java @@ -0,0 +1,36 @@ +package com.example.baseframe.http.cookie.store; + +import java.util.List; + +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +/** + * Created by goldze on 2017/5/13. + */ +public interface CookieStore { + + /** 保存url对应所有cookie */ + void saveCookie(HttpUrl url, List cookie); + + /** 保存url对应所有cookie */ + void saveCookie(HttpUrl url, Cookie cookie); + + /** 加载url所有的cookie */ + List loadCookie(HttpUrl url); + + /** 获取当前所有保存的cookie */ + List getAllCookie(); + + /** 获取当前url对应的所有的cookie */ + List getCookie(HttpUrl url); + + /** 根据url和cookie移除对应的cookie */ + boolean removeCookie(HttpUrl url, Cookie cookie); + + /** 根据url移除所有的cookie */ + boolean removeCookie(HttpUrl url); + + /** 移除所有的cookie */ + boolean removeAllCookie(); +} diff --git a/app/src/main/java/com/example/baseframe/http/cookie/store/MemoryCookieStore.java b/app/src/main/java/com/example/baseframe/http/cookie/store/MemoryCookieStore.java new file mode 100644 index 0000000..6db8f1a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/cookie/store/MemoryCookieStore.java @@ -0,0 +1,90 @@ +package com.example.baseframe.http.cookie.store; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +/** + * Created by goldze on 2017/5/13. + */ +public class MemoryCookieStore implements CookieStore { + + private final HashMap> memoryCookies = new HashMap<>(); + + @Override + public synchronized void saveCookie(HttpUrl url, List cookies) { + List oldCookies = memoryCookies.get(url.host()); + List needRemove = new ArrayList<>(); + for (Cookie newCookie : cookies) { + for (Cookie oldCookie : oldCookies) { + if (newCookie.name().equals(oldCookie.name())) { + needRemove.add(oldCookie); + } + } + } + oldCookies.removeAll(needRemove); + oldCookies.addAll(cookies); + } + + @Override + public synchronized void saveCookie(HttpUrl url, Cookie cookie) { + List cookies = memoryCookies.get(url.host()); + List needRemove = new ArrayList<>(); + for (Cookie item : cookies) { + if (cookie.name().equals(item.name())) { + needRemove.add(item); + } + } + cookies.removeAll(needRemove); + cookies.add(cookie); + } + + @Override + public synchronized List loadCookie(HttpUrl url) { + List cookies = memoryCookies.get(url.host()); + if (cookies == null) { + cookies = new ArrayList<>(); + memoryCookies.put(url.host(), cookies); + } + return cookies; + } + + @Override + public synchronized List getAllCookie() { + List cookies = new ArrayList<>(); + Set httpUrls = memoryCookies.keySet(); + for (String url : httpUrls) { + cookies.addAll(memoryCookies.get(url)); + } + return cookies; + } + + @Override + public List getCookie(HttpUrl url) { + List cookies = new ArrayList<>(); + List urlCookies = memoryCookies.get(url.host()); + if (urlCookies != null) cookies.addAll(urlCookies); + return cookies; + } + + @Override + public synchronized boolean removeCookie(HttpUrl url, Cookie cookie) { + List cookies = memoryCookies.get(url.host()); + return (cookie != null) && cookies.remove(cookie); + } + + @Override + public synchronized boolean removeCookie(HttpUrl url) { + return memoryCookies.remove(url.host()) != null; + } + + @Override + public synchronized boolean removeAllCookie() { + memoryCookies.clear(); + return true; + } +} diff --git a/app/src/main/java/com/example/baseframe/http/cookie/store/PersistentCookieStore.java b/app/src/main/java/com/example/baseframe/http/cookie/store/PersistentCookieStore.java new file mode 100644 index 0000000..bc7723b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/cookie/store/PersistentCookieStore.java @@ -0,0 +1,270 @@ +package com.example.baseframe.http.cookie.store; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +/** + * Created by goldze on 2017/5/13. + */ +public class PersistentCookieStore implements CookieStore { + + private static final String LOG_TAG = "PersistentCookieStore"; + private static final String COOKIE_PREFS = "habit_cookie"; //cookie使用prefs保存 + private static final String COOKIE_NAME_PREFIX = "cookie_"; //cookie持久化的统一前缀 + + private final HashMap> cookies; + private final SharedPreferences cookiePrefs; + + public PersistentCookieStore(Context context) { + cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE); + cookies = new HashMap<>(); + + //将持久化的cookies缓存到内存中,数据结构为 Map> + Map prefsMap = cookiePrefs.getAll(); + for (Map.Entry entry : prefsMap.entrySet()) { + if ((entry.getValue()) != null && !entry.getKey().startsWith(COOKIE_NAME_PREFIX)) { + //获取url对应的所有cookie的key,用","分割 + String[] cookieNames = TextUtils.split((String) entry.getValue(), ","); + for (String name : cookieNames) { + //根据对应cookie的Key,从xml中获取cookie的真实值 + String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null); + if (encodedCookie != null) { + Cookie decodedCookie = decodeCookie(encodedCookie); + if (decodedCookie != null) { + if (!cookies.containsKey(entry.getKey())) cookies.put(entry.getKey(), new ConcurrentHashMap()); + cookies.get(entry.getKey()).put(name, decodedCookie); + } + } + } + } + } + } + + private String getCookieToken(Cookie cookie) { + return cookie.name() + "@" + cookie.domain(); + } + + /** 当前cookie是否过期 */ + private static boolean isCookieExpired(Cookie cookie) { + return cookie.expiresAt() < System.currentTimeMillis(); + } + + /** 根据当前url获取所有需要的cookie,只返回没有过期的cookie */ + @Override + public List loadCookie(HttpUrl url) { + ArrayList ret = new ArrayList<>(); + if (cookies.containsKey(url.host())) { + Collection urlCookies = cookies.get(url.host()).values(); + for (Cookie cookie : urlCookies) { + if (isCookieExpired(cookie)) { + removeCookie(url, cookie); + } else { + ret.add(cookie); + } + } + } + return ret; + } + + /** 将url的所有Cookie保存在本地 */ + @Override + public void saveCookie(HttpUrl url, List urlCookies) { + if (!cookies.containsKey(url.host())) { + cookies.put(url.host(), new ConcurrentHashMap()); + } + for (Cookie cookie : urlCookies) { + //当前cookie是否过期 + if (isCookieExpired(cookie)) { + removeCookie(url, cookie); + } else { + saveCookie(url, cookie, getCookieToken(cookie)); + } + } + } + + @Override + public void saveCookie(HttpUrl url, Cookie cookie) { + if (!cookies.containsKey(url.host())) { + cookies.put(url.host(), new ConcurrentHashMap()); + } + //当前cookie是否过期 + if (isCookieExpired(cookie)) { + removeCookie(url, cookie); + } else { + saveCookie(url, cookie, getCookieToken(cookie)); + } + } + + /** + * 保存cookie,并将cookies持久化到本地,数据结构为 + * Url.host -> Cookie1.name,Cookie2.name,Cookie3.name + * cookie_Cookie1.name -> CookieString + * cookie_Cookie2.name -> CookieString + */ + private void saveCookie(HttpUrl url, Cookie cookie, String name) { + //内存缓存 + cookies.get(url.host()).put(name, cookie); + //文件缓存 + SharedPreferences.Editor prefsWriter = cookiePrefs.edit(); + prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet())); + prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie))); + prefsWriter.apply(); + } + + /** 根据url移除当前的cookie */ + @Override + public boolean removeCookie(HttpUrl url, Cookie cookie) { + String name = getCookieToken(cookie); + if (cookies.containsKey(url.host()) && cookies.get(url.host()).containsKey(name)) { + //内存移除 + cookies.get(url.host()).remove(name); + //文件移除 + SharedPreferences.Editor prefsWriter = cookiePrefs.edit(); + if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) { + prefsWriter.remove(COOKIE_NAME_PREFIX + name); + } + prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet())); + prefsWriter.apply(); + return true; + } else { + return false; + } + } + + @Override + public boolean removeCookie(HttpUrl url) { + if (cookies.containsKey(url.host())) { + //文件移除 + Set cookieNames = cookies.get(url.host()).keySet(); + SharedPreferences.Editor prefsWriter = cookiePrefs.edit(); + for (String cookieName : cookieNames) { + if (cookiePrefs.contains(COOKIE_NAME_PREFIX + cookieName)) { + prefsWriter.remove(COOKIE_NAME_PREFIX + cookieName); + } + } + prefsWriter.remove(url.host()).apply(); + //内存移除 + cookies.remove(url.host()); + return true; + } else { + return false; + } + } + + @Override + public boolean removeAllCookie() { + SharedPreferences.Editor prefsWriter = cookiePrefs.edit(); + prefsWriter.clear().apply(); + cookies.clear(); + return true; + } + + /** 获取所有的cookie */ + @Override + public List getAllCookie() { + List ret = new ArrayList<>(); + for (String key : cookies.keySet()) + ret.addAll(cookies.get(key).values()); + return ret; + } + + @Override + public List getCookie(HttpUrl url) { + List ret = new ArrayList<>(); + Map mapCookie = cookies.get(url.host()); + if (mapCookie != null) ret.addAll(mapCookie.values()); + return ret; + } + + /** + * cookies 序列化成 string + * + * @param cookie 要序列化的cookie + * @return 序列化之后的string + */ + private String encodeCookie(SerializableHttpCookie cookie) { + if (cookie == null) return null; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ObjectOutputStream outputStream = new ObjectOutputStream(os); + outputStream.writeObject(cookie); + } catch (IOException e) { + Log.d(LOG_TAG, "IOException in encodeCookie", e); + return null; + } + return byteArrayToHexString(os.toByteArray()); + } + + /** + * 将字符串反序列化成cookies + * + * @param cookieString cookies string + * @return cookie object + */ + private Cookie decodeCookie(String cookieString) { + byte[] bytes = hexStringToByteArray(cookieString); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + Cookie cookie = null; + try { + ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); + cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie(); + } catch (IOException e) { + Log.d(LOG_TAG, "IOException in decodeCookie", e); + } catch (ClassNotFoundException e) { + Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e); + } + return cookie; + } + + /** + * 二进制数组转十六进制字符串 + * + * @param bytes byte array to be converted + * @return string containing hex values + */ + private String byteArrayToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte element : bytes) { + int v = element & 0xff; + if (v < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(v)); + } + return sb.toString().toUpperCase(Locale.US); + } + + /** + * 十六进制字符串转二进制数组 + * + * @param hexString string of hex-encoded values + * @return decoded byte array + */ + private byte[] hexStringToByteArray(String hexString) { + int len = hexString.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16)); + } + return data; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/http/cookie/store/SerializableHttpCookie.java b/app/src/main/java/com/example/baseframe/http/cookie/store/SerializableHttpCookie.java new file mode 100644 index 0000000..7208f1b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/cookie/store/SerializableHttpCookie.java @@ -0,0 +1,60 @@ +package com.example.baseframe.http.cookie.store; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import okhttp3.Cookie; + +public class SerializableHttpCookie implements Serializable { + private static final long serialVersionUID = 6374381323722046732L; + + private transient final Cookie cookie; + private transient Cookie clientCookie; + + public SerializableHttpCookie(Cookie cookie) { + this.cookie = cookie; + } + + public Cookie getCookie() { + Cookie bestCookie = cookie; + if (clientCookie != null) { + bestCookie = clientCookie; + } + return bestCookie; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeObject(cookie.name()); + out.writeObject(cookie.value()); + out.writeLong(cookie.expiresAt()); + out.writeObject(cookie.domain()); + out.writeObject(cookie.path()); + out.writeBoolean(cookie.secure()); + out.writeBoolean(cookie.httpOnly()); + out.writeBoolean(cookie.hostOnly()); + out.writeBoolean(cookie.persistent()); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + String name = (String) in.readObject(); + String value = (String) in.readObject(); + long expiresAt = in.readLong(); + String domain = (String) in.readObject(); + String path = (String) in.readObject(); + boolean secure = in.readBoolean(); + boolean httpOnly = in.readBoolean(); + boolean hostOnly = in.readBoolean(); + boolean persistent = in.readBoolean(); + Cookie.Builder builder = new Cookie.Builder(); + builder = builder.name(name); + builder = builder.value(value); + builder = builder.expiresAt(expiresAt); + builder = hostOnly ? builder.hostOnlyDomain(domain) : builder.domain(domain); + builder = builder.path(path); + builder = secure ? builder.secure() : builder; + builder = httpOnly ? builder.httpOnly() : builder; + clientCookie = builder.build(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/BaseInterceptor.java b/app/src/main/java/com/example/baseframe/http/interceptor/BaseInterceptor.java new file mode 100644 index 0000000..6a330f3 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/BaseInterceptor.java @@ -0,0 +1,34 @@ +package com.example.baseframe.http.interceptor; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by goldze on 2017/5/10. + */ +public class BaseInterceptor implements Interceptor { + private Map headers; + + public BaseInterceptor(Map headers) { + this.headers = headers; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request() + .newBuilder(); + if (headers != null && headers.size() > 0) { + Set keys = headers.keySet(); + for (String headerKey : keys) { + builder.addHeader(headerKey, headers.get(headerKey)).build(); + } + } + //请求信息 + return chain.proceed(builder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/CacheInterceptor.java b/app/src/main/java/com/example/baseframe/http/interceptor/CacheInterceptor.java new file mode 100644 index 0000000..cfbeaa4 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/CacheInterceptor.java @@ -0,0 +1,52 @@ +package com.example.baseframe.http.interceptor; + +import android.content.Context; + +import java.io.IOException; + +import com.example.baseframe.http.NetworkUtil; +import okhttp3.CacheControl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by goldze on 2017/5/10. + * 无网络状态下智能读取缓存的拦截器 + */ +public class CacheInterceptor implements Interceptor { + + private Context context; + + public CacheInterceptor(Context context) { + this.context = context; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (NetworkUtil.isNetworkAvailable(context)) { + Response response = chain.proceed(request); + // read from cache for 60 s + int maxAge = 60; + return response.newBuilder() + .removeHeader("Pragma") + .removeHeader("Cache-Control") + .header("Cache-Control", "public, max-age=" + maxAge) + .build(); + } else { + //读取缓存信息 + request = request.newBuilder() + .cacheControl(CacheControl.FORCE_CACHE) + .build(); + Response response = chain.proceed(request); + //set cache times is 3 days + int maxStale = 60 * 60 * 24 * 3; + return response.newBuilder() + .removeHeader("Pragma") + .removeHeader("Cache-Control") + .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) + .build(); + } + } +} diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/logging/I.java b/app/src/main/java/com/example/baseframe/http/interceptor/logging/I.java new file mode 100644 index 0000000..2a7d23c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/logging/I.java @@ -0,0 +1,28 @@ +package com.example.baseframe.http.interceptor.logging; + + +import java.util.logging.Level; + +import okhttp3.internal.platform.Platform; + +/** + * @author ihsan on 10/02/2017. + */ +class I { + + protected I() { + throw new UnsupportedOperationException(); + } + + static void log(int type, String tag, String msg) { + java.util.logging.Logger logger = java.util.logging.Logger.getLogger(tag); + switch (type) { + case Platform.INFO: + logger.log(Level.INFO, msg); + break; + default: + logger.log(Level.WARNING, msg); + break; + } + } +} diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/logging/Level.java b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Level.java new file mode 100644 index 0000000..6b2248e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Level.java @@ -0,0 +1,40 @@ +package com.example.baseframe.http.interceptor.logging; + +/** + * @author ihsan on 21/02/2017. + */ + +public enum Level { + /** + * No logs. + */ + NONE, + /** + *

Example: + *

{@code
+     *  - URL
+     *  - Method
+     *  - Headers
+     *  - Body
+     * }
+ */ + BASIC, + /** + *

Example: + *

{@code
+     *  - URL
+     *  - Method
+     *  - Headers
+     * }
+ */ + HEADERS, + /** + *

Example: + *

{@code
+     *  - URL
+     *  - Method
+     *  - Body
+     * }
+ */ + BODY +} diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/logging/Logger.java b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Logger.java new file mode 100644 index 0000000..291db67 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Logger.java @@ -0,0 +1,18 @@ +package com.example.baseframe.http.interceptor.logging; + +import okhttp3.internal.platform.Platform; + +/** + * @author ihsan on 11/07/2017. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public interface Logger { + void log(int level, String tag, String msg); + + Logger DEFAULT = new Logger() { + @Override + public void log(int level, String tag, String message) { + Platform.get().log(level, message, null); + } + }; +} diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/logging/LoggingInterceptor.java b/app/src/main/java/com/example/baseframe/http/interceptor/logging/LoggingInterceptor.java new file mode 100644 index 0000000..dae15c9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/logging/LoggingInterceptor.java @@ -0,0 +1,235 @@ +package com.example.baseframe.http.interceptor.logging; + +import android.text.TextUtils; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.platform.Platform; + +/** + * @author ihsan on 09/02/2017. + */ + +public class LoggingInterceptor implements Interceptor { + + private boolean isDebug; + private Builder builder; + + private LoggingInterceptor(Builder builder) { + this.builder = builder; + this.isDebug = builder.isDebug; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (builder.getHeaders().size() > 0) { + Headers headers = request.headers(); + Set names = headers.names(); + Iterator iterator = names.iterator(); + Request.Builder requestBuilder = request.newBuilder(); + requestBuilder.headers(builder.getHeaders()); + while (iterator.hasNext()) { + String name = iterator.next(); + requestBuilder.addHeader(name, headers.get(name)); + } + request = requestBuilder.build(); + } + + if (!isDebug || builder.getLevel() == Level.NONE) { + return chain.proceed(request); + } + RequestBody requestBody = request.body(); + + MediaType rContentType = null; + if (requestBody != null) { + rContentType = request.body().contentType(); + } + + String rSubtype = null; + if (rContentType != null) { + rSubtype = rContentType.subtype(); + } + + if (rSubtype != null && (rSubtype.contains("json") + || rSubtype.contains("xml") + || rSubtype.contains("plain") + || rSubtype.contains("html"))) { + Printer.printJsonRequest(builder, request); + } else { + Printer.printFileRequest(builder, request); + } + + long st = System.nanoTime(); + Response response = chain.proceed(request); + + List segmentList = ((Request) request.tag()).url().encodedPathSegments(); + long chainMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - st); + String header = response.headers().toString(); + int code = response.code(); + boolean isSuccessful = response.isSuccessful(); + ResponseBody responseBody = response.body(); + MediaType contentType = responseBody.contentType(); + + String subtype = null; + ResponseBody body; + + if (contentType != null) { + subtype = contentType.subtype(); + } + + if (subtype != null && (subtype.contains("json") + || subtype.contains("xml") + || subtype.contains("plain") + || subtype.contains("html"))) { + String bodyString = responseBody.string(); + String bodyJson = Printer.getJsonString(bodyString); + Printer.printJsonResponse(builder, chainMs, isSuccessful, code, header, bodyJson, segmentList); + body = ResponseBody.create(contentType, bodyString); + } else { + Printer.printFileResponse(builder, chainMs, isSuccessful, code, header, segmentList); + return response; + } + return response.newBuilder().body(body).build(); + } + + @SuppressWarnings("unused") + public static class Builder { + + private static String TAG = "LoggingI"; + private boolean isDebug; + private int type = Platform.INFO; + private String requestTag; + private String responseTag; + private Level level = Level.BASIC; + private Headers.Builder builder; + private Logger logger; + + public Builder() { + builder = new Headers.Builder(); + } + + int getType() { + return type; + } + + Level getLevel() { + return level; + } + + Headers getHeaders() { + return builder.build(); + } + + String getTag(boolean isRequest) { + if (isRequest) { + return TextUtils.isEmpty(requestTag) ? TAG : requestTag; + } else { + return TextUtils.isEmpty(responseTag) ? TAG : responseTag; + } + } + + Logger getLogger() { + return logger; + } + + /** + * @param name Filed + * @param value Value + * @return Builder + * Add a field with the specified value + */ + public Builder addHeader(String name, String value) { + builder.set(name, value); + return this; + } + + /** + * @param level set log level + * @return Builder + * @see Level + */ + public Builder setLevel(Level level) { + this.level = level; + return this; + } + + /** + * Set request and response each log tag + * + * @param tag general log tag + * @return Builder + */ + public Builder tag(String tag) { + TAG = tag; + return this; + } + + /** + * Set request log tag + * + * @param tag request log tag + * @return Builder + */ + public Builder request(String tag) { + this.requestTag = tag; + return this; + } + + /** + * Set response log tag + * + * @param tag response log tag + * @return Builder + */ + public Builder response(String tag) { + this.responseTag = tag; + return this; + } + + /** + * @param isDebug set can sending log output + * @return Builder + */ + public Builder loggable(boolean isDebug) { + this.isDebug = isDebug; + return this; + } + + /** + * @param type set sending log output type + * @return Builder + * @see Platform + */ + public Builder log(int type) { + this.type = type; + return this; + } + + /** + * @param logger manuel logging interface + * @return Builder + * @see Logger + */ + public Builder logger(Logger logger) { + this.logger = logger; + return this; + } + + public LoggingInterceptor build() { + return new LoggingInterceptor(this); + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/http/interceptor/logging/Printer.java b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Printer.java new file mode 100644 index 0000000..d64b767 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/http/interceptor/logging/Printer.java @@ -0,0 +1,232 @@ +package com.example.baseframe.http.interceptor.logging; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.List; + +import okhttp3.FormBody; +import okhttp3.Request; +import okio.Buffer; + +/** + * @author ihsan on 09/02/2017. + */ + +class Printer { + + private static final int JSON_INDENT = 3; + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + private static final String DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR; + + private static final String[] OMITTED_RESPONSE = {LINE_SEPARATOR, "Omitted response body"}; + private static final String[] OMITTED_REQUEST = {LINE_SEPARATOR, "Omitted request body"}; + + private static final String N = "\n"; + private static final String T = "\t"; + private static final String REQUEST_UP_LINE = "┌────── Request ────────────────────────────────────────────────────────────────────────"; + private static final String END_LINE = "└───────────────────────────────────────────────────────────────────────────────────────"; + private static final String RESPONSE_UP_LINE = "┌────── Response ───────────────────────────────────────────────────────────────────────"; + private static final String BODY_TAG = "Body:"; + private static final String URL_TAG = "URL: "; + private static final String METHOD_TAG = "Method: @"; + private static final String HEADERS_TAG = "Headers:"; + private static final String STATUS_CODE_TAG = "Status Code: "; + private static final String RECEIVED_TAG = "Received in: "; + private static final String CORNER_UP = "┌ "; + private static final String CORNER_BOTTOM = "└ "; + private static final String CENTER_LINE = "├ "; + private static final String DEFAULT_LINE = "│ "; + + protected Printer() { + throw new UnsupportedOperationException(); + } + + private static boolean isEmpty(String line) { + return TextUtils.isEmpty(line) || N.equals(line) || T.equals(line) || TextUtils.isEmpty(line.trim()); + } + + static void printJsonRequest(LoggingInterceptor.Builder builder, Request request) { + String requestBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(request); + String tag = builder.getTag(true); + if (builder.getLogger() == null) + I.log(builder.getType(), tag, REQUEST_UP_LINE); + logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false); + logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true); + if (request.body() instanceof FormBody) { + StringBuilder formBody = new StringBuilder(); + FormBody body = (FormBody) request.body(); + if (body != null && body.size() != 0) { + for (int i = 0; i < body.size(); i++) { + formBody.append(body.encodedName(i) + "=" + body.encodedValue(i) + "&"); + } + formBody.delete(formBody.length() - 1, formBody.length()); + logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true); + } + } + if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) { + logLines(builder.getType(), tag, requestBody.split(LINE_SEPARATOR), builder.getLogger(), true); + } + if (builder.getLogger() == null) + I.log(builder.getType(), tag, END_LINE); + } + + static void printJsonResponse(LoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful, + int code, String headers, String bodyString, List segments) { + String responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getJsonString(bodyString); + String tag = builder.getTag(false); + if (builder.getLogger() == null) + I.log(builder.getType(), tag, RESPONSE_UP_LINE); + + logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful, + builder.getLevel(), segments), builder.getLogger(), true); + if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) { + logLines(builder.getType(), tag, responseBody.split(LINE_SEPARATOR), builder.getLogger(), true); + } + if (builder.getLogger() == null) + I.log(builder.getType(), tag, END_LINE); + } + + static void printFileRequest(LoggingInterceptor.Builder builder, Request request) { + String tag = builder.getTag(true); + if (builder.getLogger() == null) + I.log(builder.getType(), tag, REQUEST_UP_LINE); + logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false); + logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true); + if (request.body() instanceof FormBody) { + StringBuilder formBody = new StringBuilder(); + FormBody body = (FormBody) request.body(); + if (body != null && body.size() != 0) { + for (int i = 0; i < body.size(); i++) { + formBody.append(body.encodedName(i) + "=" + body.encodedValue(i) + "&"); + } + formBody.delete(formBody.length() - 1, formBody.length()); + logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true); + } + } + if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) { + logLines(builder.getType(), tag, OMITTED_REQUEST, builder.getLogger(), true); + } + if (builder.getLogger() == null) + I.log(builder.getType(), tag, END_LINE); + } + + static void printFileResponse(LoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful, + int code, String headers, List segments) { + String tag = builder.getTag(false); + if (builder.getLogger() == null) + I.log(builder.getType(), tag, RESPONSE_UP_LINE); + + logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful, + builder.getLevel(), segments), builder.getLogger(), true); + logLines(builder.getType(), tag, OMITTED_RESPONSE, builder.getLogger(), true); + if (builder.getLogger() == null) + I.log(builder.getType(), tag, END_LINE); + } + + private static String[] getRequest(Request request, Level level) { + String message; + String header = request.headers().toString(); + boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC; + message = METHOD_TAG + request.method() + DOUBLE_SEPARATOR + + (isEmpty(header) ? "" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR + dotHeaders(header) : ""); + return message.split(LINE_SEPARATOR); + } + + private static String[] getResponse(String header, long tookMs, int code, boolean isSuccessful, + Level level, List segments) { + String message; + boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC; + String segmentString = slashSegments(segments); + message = ((!TextUtils.isEmpty(segmentString) ? segmentString + " - " : "") + "is success : " + + isSuccessful + " - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG + + code + DOUBLE_SEPARATOR + (isEmpty(header) ? "" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR + + dotHeaders(header) : "")); + return message.split(LINE_SEPARATOR); + } + + private static String slashSegments(List segments) { + StringBuilder segmentString = new StringBuilder(); + for (String segment : segments) { + segmentString.append("/").append(segment); + } + return segmentString.toString(); + } + + private static String dotHeaders(String header) { + String[] headers = header.split(LINE_SEPARATOR); + StringBuilder builder = new StringBuilder(); + String tag = "─ "; + if (headers.length > 1) { + for (int i = 0; i < headers.length; i++) { + if (i == 0) { + tag = CORNER_UP; + } else if (i == headers.length - 1) { + tag = CORNER_BOTTOM; + } else { + tag = CENTER_LINE; + } + builder.append(tag).append(headers[i]).append("\n"); + } + } else { + for (String item : headers) { + builder.append(tag).append(item).append("\n"); + } + } + return builder.toString(); + } + + private static void logLines(int type, String tag, String[] lines, Logger logger, boolean withLineSize) { + for (String line : lines) { + int lineLength = line.length(); + int MAX_LONG_SIZE = withLineSize ? 110 : lineLength; + for (int i = 0; i <= lineLength / MAX_LONG_SIZE; i++) { + int start = i * MAX_LONG_SIZE; + int end = (i + 1) * MAX_LONG_SIZE; + end = end > line.length() ? line.length() : end; + if (logger == null) { + I.log(type, tag, DEFAULT_LINE + line.substring(start, end)); + } else { + logger.log(type, tag, line.substring(start, end)); + } + } + } + } + + private static String bodyToString(final Request request) { + try { + final Request copy = request.newBuilder().build(); + final Buffer buffer = new Buffer(); + if (copy.body() == null) + return ""; + copy.body().writeTo(buffer); + return getJsonString(buffer.readUtf8()); + } catch (final IOException e) { + return "{\"err\": \"" + e.getMessage() + "\"}"; + } + } + + static String getJsonString(String msg) { + String message; + try { + if (msg.startsWith("{")) { + JSONObject jsonObject = new JSONObject(msg); + message = jsonObject.toString(JSON_INDENT); + } else if (msg.startsWith("[")) { + JSONArray jsonArray = new JSONArray(msg); + message = jsonArray.toString(JSON_INDENT); + } else { + message = msg; + } + } catch (JSONException e) { + message = msg; + } + return message; + } + +} diff --git a/app/src/main/java/com/example/baseframe/listener/ClickListener.java b/app/src/main/java/com/example/baseframe/listener/ClickListener.java new file mode 100644 index 0000000..81749bc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/listener/ClickListener.java @@ -0,0 +1,12 @@ +package com.example.baseframe.listener; + +import android.view.View; + +/** + * @Description: Result类作用描述 + * @Author: yzh + * @CreateDate: 2019/11/1 11:43 + */ +public interface ClickListener { + void onResult(View v); +} diff --git a/app/src/main/java/com/example/baseframe/listener/Listener.java b/app/src/main/java/com/example/baseframe/listener/Listener.java new file mode 100644 index 0000000..d473d5e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/listener/Listener.java @@ -0,0 +1,10 @@ +package com.example.baseframe.listener; + +/** + * @Description: Result类作用描述 + * @Author: yzh + * @CreateDate: 2019/11/1 11:43 + */ +public interface Listener { + void onResult(); +} diff --git a/app/src/main/java/com/example/baseframe/listener/ResultCallback.java b/app/src/main/java/com/example/baseframe/listener/ResultCallback.java new file mode 100644 index 0000000..ad4a663 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/listener/ResultCallback.java @@ -0,0 +1,8 @@ +package com.example.baseframe.listener; + +/** + * @author yzh + */ +public interface ResultCallback { + void onResult(T result); +} diff --git a/app/src/main/java/com/example/baseframe/receiver/AppStateReceiver.java b/app/src/main/java/com/example/baseframe/receiver/AppStateReceiver.java new file mode 100644 index 0000000..3793aa9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/AppStateReceiver.java @@ -0,0 +1,138 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + +/** + * detail: 应用状态监听广播 ( 安装、更新、卸载 ) + * @author Ttt + */ +public final class AppStateReceiver extends BroadcastReceiver { + + private AppStateReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = AppStateReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 被操作应用包名 + String packageName = null; + Uri uri = intent.getData(); + if (uri != null) { +// packageName = uri.toString(); + packageName = uri.getEncodedSchemeSpecificPart(); + } + // 判断类型 + switch (action) { + case Intent.ACTION_PACKAGE_ADDED: // 应用安装 + if (sListener != null) { + sListener.onAdded(packageName); + } + break; + case Intent.ACTION_PACKAGE_REPLACED: // 应用更新 + if (sListener != null) { + sListener.onReplaced(packageName); + } + break; + case Intent.ACTION_PACKAGE_REMOVED: // 应用卸载 + if (sListener != null) { + sListener.onRemoved(packageName); + } + break; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // 应用状态监听广播 + private static final AppStateReceiver sReceiver = new AppStateReceiver(); + + /** + * 注册应用状态监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 安装 + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + // 更新 + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + // 卸载 + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册应用状态监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 应用状态监听事件 + private static AppStateListener sListener; + + /** + * 设置应用状态监听事件 + * @param listener {@link AppStateListener} + * @return {@link AppStateReceiver} + */ + public static AppStateReceiver setAppStateListener(final AppStateListener listener) { + AppStateReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 应用状态监听事件 + * @author Ttt + */ + public interface AppStateListener { + + /** + * 应用安装 + * @param packageName 应用包名 + */ + void onAdded(String packageName); + + /** + * 应用更新 + * @param packageName 应用包名 + */ + void onReplaced(String packageName); + + /** + * 应用卸载 + * @param packageName 应用包名 + */ + void onRemoved(String packageName); + } +} diff --git a/app/src/main/java/com/example/baseframe/receiver/BatteryReceiver.java b/app/src/main/java/com/example/baseframe/receiver/BatteryReceiver.java new file mode 100644 index 0000000..5f0e0de --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/BatteryReceiver.java @@ -0,0 +1,166 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 电量监听广播 + * @author Ttt + */ +public final class BatteryReceiver extends BroadcastReceiver { + + private BatteryReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = BatteryReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 获取当前电量, 范围是 0-100 + int level = intent.getIntExtra("level", 0); + // 判断类型 + switch (action) { + case Intent.ACTION_BATTERY_CHANGED: // 电量状态发送改变 + if (sListener != null) { + sListener.onBatteryChanged(level); + } + break; + case Intent.ACTION_BATTERY_LOW: // 电量低 + if (sListener != null) { + sListener.onBatteryLow(level); + } + break; + case Intent.ACTION_BATTERY_OKAY: // 电量从低变回高通知 + if (sListener != null) { + sListener.onBatteryOkay(level); + } + break; + case Intent.ACTION_POWER_CONNECTED: // 连接充电器 + if (sListener != null) { + sListener.onPowerConnected(level, true); + } + break; + case Intent.ACTION_POWER_DISCONNECTED: // 断开充电器 + if (sListener != null) { + sListener.onPowerConnected(level, false); + } + break; + case Intent.ACTION_POWER_USAGE_SUMMARY: // 电力使用情况总结 + if (sListener != null) { + sListener.onPowerUsageSummary(level); + } + break; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // 电量监听广播 + private static final BatteryReceiver sReceiver = new BatteryReceiver(); + + /** + * 注册电量监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 电量状态发送改变 + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + // 电量低 + filter.addAction(Intent.ACTION_BATTERY_LOW); + // 电量从低变回高 + filter.addAction(Intent.ACTION_BATTERY_OKAY); + // 连接充电器 + filter.addAction(Intent.ACTION_POWER_CONNECTED); + // 断开充电器 + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + // 电力使用情况总结 + filter.addAction(Intent.ACTION_POWER_USAGE_SUMMARY); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册电量监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 电量监听事件 + private static BatteryListener sListener; + + /** + * 设置电量监听事件 + * @param listener {@link BatteryListener} + * @return {@link BatteryReceiver} + */ + public static BatteryReceiver setBatteryListener(final BatteryListener listener) { + BatteryReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 电量监听事件 + * @author Ttt + */ + public interface BatteryListener { + + /** + * 电量改变通知 + * @param level 电量百分比 + */ + void onBatteryChanged(int level); + + /** + * 电量低通知 + * @param level 电量百分比 + */ + void onBatteryLow(int level); + + /** + * 电量从低变回高通知 + * @param level 电量百分比 + */ + void onBatteryOkay(int level); + + /** + * 充电状态改变通知 + * @param level 电量百分比 + * @param isConnected 是否充电连接中 + */ + void onPowerConnected(int level, boolean isConnected); + + /** + * 电力使用情况总结 + * @param level 电量百分比 + */ + void onPowerUsageSummary(int level); + } +} diff --git a/app/src/main/java/com/example/baseframe/receiver/NetWorkReceiver.java b/app/src/main/java/com/example/baseframe/receiver/NetWorkReceiver.java new file mode 100644 index 0000000..07498a7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/NetWorkReceiver.java @@ -0,0 +1,181 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.Build; + +import androidx.annotation.RequiresPermission; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 网络监听广播 + * @author Ttt + */ +public final class NetWorkReceiver extends BroadcastReceiver { + + private NetWorkReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = NetWorkReceiver.class.getSimpleName(); + // 当前连接的状态 + private static int mConnectState = NetWorkReceiver.NO_NETWORK; + + // ======== + // = 常量 = + // ======== + + private static final int BASE = 202030; + // Wifi + public static final int NET_WIFI = BASE + 1; + // 移动网络 + public static final int NET_MOBILE = BASE + 2; + // ( 无网络 / 未知 ) 状态 + public static final int NO_NETWORK = BASE + 3; + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 网络连接状态改变时通知 + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { + // 设置连接类型 + mConnectState = getConnectType(); + // 触发事件 + if (NetWorkReceiver.sListener != null) { + NetWorkReceiver.sListener.onNetworkState(mConnectState); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + /** + * 是否连接网络 + * @return {@code true} yes, {@code false} no + */ + public static boolean isConnectNetWork() { + return (mConnectState == NET_WIFI || mConnectState == NET_MOBILE); + } + + /** + * 获取连接的网络类型 + * @return 连接的网络类型 + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public static int getConnectType() { + // 获取手机所有连接管理对象 ( 包括对 wi-fi,net 等连接的管理 ) + try { + // 获取网络连接状态 + ConnectivityManager cManager = AppUtils.getConnectivityManager(); + // 版本兼容处理 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + // 判断连接的是否 Wifi + NetworkInfo.State wifiState = cManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState(); + // 判断是否连接上 + if (wifiState == NetworkInfo.State.CONNECTED || wifiState == NetworkInfo.State.CONNECTING) { + return NET_WIFI; + } else { + // 判断连接的是否移动网络 + NetworkInfo.State mobileState = cManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).getState(); + // 判断移动网络是否连接上 + if (mobileState == NetworkInfo.State.CONNECTED || mobileState == NetworkInfo.State.CONNECTING) { + return NET_MOBILE; + } + } + } else { + // 获取当前活跃的网络 ( 连接的网络信息 ) + Network network = cManager.getActiveNetwork(); + if (network != null) { + NetworkCapabilities networkCapabilities = cManager.getNetworkCapabilities(network); + // 判断连接的是否 Wifi + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return NET_WIFI; + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return NET_MOBILE; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getConnectType"); + } + return NO_NETWORK; + } + + // = + + // 网络广播监听 + private static final NetWorkReceiver sReceiver = new NetWorkReceiver(); + + /** + * 注册网络广播监听 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 网络连接状态改变时通知 + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + filter.setPriority(Integer.MAX_VALUE); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册网络广播监听 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 监听通知事件 + private static NetwordStateListener sListener; + + /** + * 设置监听通知事件 + * @param listener {@link NetwordStateListener} + * @return {@link NetWorkReceiver} + */ + public static NetWorkReceiver setNetListener(final NetwordStateListener listener) { + NetWorkReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 监听通知事件 + * @author Ttt + */ + public interface NetwordStateListener { + + /** + * 网络连接状态改变时通知 + * @param type 通知类型 + */ + void onNetworkState(int type); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/receiver/PhoneReceiver.java b/app/src/main/java/com/example/baseframe/receiver/PhoneReceiver.java new file mode 100644 index 0000000..bc7b6d9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/PhoneReceiver.java @@ -0,0 +1,177 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 手机监听广播 + * @author Ttt + *
+ *     所需权限
+ *     
+ *     
+ * 
+ */ +public final class PhoneReceiver extends BroadcastReceiver { + + private PhoneReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = PhoneReceiver.class.getSimpleName(); + // 电话状态监听意图 + private static final String PHONE_STATE = "android.intent.action.PHONE_STATE"; + // 拨出电话意图 + private static final String NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"; + // 通话号码 + private String mNumber; + // 是否拨号打出 + private boolean mIsDialOut; + + // ======== + // = 状态 = + // ======== + + // 未接 + private static final String RINGING = "RINGING"; + // 已接 + private static final String OFFHOOK = "OFFHOOK"; + // 挂断 + private static final String IDLE = "IDLE"; + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 判断类型 + if (NEW_OUTGOING_CALL.equals(action)) { + // 表示属于拨号 + mIsDialOut = true; + // 拨出号码 + mNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + // 触发事件 + if (sListener != null) { // 播出电话 + sListener.onPhoneStateChanged(CallState.Outgoing, mNumber); + } + } else if (PHONE_STATE.equals(action)) { + // 通话号码 + mNumber = intent.getStringExtra("incoming_number"); + // 状态 + String state = intent.getStringExtra("state"); + // 判断状态 + switch (state) { + case RINGING: // 未接 + mIsDialOut = false; + if (sListener != null) { // 接入电话铃响 + sListener.onPhoneStateChanged(CallState.IncomingRing, mNumber); + } + break; + case OFFHOOK: // 已接 + if (!mIsDialOut && sListener != null) { // 接入通话中 + sListener.onPhoneStateChanged(CallState.Incoming, mNumber); + } + break; + case IDLE: // 挂断 + if (mIsDialOut) { + if (sListener != null) { // 播出电话结束 + sListener.onPhoneStateChanged(CallState.OutgoingEnd, mNumber); + } + } else { + if (sListener != null) { // 接入通话完毕 + sListener.onPhoneStateChanged(CallState.IncomingEnd, mNumber); + } + } + break; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // 电话监听广播 + private static final PhoneReceiver sReceiver = new PhoneReceiver(); + + /** + * 注册电话监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 电话状态监听 + filter.addAction(PHONE_STATE); + filter.addAction(NEW_OUTGOING_CALL); + filter.setPriority(Integer.MAX_VALUE); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册电话监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 电话状态监听事件 + private static PhoneListener sListener; + + /** + * 设置电话状态监听事件 + * @param listener {@link PhoneListener} + * @return {@link PhoneReceiver} + */ + public static PhoneReceiver setPhoneListener(final PhoneListener listener) { + PhoneReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 电话状态监听事件 + * @author Ttt + */ + public interface PhoneListener { + + /** + * 电话状态改变通知 + * @param state 通话状态 + * @param number 通话号码 + */ + void onPhoneStateChanged(CallState state, String number); + } + + /** + * detail: 通话状态 + * @author Ttt + */ + public enum CallState { + + Outgoing, // 播出电话 + OutgoingEnd, // 播出电话结束 + IncomingRing, // 接入电话铃响 + Incoming, // 接入通话中 + IncomingEnd // 接入通话完毕 + } +} diff --git a/app/src/main/java/com/example/baseframe/receiver/ScreenReceiver.java b/app/src/main/java/com/example/baseframe/receiver/ScreenReceiver.java new file mode 100644 index 0000000..cfbb2c8 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/ScreenReceiver.java @@ -0,0 +1,126 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 屏幕监听广播 ( 锁屏 / 解锁 / 亮屏 ) + * @author Ttt + */ +public final class ScreenReceiver extends BroadcastReceiver { + + private ScreenReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = ScreenReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 判断类型 + switch (action) { + case Intent.ACTION_SCREEN_ON: // 开屏 + if (sListener != null) { + sListener.screenOn(); + } + break; + case Intent.ACTION_SCREEN_OFF: // 锁屏 + if (sListener != null) { + sListener.screenOff(); + } + break; + case Intent.ACTION_USER_PRESENT: // 解锁 + if (sListener != null) { + sListener.userPresent(); + } + break; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // 屏幕广播监听 + private static final ScreenReceiver sReceiver = new ScreenReceiver(); + + /** + * 注册屏幕监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 屏幕状态改变通知 + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + filter.setPriority(Integer.MAX_VALUE); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册屏幕监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 屏幕监听事件 + private static ScreenListener sListener; + + /** + * 设置屏幕监听事件 + * @param listener {@link ScreenListener} + * @return {@link ScreenReceiver} + */ + public static ScreenReceiver setScreenListener(final ScreenListener listener) { + ScreenReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 屏幕监听事件 + * @author Ttt + */ + public interface ScreenListener { + + /** + * 用户打开屏幕 ( 屏幕变亮 ) + */ + void screenOn(); + + /** + * 锁屏触发 + */ + void screenOff(); + + /** + * 用户解锁触发 + */ + void userPresent(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/receiver/SmsReceiver.java b/app/src/main/java/com/example/baseframe/receiver/SmsReceiver.java new file mode 100644 index 0000000..8af3742 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/SmsReceiver.java @@ -0,0 +1,155 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.telephony.SmsMessage; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 短信监听广播 + * @author Ttt + *
+ *     所需权限
+ *     
+ * 
+ */ +public final class SmsReceiver extends BroadcastReceiver { + + private SmsReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = SmsReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + try { + Object[] pdus = (Object[]) intent.getExtras().get("pdus"); + String originatingAddress = null; + String serviceCenterAddress = null; + if (pdus != null) { + // 消息内容 + String message = ""; + // 循环拼接内容 + for (Object object : pdus) { + SmsMessage sms = SmsMessage.createFromPdu((byte[]) object); + message += sms.getMessageBody(); // 消息内容 - 多条消息, 合并成一条 + originatingAddress = sms.getOriginatingAddress(); + serviceCenterAddress = sms.getServiceCenterAddress(); + // 触发事件 + if (sListener != null) { + sListener.onMessage(sms); + } + } + // 触发事件 + if (sListener != null) { + sListener.onMessage(message, originatingAddress, serviceCenterAddress); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + /** + * 获取消息数据 + * @param message {@link SmsMessage} + * @return 消息数据 + */ + public static String getMessageData(final SmsMessage message) { + StringBuilder builder = new StringBuilder(); + if (message != null) { + builder.append("\ngetDisplayMessageBody: " + message.getDisplayMessageBody()); + builder.append("\ngetDisplayOriginatingAddress: " + message.getDisplayOriginatingAddress()); + builder.append("\ngetEmailBody: " + message.getEmailBody()); + builder.append("\ngetEmailFrom: " + message.getEmailFrom()); + builder.append("\ngetMessageBody: " + message.getMessageBody()); + builder.append("\ngetOriginatingAddress: " + message.getOriginatingAddress()); + builder.append("\ngetPseudoSubject: " + message.getPseudoSubject()); + builder.append("\ngetServiceCenterAddress: " + message.getServiceCenterAddress()); + builder.append("\ngetIndexOnIcc: " + message.getIndexOnIcc()); + builder.append("\ngetMessageClass: " + message.getMessageClass()); + builder.append("\ngetUserData: " + new String(message.getUserData())); + } + return builder.toString(); + } + + // = + + // 短信监听广播 + private static final SmsReceiver sReceiver = new SmsReceiver(); + + /** + * 注册短信监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 短信获取监听 + filter.addAction("android.provider.Telephony.SMS_RECEIVED"); + filter.setPriority(Integer.MAX_VALUE); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册短信监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 短信监听事件 + private static SmsListener sListener; + + /** + * 设置短信监听事件 + * @param listener {@link SmsListener} + * @return {@link SmsReceiver} + */ + public static SmsReceiver setSmsListener(final SmsListener listener) { + SmsReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 短信监听事件 + * @author Ttt + */ + public static abstract class SmsListener { + + /** + * 最终触发通知 ( 超过长度的短信消息, 最终合并成一条内容体 ) + * @param message 短信内容 + * @param originatingAddress 短信的原始地址 ( 发件人 ) + * @param serviceCenterAddress 短信服务中心地址 + */ + public abstract void onMessage(String message, String originatingAddress, String serviceCenterAddress); + + /** + * 收到消息提醒 ( 超过长度的消息变成两条会触发多次 ) + * @param message {@link SmsMessage} + */ + public void onMessage(SmsMessage message) { + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/receiver/TimeReceiver.java b/app/src/main/java/com/example/baseframe/receiver/TimeReceiver.java new file mode 100644 index 0000000..9efd78b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/TimeReceiver.java @@ -0,0 +1,126 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; + + +/** + * detail: 时间监听广播 + * @author Ttt + */ +public final class TimeReceiver extends BroadcastReceiver { + + private TimeReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = TimeReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + try { + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 判断类型 + switch (action) { + case Intent.ACTION_TIMEZONE_CHANGED: // 时区改变 + if (sListener != null) { + sListener.onTimeZoneChanged(); + } + break; + case Intent.ACTION_TIME_CHANGED: // 设置时间 + if (sListener != null) { + sListener.onTimeChanged(); + } + break; + case Intent.ACTION_TIME_TICK: // 每分钟调用 + if (sListener != null) { + sListener.onTimeTick(); + } + break; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // 时间监听广播 + private static final TimeReceiver sReceiver = new TimeReceiver(); + + /** + * 注册时间监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 监听时间、时区改变通知 + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.setPriority(Integer.MAX_VALUE); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册时间监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // 时间监听事件 + private static TimeListener sListener; + + /** + * 设置时间监听事件 + * @param listener {@link TimeListener} + * @return {@link TimeReceiver} + */ + public static TimeReceiver setTimeListener(final TimeListener listener) { + TimeReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: 时间监听事件 + * @author Ttt + */ + public interface TimeListener { + + /** + * 时区改变 + */ + void onTimeZoneChanged(); + + /** + * 设置时间 + */ + void onTimeChanged(); + + /** + * 每分钟调用 + */ + void onTimeTick(); + } +} diff --git a/app/src/main/java/com/example/baseframe/receiver/WifiReceiver.java b/app/src/main/java/com/example/baseframe/receiver/WifiReceiver.java new file mode 100644 index 0000000..4c768b7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/receiver/WifiReceiver.java @@ -0,0 +1,276 @@ +package com.example.baseframe.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Message; +import android.os.Parcelable; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; +import com.example.baseframe.utils.wifi.WifiUtils; + + +/** + * detail: Wifi 监听广播 + * @author Ttt + *
+ *     所需权限
+ *     
+ *     
+ * 
+ */ +public final class WifiReceiver extends BroadcastReceiver { + + private WifiReceiver() { + super(); + } + + // 日志 TAG + private static final String TAG = WifiReceiver.class.getSimpleName(); + + // ======== + // = 常量 = + // ======== + + private static final int BASE = 302030; + // startScan() 扫描附近 Wifi 结束触发 + public static final int WIFI_SCAN_FINISH = BASE + 1; + // 已连接的 Wifi 强度发生变化 + public static final int WIFI_RSSI_CHANGED = BASE + 2; + // Wifi 认证错误 ( 密码错误等 ) + public static final int WIFI_ERROR_AUTHENTICATING = BASE + 3; + // 连接错误 ( 其他错误 ) + public static final int WIFI_ERROR_UNKNOWN = BASE + 4; + // Wifi 已打开 + public static final int WIFI_STATE_ENABLED = BASE + 5; + // Wifi 正在打开 + public static final int WIFI_STATE_ENABLING = BASE + 6; + // Wifi 已关闭 + public static final int WIFI_STATE_DISABLED = BASE + 7; + // Wifi 正在关闭 + public static final int WIFI_STATE_DISABLING = BASE + 8; + // Wifi 状态未知 + public static final int WIFI_STATE_UNKNOWN = BASE + 9; + // Wifi 连接成功 + public static final int CONNECTED = BASE + 10; + // Wifi 连接中 + public static final int CONNECTING = BASE + 11; + // Wifi 连接失败、断开 + public static final int DISCONNECTED = BASE + 12; + // Wifi 暂停、延迟 + public static final int SUSPENDED = BASE + 13; + // Wifi 未知 + public static final int UNKNOWN = BASE + 14; + + @Override + public void onReceive(Context context, Intent intent) { + try { + // 触发回调通知 ( 每次进入都通知 ) + if (sListener != null) sListener.onIntoTrigger(); + // 触发意图 + String action = intent.getAction(); + // 打印当前触发的广播 + ALog.dTag(TAG, "onReceive Action: " + action); + // 判断类型 + switch (action) { + case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION: // 当调用 WifiManager 的 startScan() 方法, 扫描结束后, 系统会发出改 Action 广播 + if (sListener != null) { + sListener.onTrigger(WIFI_SCAN_FINISH); + } + break; + case WifiManager.RSSI_CHANGED_ACTION: // 当前连接的 Wifi 强度发生变化触发 + if (sListener != null) { + sListener.onTrigger(WIFI_RSSI_CHANGED); + } + break; + case WifiManager.SUPPLICANT_STATE_CHANGED_ACTION: // 发送 Wifi 连接的过程信息, 如果出错 ERROR 信息才会收到, 连接 Wifi 时触发, 触发多次 + // 出现错误状态, 则获取错误状态 + int wifiErrorCode = intent.getIntExtra(WifiManager.EXTRA_SUPPLICANT_ERROR, 0); + // 获取错误状态 + switch (wifiErrorCode) { + case WifiManager.ERROR_AUTHENTICATING: // 认证错误, 如密码错误等 + if (sListener != null) { + sListener.onTrigger(WIFI_ERROR_AUTHENTICATING); + } + break; + default: // 连接错误 ( 其他错误 ) + if (sListener != null) { + sListener.onTrigger(WIFI_ERROR_UNKNOWN); + } + break; + } + break; + case WifiManager.WIFI_STATE_CHANGED_ACTION: // 监听 Wifi 打开与关闭等状态 + // 获取 Wifi 状态 + int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN); + switch (wifiState) { + case WifiManager.WIFI_STATE_ENABLED: // 已打开 + if (sListener != null) { + sListener.onTrigger(WIFI_STATE_ENABLED); + } + break; + case WifiManager.WIFI_STATE_ENABLING: // 正在打开 + if (sListener != null) { + sListener.onTrigger(WIFI_STATE_ENABLING); + } + break; + case WifiManager.WIFI_STATE_DISABLED: // 已关闭 + if (sListener != null) { + sListener.onTrigger(WIFI_STATE_DISABLED); + } + break; + case WifiManager.WIFI_STATE_DISABLING: // 正在关闭 + if (sListener != null) { + sListener.onTrigger(WIFI_STATE_DISABLING); + } + break; + case WifiManager.WIFI_STATE_UNKNOWN: // 未知 + if (sListener != null) { + sListener.onTrigger(WIFI_STATE_UNKNOWN); + } + break; + } + break; + case WifiManager.NETWORK_STATE_CHANGED_ACTION: // Wifi 连接过程的状态 + Parcelable parcelableExtra = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + if (parcelableExtra != null) { + // 获取连接信息 + NetworkInfo networkInfo = (NetworkInfo) parcelableExtra; + // 获取连接的状态 + NetworkInfo.State state = networkInfo.getState(); + // 通知消息 + Message msg = new Message(); + // 当前连接的 SSID + msg.obj = WifiUtils.getSSID(); + // 判断连接状态 + switch (state) { + case CONNECTED: // 连接成功 + msg.what = CONNECTED; + break; + case CONNECTING: // 连接中 + msg.what = CONNECTING; + break; + case DISCONNECTED: // 连接失败、断开 + msg.what = DISCONNECTED; + break; + case SUSPENDED: // 暂停、延迟 + msg.what = SUSPENDED; + break; + case UNKNOWN: // 未知 + msg.what = UNKNOWN; + break; + } + // 触发回调 + if (sListener != null) { + sListener.onTrigger(msg.what, msg); + } + } + break; + case WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION: // 判断是否 Wifi 打开了, 变化触发一次 + // 判断是否打开 Wifi + boolean isOpenWifi = intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false); + // 触发回调 + if (sListener != null) { + sListener.onWifiSwitch(isOpenWifi); + } + break; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "onReceive"); + } + } + + // ================ + // = 对外公开方法 = + // ================ + + // Wifi 监听广播 + private static final WifiReceiver sReceiver = new WifiReceiver(); + + /** + * 注册 Wifi 监听广播 + */ + public static void registerReceiver() { + try { + IntentFilter filter = new IntentFilter(); + // 当调用 WifiManager 的 startScan() 方法, 扫描结束后, 系统会发出改 Action 广播 + filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + // 当前连接的 Wifi 强度发生变化触发 + filter.addAction(WifiManager.RSSI_CHANGED_ACTION); + // Wifi 在连接过程的状态返回 + filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + // 监听 Wifi 的打开与关闭等状态, 与 Wifi 的连接无关 + filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + // 发送 Wifi 连接的过程信息, 如果出错 ERROR 信息才会收到, 连接 Wifi 时触发, 触发多次 + filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); + // 判断是否 Wifi 打开了, 变化触发一次 + filter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION); + // 注册广播 + AppUtils.registerReceiver(sReceiver, filter); + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + } + + /** + * 取消注册 Wifi 监听广播 + */ + public static void unregisterReceiver() { + try { + AppUtils.unregisterReceiver(sReceiver); + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + } + + // = + + // Wifi 监听事件 + private static WifiListener sListener; + + /** + * 设置 Wifi 监听事件 + * @param listener {@link WifiListener} + * @return {@link WifiReceiver} + */ + public static WifiReceiver setWifiListener(final WifiListener listener) { + WifiReceiver.sListener = listener; + return sReceiver; + } + + /** + * detail: Wifi 监听事件 + * @author Ttt + */ + public static abstract class WifiListener { + + /** + * 触发回调通知 ( 每次进入都通知 ) + */ + public void onIntoTrigger() { + } + + /** + * 触发回调通知 + * @param what 触发类型 + */ + public abstract void onTrigger(int what); + + /** + * 触发回调通知 ( Wifi 连接过程的状态 ) + * @param what 触发类型 + * @param msg 触发信息 + */ + public abstract void onTrigger(int what, Message msg); + + /** + * Wifi 开关状态 + * @param isOpenWifi 是否打开 Wifi + */ + public abstract void onWifiSwitch(boolean isOpenWifi); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/rx/RxBus.java b/app/src/main/java/com/example/baseframe/rx/RxBus.java new file mode 100644 index 0000000..8d4726f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/rx/RxBus.java @@ -0,0 +1,112 @@ +package com.example.baseframe.rx; + + +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.functions.Predicate; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.Subject; + +/** + * Created by yzh on 19/11/11. + */ +public class RxBus { + /** + * 参考: http://hanhailong.com/2015/10/09/RxBus%E2%80%94%E9%80%9A%E8%BF%87RxJava%E6%9D%A5%E6%9B%BF%E6%8D%A2EventBus/ + * http://www.loongwind.com/archives/264.html + * https://theseyears.gitbooks.io/android-architecture-journey/content/rxbus.html + */ + + //RxBus=用RxJava模拟实现的EventBus的功能,不需要再额外引入EventBus库增加app代码量 +// RxBus.getDefault().post(1,"呵呵呵哒"); +// +// RxBus.getDefault().toObservable(String.class).subscribe(new Consumer() { +// @Override +// public void accept(String s) throws Exception { +// +// } +// }); +// } + + + private static volatile RxBus mDefaultInstance; + + private RxBus() { + } + + public static RxBus getDefault() { + if (mDefaultInstance == null) { + synchronized (RxBus.class) { + if (mDefaultInstance == null) { + mDefaultInstance = new RxBus(); + } + } + } + return mDefaultInstance; + } + + private final Subject _bus = PublishSubject.create().toSerialized(); + + public void send(Object o) { + _bus.onNext(o); + } + + public Observable toObservable() { + return _bus; + } + + /** + * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者 + * + * @param eventType 事件类型 + * @param + * @return + */ + public Observable toObservable(Class eventType) { + return _bus.ofType(eventType); + } + + /** + * 提供了一个新的事件,根据code进行分发 + * + * @param code 事件code + * @param o + */ + public void post(int code, Object o) { + _bus.onNext(new RxBusMessage(code, o)); + + } + + /** + * 根据传递的code和 eventType 类型返回特定类型(eventType)的 被观察者 + * 对于注册了code为0,class为voidMessage的观察者,那么就接收不到code为0之外的voidMessage。 + * + * @param code 事件code + * @param eventType 事件类型 + * @param + * @return + */ + public Observable toObservable(final int code, final Class eventType) { + return _bus.ofType(RxBusMessage.class) + .filter(new Predicate() { + @Override + public boolean test(RxBusMessage rxBusMessage) throws Exception { + //过滤code和eventType都相同的事件 + return rxBusMessage.getCode() == code && eventType.isInstance(rxBusMessage.getObject()); + } + }).map(new Function() { + @Override + public Object apply(RxBusMessage rxBusMessage) throws Exception { + return rxBusMessage.getObject(); + } + }).cast(eventType); + } + + /** + * 判断是否有订阅者 + */ + public boolean hasObservers() { + return _bus.hasObservers(); + } + +} diff --git a/app/src/main/java/com/example/baseframe/rx/RxBusCode.java b/app/src/main/java/com/example/baseframe/rx/RxBusCode.java new file mode 100644 index 0000000..a90b874 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/rx/RxBusCode.java @@ -0,0 +1,12 @@ +package com.example.baseframe.rx; + + +public class RxBusCode { + + // 对应type + public static final int TYPE_0 = 0; + // 首页跳转到详情 + public static final int TYPE_1 = 1; + // 下载 + public static final int TYPE_2 = 2; +} diff --git a/app/src/main/java/com/example/baseframe/rx/RxBusMessage.java b/app/src/main/java/com/example/baseframe/rx/RxBusMessage.java new file mode 100644 index 0000000..cce69e3 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/rx/RxBusMessage.java @@ -0,0 +1,19 @@ +package com.example.baseframe.rx; + +public class RxBusMessage { + private int code; + private Object object; + public RxBusMessage(int code, Object object){ + this.code=code; + this.object=object; + } + public RxBusMessage(){} + + public int getCode() { + return code; + } + + public Object getObject() { + return object; + } +} diff --git a/app/src/main/java/com/example/baseframe/rx/RxSubscriptions.java b/app/src/main/java/com/example/baseframe/rx/RxSubscriptions.java new file mode 100644 index 0000000..e6c265a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/rx/RxSubscriptions.java @@ -0,0 +1,36 @@ +package com.example.baseframe.rx; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * 管理 CompositeSubscription + */ +public class RxSubscriptions { + private static CompositeDisposable mSubscriptions = new CompositeDisposable(); + + public static boolean isDisposed() { + return mSubscriptions.isDisposed(); + } + + public static void add(Disposable s) { + if (s != null) { + mSubscriptions.add(s); + } + } + + public static void remove(Disposable s) { + if (s != null) { + mSubscriptions.remove(s); + } + } + + public static void clear() { + mSubscriptions.clear(); + } + + public static void dispose() { + mSubscriptions.dispose(); + } + +} diff --git a/app/src/main/java/com/example/baseframe/rx/RxTimerUtil.java b/app/src/main/java/com/example/baseframe/rx/RxTimerUtil.java new file mode 100644 index 0000000..01a5fa0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/rx/RxTimerUtil.java @@ -0,0 +1,279 @@ +package com.example.baseframe.rx; + +import android.graphics.Color; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.utils.CommUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; + +/** + * Rxjava2.x实现轮询定时器. + * 2019年04月15日09:07:59 + * @author yzh + */ +public class RxTimerUtil { + + /** + * 作用1、避免重复执行相同name定时器2,计时结束后取消订阅 + */ + private static Map mDisposableMap=new HashMap(); + /** + * x秒后执行next操作 + */ + public static void timer(long seconds, final String name, final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.timer(seconds, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + //取消订阅 + cancel(name); + } + + @Override + public void onComplete() { + //取消订阅 + cancel(name); + } + }); + } + + + /** + * 每隔milliseconds秒后执行next操作 + * @param milliseconds + * @param name 给当前定时器命名 + * @param next + */ + public static void interval(long milliseconds, final String name , final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(milliseconds, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + cancel(name); + ALog.e("---onError---"); + } + + @Override + public void onComplete() { + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void interval(long milliseconds, TimeUnit unit, final String name, final IRxNext next) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(milliseconds,unit) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable disposable) { + mDisposableMap.put(name,disposable); + } + + @Override + public void onNext(@NonNull Long number) { + if (next != null) { + next.doNext(number,name); + } + } + + @Override + public void onError(@NonNull Throwable e) { + cancel(name); + } + + @Override + public void onComplete() { + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void countDownTimer(final long seconds, final String name, final TextView tv) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(0, 1, TimeUnit.SECONDS) + .take(seconds + 1) + .map(new Function() { + @Override + public Long apply(Long aLong) throws Exception { + return seconds - aLong; + } + }) + .observeOn(AndroidSchedulers.mainThread())//ui线程中进行控件更新 + .doOnSubscribe(new Consumer() { + @Override + public void accept(Disposable disposable) throws Exception { + tv.setEnabled(false); + tv.setTextColor(Color.BLACK); + } + }).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) {mDisposableMap.put(name,disposable); } + @Override + public void onNext(Long num) { + tv.setText("剩余" + num + "秒"); + } + @Override + public void onError(Throwable e) {cancel(name);} + + @Override + public void onComplete() { + //回复原来初始状态 + tv.setEnabled(true); + tv.setText("发送验证码"); + cancel(name); + } + }); + } + + + /** + * 每隔xx后执行next操作 + */ + public static void countDownTimer(final long seconds, final String name,TextView tv,ITimer iTimer) { + if(mDisposableMap.containsKey(name)){ + ALog.e(TextUtils.concat("已经有定时器【",name,"】在执行了,本次重复定时器不在执行").toString()); + return; + } + Observable.interval(0, 1, TimeUnit.SECONDS) + .take(seconds + 1) + .map(new Function() { + @Override + public Long apply(Long aLong) throws Exception { + return seconds - aLong; + } + }) + .observeOn(AndroidSchedulers.mainThread())//ui线程中进行控件更新 + .doOnSubscribe(new Consumer() { + @Override + public void accept(Disposable disposable) throws Exception { + if(tv!=null){ + tv.setVisibility(View.VISIBLE); + CommUtils.setTextColor(tv, R.color.color_write); + } + + } + }) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) {mDisposableMap.put(name,disposable); } + @Override + public void onNext(Long num) { + //tv.setText("剩余" + num + "秒"); + + if(tv!=null){ + if(num<=3){ + CommUtils.setTextColor(tv, R.color.color_red); + } + tv.setText(String.valueOf(num)); + } + + iTimer.doNext(num,num==0); + } + @Override + public void onError(Throwable e) {cancel(name);} + + @Override + public void onComplete() { + //回复原来初始状态 + // tv.setEnabled(true); + // tv.setText("发送验证码"); + cancel(name); + if(tv!=null){ + tv.setVisibility(View.GONE); + } + + } + }); + } + + + /** + * 取消订阅 + */ + public static void cancel(String timerName) { + Disposable mDisposable= (Disposable) mDisposableMap.get(timerName); + if (mDisposable != null) { + mDisposableMap.remove(timerName); + + if(!mDisposable.isDisposed()){ + mDisposable.dispose(); + Log.i("RxTimerUtil","---Rx定时器【"+timerName+"】取消---"); + } + + } + } + + public interface IRxNext { + void doNext(long number, String timerName); + } + public interface ITimer { + void doNext(long number, boolean complete); + } +} diff --git a/app/src/main/java/com/example/baseframe/ui/MainDetailActivity.java b/app/src/main/java/com/example/baseframe/ui/MainDetailActivity.java new file mode 100644 index 0000000..52497c6 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/MainDetailActivity.java @@ -0,0 +1,138 @@ +package com.example.baseframe.ui; + +import android.view.View; + +import androidx.databinding.DataBindingUtil; + +import com.blankj.ALog; + +import com.example.baseframe.R; +import com.example.baseframe.base.BaseActivity; +import com.example.baseframe.databinding.ActivityMainDetailBinding; +import com.example.baseframe.databinding.TitleLayoutBinding; +import com.example.baseframe.downloadapk.DownloadAPk; +import com.example.baseframe.rx.RxBus; +import com.example.baseframe.rx.RxBusCode; +import com.example.baseframe.ui.viewmodel.MainDetialViewModel; +import com.example.baseframe.utils.BarUtils; +import com.example.baseframe.utils.ClickUtils; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.StatusBarUtil; +import com.example.baseframe.utils.ToastUtils; +import com.example.baseframe.utils.permission.PermissionsUtil; +import com.example.baseframe.utils.permission.PermissionsUtils; + + +/** + * @Description: banner 详情 + * @Author: yzh + * @CreateDate: 2019/11/19 16:01 + */ +public class MainDetailActivity extends BaseActivity { + TitleLayoutBinding mTitleLayoutBinding; + @Override + protected int getLayoutId() { + return R.layout.activity_main_detail; + } + + @Override + public void initViewObservable() { + // mViewModel.downLoadEvent.observe(this,o -> download(o)); + + mViewModel.downLoadEvent.observe(this, this::download); + mViewModel.permissionsEvent.observe(this,o -> { + ToastUtils.showShort("权限申请"); + PermissionsUtils.getInstance().chekPermissions(this, PermissionsUtil.PERMISSION_CAMERA, new PermissionsUtils.IPermissionsResult() { + @Override + public void passPermissons() { + ToastUtils.showShort("获取相机成功"); + } + @Override + public void forbitPermissons() { + ToastUtils.showShort("您拒绝了存储权限"); + } + }); + }); + + mViewModel.errorEvent.observe(this,o ->{ + throw new NullPointerException(o); + }); + } + + + @Override + protected void initView() { + StatusBarUtil.setColorNoTranslucent(mContext, getColors(R.color.colorAccent)); + // BarUtils.addMarginTopEqualStatusBarHeight(mBinding.rootLayout); + BarUtils.setStatusBarLightMode(mContext,true); + //示例如何动态的添加一个BindingView + mTitleLayoutBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.title_layout,mBinding.topLayout, false); + mBinding.topLayout.addView(mTitleLayoutBinding.getRoot()); + + mTitleLayoutBinding.titleBack.setOnClickListener(v -> finish()); + // mBinding.mUpdateButton.setOnClickListener(v -> download()); + + // WanAndroidBannerBean mBannerBean=(WanAndroidBannerBean) getIntent().getSerializableExtra("bannerBean"); + // mBinding.setBannerBean(mBannerBean); + mTitleLayoutBinding.titleText.setText(mViewModel.mBannerBean.getTitle()); + + + //点击工具类使用 + ClickUtils.applyPressedViewScale(mBinding.btn1,mBinding.btn2);//给view 添加缩放点击效果 + ClickUtils.applyPressedViewAlpha(mBinding.primaryMessageDetailsImg,0.6f);//给view 透明度点击效果 + ClickUtils.applyPressedBgAlpha(mBinding.btn3,0.5f);//给view 透明度背景点击效果 + ClickUtils.applyPressedBgDark(mBinding.btn4);//给view 背景变暗点击效果 + + mBinding.primaryMessageDetailsImg.setOnClickListener(new ClickUtils.OnMultiClickListener(5) { + @Override + public void onTriggerClick(View v) { + ToastUtils.showShort("指定时间内连续点击5次触发事件"); + } + + @Override + public void onBeforeTriggerClick(View v, int count) { + ToastUtils.showShort(String.valueOf(count)); + } + }); + + } + + + public void download(String url){ + CommUtils.showDialog(mContext,"提示","测试更新apk" + ,"确定","取消",() -> { + PermissionsUtils.getInstance().chekPermissions(this, PermissionsUtil.PERMISSION_FILE, new PermissionsUtils.IPermissionsResult() { + @Override + public void passPermissons() { + + canInstallAPK(() -> + DownloadAPk.getInstance().downApk(mContext, url + , new DownloadAPk.DownLoadListener() { + @Override + public void onProgressUpdate(int progress) { + mViewModel.mBannerBean.progressValue.set(progress); + } + + @Override + public void finish(String filePath) { + ALog.d(filePath); + } + }) + ); + + } + @Override + public void forbitPermissons() { + ToastUtils.showShort("您拒绝了存储权限,将无法下载更新"); + } + }); + },null); + } + + @Override + protected void onDestroy() { + + RxBus.getDefault().post(RxBusCode.TYPE_0,"MainDetailActivity"); + super.onDestroy(); + } +} diff --git a/app/src/main/java/com/example/baseframe/ui/Test.java b/app/src/main/java/com/example/baseframe/ui/Test.java new file mode 100644 index 0000000..85a133b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/Test.java @@ -0,0 +1,21 @@ +package com.example.baseframe.ui; + +/** + * Created by yzh on 2020/1/7 15:00. + */ +public class Test { + public static void main(String[] args) { +// System.out.println( ArithUtils.roundSub("11.0",0)); +// System.out.println( ArithUtils.roundSub("11.1",0)); +// System.out.println( ArithUtils.roundSub("11.6",0)); +// String s="the number one is-good!"; +// System.out.println("s="+s.indexOf("one is")); +// System.out.println("s:"+s.charAt(s.indexOf("one is")+"one is".length())); + + + String s1="aa bb ccc d!"; + System.out.println("s1:"+s1); + String s2=s1.replaceAll(" {2,}"," ");//将2个以上的空格,替换为1个空格(即多个空格只保留1个) + System.out.println("s2:"+s2); + } +} diff --git a/app/src/main/java/com/example/baseframe/ui/TestDetailFragment.java b/app/src/main/java/com/example/baseframe/ui/TestDetailFragment.java new file mode 100644 index 0000000..41172c2 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/TestDetailFragment.java @@ -0,0 +1,155 @@ +package com.example.baseframe.ui; + +import android.app.ProgressDialog; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; + +import com.blankj.ALog; + +import com.example.baseframe.R; +import com.example.baseframe.base.BaseFragment; +import com.example.baseframe.databinding.ActivityMainDetailBinding; +import com.example.baseframe.databinding.TitleLayoutBinding; +import com.example.baseframe.download.DownLoadManager; +import com.example.baseframe.download.ProgressCallBack; +import com.example.baseframe.ui.viewmodel.MainDetialViewModel; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.StatusBarUtil; +import com.example.baseframe.utils.ToastUtils; +import com.example.baseframe.utils.permission.PermissionsUtil; +import com.example.baseframe.utils.permission.PermissionsUtils; + + +import java.io.File; + +import okhttp3.ResponseBody; + +import static com.example.baseframe.utils.CommUtils.getInstallAppIntent; + +/** + * 除了下载功能与MainDetailActivity一样 --只是为了 示例Fragment 使用 + * Anthor yzh Date 2019/12/9 13:57 + */ +public class TestDetailFragment extends BaseFragment { + TitleLayoutBinding mTitleLayoutBinding; + + @Override + protected int getLayoutId(LayoutInflater inflater, @Nullable ViewGroup container) { + return R.layout.activity_main_detail; + } + + @Override + public void initViewObservable() { + mViewModel.downLoadEvent.observe(this, this::downloadApk); + + mViewModel.permissionsEvent.observe(this,o -> { + ToastUtils.showShort("权限申请"); + PermissionsUtils.getInstance().chekPermissions(mActivity, PermissionsUtil.PERMISSION_CAMERA, new PermissionsUtils.IPermissionsResult() { + @Override + public void passPermissons() { + ToastUtils.showShort("获取相机成功"); + } + @Override + public void forbitPermissons() { + ToastUtils.showShort("您拒绝了相机权限"); + } + }); + }); + + mViewModel.errorEvent.observe(this,o ->{ + throw new NullPointerException(o); + }); + } + + @Override + protected void initView() { + StatusBarUtil.setColorNoTranslucent(mActivity, getColors(R.color.colorPrimary)); + //示例如何动态的添加一个BindingView + mTitleLayoutBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.title_layout,mBinding.topLayout, false); + mBinding.topLayout.addView(mTitleLayoutBinding.getRoot()); + + mTitleLayoutBinding.titleBack.setOnClickListener(v -> mActivity.finish()); + // mBinding.mUpdateButton.setOnClickListener(v -> download()); + + // WanAndroidBannerBean mBannerBean=(WanAndroidBannerBean) getIntent().getSerializableExtra("bannerBean"); + // mBinding.setBannerBean(mBannerBean); + mTitleLayoutBinding.titleText.setText("示例Fragment使用"); + + mViewModel.downBtnName.set("===使用Retrofit方式下载apk==="); + } + + + public void downloadApk(String url){ + CommUtils.showDialog(mActivity,"提示","测试Retrofit方式更新apk" + ,"确定","取消",() -> { + PermissionsUtils.getInstance().chekPermissions(mActivity, PermissionsUtil.PERMISSION_FILE, new PermissionsUtils.IPermissionsResult() { + @Override + public void passPermissons() { + canInstallAPK(() -> downLoad(url)); + + } + @Override + public void forbitPermissons() { + ToastUtils.showShort("您拒绝了存储权限,将无法下载更新"); + } + }); + },null); + + } + public void downLoad(String url){ + String destFileDir = mActivity.getCacheDir().getPath(); + String destFileName ="downlaod.apk"; + final ProgressDialog progressDialog = new ProgressDialog(mActivity); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setTitle("正在下载..."); + progressDialog.setCancelable(true); + progressDialog.show(); + DownLoadManager.getInstance().load(url, new ProgressCallBack(destFileDir, destFileName) { + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onCompleted() { + progressDialog.dismiss(); + mViewModel.mBannerBean.progressValue.set(100); + } + + @Override + public void onSuccess(ResponseBody responseBody) { + ToastUtils.showShort("文件下载完成!"); + String fileStr=destFileDir+ File.separator +destFileName; + ALog.v(fileStr); + + startActivity(getInstallAppIntent(fileStr)); + } + + @Override + public void progress(final long progress, final long total) { + progressDialog.setMax((int) total); + progressDialog.setProgress((int) progress); + + + int mProgress = (int) (+progress * 100 / total); + //进度显示2位小数: + // double mProgress= ArithUtils.round((progress * 100 / (double) total),2); + + Log.v("DownloadAPk",mProgress + "% 总大小:" + total+"已下载大小:"+progress); + mViewModel.mBannerBean.progressValue.set(mProgress); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + ToastUtils.showShort("文件下载失败!"); + progressDialog.dismiss(); + } + }); + } + +} diff --git a/app/src/main/java/com/example/baseframe/ui/TestWeightActivity.java b/app/src/main/java/com/example/baseframe/ui/TestWeightActivity.java new file mode 100644 index 0000000..25e57a9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/TestWeightActivity.java @@ -0,0 +1,89 @@ +package com.example.baseframe.ui; + +import com.blankj.ALog; + +import com.example.baseframe.R; +import com.example.baseframe.base.BaseActivity; +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.databinding.ActivityTestWeightBinding; +import com.example.baseframe.rx.RxTimerUtil; +import com.example.baseframe.utils.animations.Other; +import com.example.baseframe.utils.animations.RxAnimation; +import com.example.baseframe.weight.IProgressBar; +import com.example.baseframe.weight.SuperTextView; + + +import java.util.concurrent.TimeUnit; + +/** + * @anthor yzh + * @time 2019/11/30 15:07 + */ +public class TestWeightActivity extends BaseActivity { + + int num=0; + @Override + protected int getLayoutId() { + return R.layout.activity_test_weight; + } + +// @Override +// protected BaseMvvmViewModel initMVVMViewModel() { +// return null; +// } + + @Override + public void initViewObservable() { + + } + + @Override + protected void initView() { + mBinding.mCircleProgressBar.setIProgressBarTextGenerator(new IProgressBar.IProgressBarTextGenerator() { + @Override + public String generateText(IProgressBar progressBar, int value, int maxValue) { + if(value==maxValue){ + mBinding.mCircleProgressBar.setProgress(0); + RxTimerUtil.cancel("TestWeightActivity"); + } + return 100 * value / maxValue + "%"; + } + }); + + //倒计时工具类 + RxTimerUtil.interval(100, TimeUnit.MILLISECONDS,"TestWeightActivity",(number, timerName) -> { + mBinding.mCircleProgressBar.setProgress(num++); + + }); + + mBinding.mCountDownView.startCountDown(); + mBinding.mCountDownView.setAddCountDownListener(() -> mBinding.mCountDownView.startCountDown()); + mBinding.mLineWaveVoiceView.startRecord(); + + //示例动画工具类使用 + RxAnimation.get().setAnimation(Other.pulseAnimator(mBinding.mCountDownView,2)) + .setDuration(1000) + .start(); + + + String content="Android仿酷狗动感歌词(支持翻译和音译歌词)显示效果\nhttps://www.jianshu.com/p/9e7111db7b41"; + mBinding.mSuperTextView.setDynamicText(content); + mBinding.mSuperTextView.setDynamicStyle(SuperTextView.DynamicStyle.CHANGE_COLOR); + mBinding.mSuperTextView.setDurationByToalTime(8*1000); + mBinding.mSuperTextView.start(); + mBinding.mSuperTextView.setOnDynamicListener(new SuperTextView.OnDynamicListener() { + @Override + public void onChange(int position, int total) { + int lineCount = mBinding.mSuperTextView.getLineCount(); + int lineHeight = mBinding.mSuperTextView.getLineHeight(); + ALog.i(String.format("总行数:%s,行高度:%s", lineCount, lineHeight)); + } + + @Override + public void onCompile() { + mBinding.mSuperTextView.setDynamicStyle(SuperTextView.DynamicStyle.TYPEWRITING); + mBinding.mSuperTextView.start(); + } + }); + } +} diff --git a/app/src/main/java/com/example/baseframe/ui/main/MainNewActivity.java b/app/src/main/java/com/example/baseframe/ui/main/MainNewActivity.java new file mode 100644 index 0000000..90182ec --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/main/MainNewActivity.java @@ -0,0 +1,145 @@ +package com.example.baseframe.ui.main; + +import android.os.Bundle; +import android.view.KeyEvent; + +import androidx.lifecycle.Observer; + +import com.blankj.ALog; + +import com.example.baseframe.BuildConfig; +import com.example.baseframe.R; +import com.example.baseframe.base.BaseActivity; +import com.example.baseframe.bean.WanAndroidBannerBean; +import com.example.baseframe.databinding.ActivityNewMainBinding; +import com.example.baseframe.rx.RxBus; +import com.example.baseframe.rx.RxBusCode; +import com.example.baseframe.ui.MainDetailActivity; +import com.example.baseframe.ui.viewmodel.MainNewViewModel; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.GlideImageLoader; +import com.example.baseframe.utils.StatusBarUtil; +import com.example.baseframe.utils.ToastUtils; +import com.example.baseframe.webview.WebViewActivity; +import com.example.baseframe.weight.banner.BannerConfig; +import com.example.baseframe.weight.banner.Transformer; + + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.disposables.Disposable; + +/** + * ViewModelProviders.of 方式 初始化的ViewModel,ViewModel不持有context,LiveDada通知回调 不用担心管理内存泄漏问题 方式 实现的MVVM(推荐这种) + */ +public class MainNewActivity extends BaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + StatusBarUtil.setTransparentForImageView(mContext,null); + + //示例 RxBus使用 + Disposable subscribe = RxBus.getDefault().toObservable(RxBusCode.TYPE_0,String.class).subscribe(s -> ALog.i("返回值:"+s)); + addRxDisposable(subscribe); + } + + @Override + protected int getLayoutId() { + return R.layout.activity_new_main; + } + + @Override + public void initViewObservable() { + //mViewModel.toastEvent.observe(this, s -> ToastUtils.showShort(s)); + mViewModel.toastEvent.observe(this, ToastUtils::showShort); + } + + + @Override + protected void initView() { + + initBanner(); + mBinding.mRefreshLayout.setOnRefreshListener(refreshLayout ->getHomeList(true)); + mBinding.mRefreshLayout.setOnLoadMoreListener(refreshLayout ->getHomeList(false)); + } + + private void initBanner(){ + + //设置banner样式 + mBinding.include.mBanner.setBannerStyle(BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE)//设置banner样式 + .setImageLoader(new GlideImageLoader())//设置图片加载器 + //.setImages(images) + .setBannerAnimation(Transformer.ZoomOutSlide) + //.setBannerTitles(titles);//设置标题集合(当banner样式有显示title时) + .isAutoPlay(true)//设置自动轮播,默认为true + .setDelayTime(3000)//设置轮播时间 + .setIndicatorGravity(BannerConfig.LEFT) //设置指示器位置(当banner模式中有指示器时) + .start();//banner设置方法全部调用完毕时最后调用 + + getWanBanner(); + + } + + private void getHomeList(boolean isRefresh){ + + mViewModel.getHomeList(-1,isRefresh).observe(this,articlesBeans -> + showEmptyView(articlesBeans,mViewModel.mAdapter,mBinding.mRefreshLayout,"数据空空如也~") + ); + } + + private void getWanBanner() { + + mViewModel.getWanAndroidBanner().observe(this, new Observer>() { + @Override + public void onChanged(List dataBeans) { + mBinding.mRefreshLayout.autoRefresh(500); + if(CommUtils.isListNotNull(dataBeans)){ + List images=new ArrayList<>(); + List titles=new ArrayList<>(); + for (WanAndroidBannerBean articlesBean:dataBeans){ + images.add(articlesBean.getImagePath()); + titles.add(articlesBean.getTitle()); + } + mBinding.include.mBanner.update(images,titles) + .setOnBannerListener(position -> { + + ToastUtils.showShort(dataBeans.get(position).getUrl()); + Bundle bundle=new Bundle(); + bundle.putSerializable("bannerBean",dataBeans.get(position)); + startActivity(MainDetailActivity.class,bundle); + }); + + } else { + ToastUtils.showShort("获取banner失败~~"); + } + } + }); + + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (BuildConfig.DEBUG) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_DOWN: + WebViewActivity.loadUrl("https://github.com/yezihengok",null); + return super.onKeyDown(keyCode, event); + case KeyEvent.KEYCODE_VOLUME_UP: + return super.onKeyDown(keyCode, event); + case KeyEvent.KEYCODE_MENU: + + return true; + case KeyEvent.KEYCODE_BACK: + + break; + } + } + + return super.onKeyDown(keyCode, event); + } + + +} diff --git a/app/src/main/java/com/example/baseframe/ui/viewmodel/MainDetialViewModel.java b/app/src/main/java/com/example/baseframe/ui/viewmodel/MainDetialViewModel.java new file mode 100644 index 0000000..c626866 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/viewmodel/MainDetialViewModel.java @@ -0,0 +1,69 @@ +package com.example.baseframe.ui.viewmodel; + +import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableField; + +import com.blankj.ALog; + +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.bean.WanAndroidBannerBean; +import com.example.baseframe.bus.event.SingleLiveEvent; +import com.example.baseframe.listener.ClickListener; +import com.example.baseframe.ui.TestDetailFragment; +import com.example.baseframe.ui.TestWeightActivity; +import com.example.baseframe.webview.WebViewActivity; + + +/** + * Anthor yzh Date 2019/12/6 11:32 + */ +public class MainDetialViewModel extends BaseViewModel { + private Bundle bundle; + public WanAndroidBannerBean mBannerBean; + + //权限点击事件 在onBindingClick里调用了call, + public SingleLiveEvent permissionsEvent = new SingleLiveEvent<>(); + + public SingleLiveEvent downLoadEvent = new SingleLiveEvent<>(); + public SingleLiveEvent errorEvent = new SingleLiveEvent<>(); + + + public ObservableField downBtnName = new ObservableField<>("点击体验下载的乐趣"); + public MainDetialViewModel(@NonNull Application application) { + super(application); + } + + @Override + public void onBundle(Bundle bundle) { + this.bundle=bundle; + if(bundle!=null){ + mBannerBean= (WanAndroidBannerBean) bundle.getSerializable("bannerBean"); + ALog.w("示例ViewModel获取acitivty的传值:"+mBannerBean.toString()); + } + + + } + + //闪退点击事件 + public ClickListener errorClick=(v) -> errorEvent.setValue("哟哟哟,项目又报空指针了呢"); + + //下载 + public ClickListener downloadClick=(v) -> downLoadEvent.setValue("http://s.duapps.com/apks/own/ESFileExplorer-V4.2.1.7.apk"); + + //跳转测试Fragment + public ClickListener fragmentClick=(v) -> startContainerActivity(TestDetailFragment.class.getCanonicalName(), bundle); + + //跳转网页 + public void toWebView(){ + // WebViewActivity.loadUrl("http://www.baidu.com",null); + WebViewActivity.loadUrl("http://www.baidu.commmmmmmm",null); + } + + //跳转 + public void toTestWeight(){ + startActivity(TestWeightActivity.class); + } +} diff --git a/app/src/main/java/com/example/baseframe/ui/viewmodel/MainNewViewModel.java b/app/src/main/java/com/example/baseframe/ui/viewmodel/MainNewViewModel.java new file mode 100644 index 0000000..08c6a1b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/ui/viewmodel/MainNewViewModel.java @@ -0,0 +1,134 @@ +package com.example.baseframe.ui.viewmodel; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableArrayList; +import androidx.lifecycle.MutableLiveData; + +import com.example.baseframe.R; +import com.example.baseframe.api.CommonObserver; + +import com.example.baseframe.api.HttpReq; +import com.example.baseframe.base.BaseMvvmRecyclerAdapter; +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.bean.ArticlesBean; +import com.example.baseframe.bean.ResultBeans; + +import com.example.baseframe.bean.WanAndroidBannerBean; +import com.example.baseframe.bus.event.SingleLiveEvent; +import com.example.baseframe.utils.ToastUtils; +import com.example.baseframe.webview.WebViewActivity; + + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.disposables.Disposable; + +import static com.example.baseframe.api.ConfigApi.ERROR_CODE; +import static com.example.baseframe.utils.CommUtils.isListNotNull; + +/** + * + * Author: yzh + * @CreateDate: 2019/11/16 11:58 + */ +@SuppressLint("CheckResult") +public class MainNewViewModel extends BaseViewModel { + + //使用LiveData 可通知Activity去toast + public SingleLiveEvent toastEvent = new SingleLiveEvent<>(); + + @Override + public void onBundle(Bundle bundle) { + } + + public MainNewViewModel(@NonNull Application application) { + super(application); + } + + private ObservableArrayList mList=new ObservableArrayList<>(); + + public BaseMvvmRecyclerAdapter mAdapter=new BaseMvvmRecyclerAdapter(R.layout.item_message, mList) { + @Override + public void convert(BindingViewHolder holder, ArticlesBean item,int position) { + holder.itemView.setOnClickListener(v -> { + //当然可以直接在这toast,这里示例:回调给activity去处理 + //ToastUtils.showShort(item.getLink()); + toastEvent.setValue("试试点击banner"); + WebViewActivity.loadUrl(item.getLink(),null); + }); + } + }; + + +// 在LiveData出现之前,一般状态分发我们使用EventBus或者RxJava,这些都很容易出现内存泄漏问题,而且需要我们手动管理生命周期。而LiveData则规避了这些问题, +// LiveData是一个持有Activity、Fragment生命周期的数据容器。当数据源发生变化的时候,通知它的观察者(相应的界面)更新UI界面。同时它只会通知处于Active状态的观察者更新界面, +// 如果某个观察者的状态处于Paused或Destroyed时那么它将不会收到通知。所以不用担心内存泄漏问题。 + + + public MutableLiveData> getWanAndroidBanner() { + + final MutableLiveData> data = new MutableLiveData<>(); + HttpReq.getInstance().getWanBanner() + .subscribe(new CommonObserver>(this,true) { + @Override + public void success(ResultBeans bannerBean) { + //ResultBean 不会为空不需要做为空判断,因为前面调用onErrorReturn失败new一个空对象 + // if (bannerBean != null){ +// data.setValue(bannerBean); +// } else { +// data.setValue(null); +// } + data.setValue(bannerBean.getData()); + } + + @Override + public void error(Throwable e) { + data.setValue(null); + } + }); + return data; + } + + + + @SuppressLint("CheckResult") + public MutableLiveData> getHomeList(int cid, boolean isRefresh){ + if(isRefresh){ + // mPage = 1; + mPage.set(1); + } + final MutableLiveData> data = new MutableLiveData<>(); + Disposable subscribe= HttpReq.getInstance().getHomeList(mPage.get(),cid) + .subscribe(homeListBean -> { //可以使用CommonObserver弹窗 + if(homeListBean.getErrorCode()==ERROR_CODE){ + ToastUtils.showShort("接口请求失败了~~"); + } + List articlesBeans = new ArrayList<>(); + if(homeListBean.getData()!=null){ + articlesBeans=homeListBean.getData().getDatas(); + if(isListNotNull(articlesBeans)){ + if(isRefresh){ + mList.clear(); + } + mList.addAll(articlesBeans); + } + // mAdapter.setNewData(mList); list改变 无需调用刷新,已封装在BaseMvvmRecyclerAdapter + // mAdapter.notifyDataSetChanged(); + } + + setPage(mList,isRefresh); + data.setValue(mList); + }); + + //没有用 CommonObserver 这里添加addDisposable + addDisposable(subscribe); + return data; + } + + +} diff --git a/app/src/main/java/com/example/baseframe/utils/ADBUtils.java b/app/src/main/java/com/example/baseframe/utils/ADBUtils.java new file mode 100644 index 0000000..c245c69 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ADBUtils.java @@ -0,0 +1,2253 @@ +package com.example.baseframe.utils; + +import android.content.Intent; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.IntRange; + +import com.blankj.ALog; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +/** + * detail: ADB shell 工具类 + * @author Ttt + *
+ *     Awesome ADB 一份超全超详细的 ADB 用法大全
+ *     @see 
+ *     

+ * 获取 APP 列表 ( 包名 ) + * @see
+ *

+ * adb shell input + * @see
+ *

+ * grep 是 linux 下的命令, windows 用 findstr + * 开启 Thread 执行, 非主线程, 否则无响应并无效 + *
+ */ +public final class ADBUtils { + + private ADBUtils() { + } + + // 日志 TAG + private static final String TAG = ADBUtils.class.getSimpleName(); + // 正则表达式: 空格 + private static final String REGEX_SPACE = "\\s"; + // 换行字符串 + private static final String NEW_LINE_STR = System.getProperty("line.separator"); + + /** + * 判断设备是否 root + * @return {@code true} yes, {@code false} no + */ + public static boolean isDeviceRooted() { + String su = "su"; + String[] locations = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", + "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"}; + for (String location : locations) { + if (new File(location + su).exists()) { + return true; + } + } + return false; + } + + /** + * 请求 Root 权限 + * @return {@code true} success, {@code false} fail + */ + public static boolean requestRoot() { + return ShellUtils.execCmd("exit", true).isSuccess(); + } + + /** + * 判断 APP 是否授权 Root 权限 + * @return {@code true} yes, {@code false} no + */ + public static boolean isGrantedRoot() { + return ShellUtils.execCmd("exit", true).isSuccess2(); + } + + // ============ + // = 应用管理 = + // ============ + + // ============ + // = 应用列表 = + // ============ + + /** + * 获取 APP 列表 ( 包名 ) + * @param type options + * @return 对应选项的应用包名列表 + */ + public static List getAppList(final String type) { + // adb shell pm list packages [options] + String typeStr = CommUtils.isEmpty(type) ? "" : " " + type; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("pm list packages" + typeStr, false); + if (result.isSuccess3()) { + try { + String[] arrays = result.successMsg.split(NEW_LINE_STR); + return Arrays.asList(arrays); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppList type: " + typeStr); + } + } + return null; + } + + /** + * 获取 APP 安装列表 ( 包名 ) + * @return APP 安装列表 ( 包名 ) + */ + public static List getInstallAppList() { + return getAppList(null); + } + + /** + * 获取用户安装的应用列表 ( 包名 ) + * @return 用户安装的应用列表 ( 包名 ) + */ + public static List getUserAppList() { + return getAppList("-3"); + } + + /** + * 获取系统应用列表 ( 包名 ) + * @return 系统应用列表 ( 包名 ) + */ + public static List getSystemAppList() { + return getAppList("-s"); + } + + /** + * 获取启用的应用列表 ( 包名 ) + * @return 启用的应用列表 ( 包名 ) + */ + public static List getEnableAppList() { + return getAppList("-e"); + } + + /** + * 获取禁用的应用列表 ( 包名 ) + * @return 禁用的应用列表 ( 包名 ) + */ + public static List getDisableAppList() { + return getAppList("-d"); + } + + /** + * 获取包名包含字符串 xxx 的应用列表 + * @param filter 过滤获取字符串 + * @return 包名包含字符串 xxx 的应用列表 + */ + public static List getAppListToFilter(final String filter) { + if (CommUtils.isEmpty(filter)) return null; + return getAppList("| grep " + filter.trim()); + } + + /** + * 判断是否安装应用 + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isInstalledApp(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + return ShellUtils.execCmd("pm path " + packageName, false).isSuccess3(); + } + + /** + * 查看应用安装路径 + * @param packageName 应用包名 + * @return 应用安装路径 + */ + public static String getAppInstallPath(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("pm path " + packageName, false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 清除应用数据与缓存 - 相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean clearAppDataCache(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + // adb shell pm clear + String cmd = "pm clear %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, packageName), true); + return result.isSuccess4("success"); + } + + // ============ + // = 应用信息 = + // ============ + + /** + * 查看应用详细信息 + *
+     *     输出中包含很多信息, 包括 Activity Resolver Table、Registered ContentProviders、
+     *     包名、userId、安装后的文件资源代码等路径、版本信息、权限信息和授予状态、签名版本信息等
+     * 
+ * @param packageName 应用包名 + * @return 应用详细信息 + */ + public static String getAppMessage(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys package " + packageName, true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 APP versionCode + * @param packageName 应用包名 + * @return versionCode + */ + public static int getVersionCode(final String packageName) { + if (CommUtils.isEmpty(packageName)) return 0; + try { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys package " + packageName + " | grep version", true); + if (result.isSuccess3()) { + String[] arrays = result.successMsg.split(REGEX_SPACE); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + try { + String[] datas = str.split("="); + if (datas.length == 2) { + if (datas[0].toLowerCase().equals("versionCode".toLowerCase())) { + return Integer.parseInt(datas[1]); + } + } + } catch (Exception e) { + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getVersionCode"); + } + return 0; + } + + /** + * 获取 APP versionName + * @param packageName 应用包名 + * @return versionName + */ + public static String getVersionName(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys package " + packageName + " | grep version", true); + if (result.isSuccess3()) { + String[] arrays = result.successMsg.split(REGEX_SPACE); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + try { + String[] datas = str.split("="); + if (datas.length == 2) { + if (datas[0].toLowerCase().equals("versionName".toLowerCase())) { + return datas[1]; + } + } + } catch (Exception e) { + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getVersionName"); + } + return null; + } + + // ============== + // = 安装、卸载 = + // ============== + + /** + * 安装应用 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final String filePath) { + return installApp(filePath, "-rtsd"); + } + + /** + * 安装应用 + *
+     *     -l 将应用安装到保护目录 /mnt/asec
+     *     -r 允许覆盖安装
+     *     -t 允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
+     *     -s 将应用安装到 sdcard
+     *     -d 允许降级覆盖安装
+     *     -g 授予所有运行时权限
+     * 
+ * @param filePath 文件路径 + * @param params 安装选项 + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final String filePath, final String params) { + if (CommUtils.isEmpty(params)) return false; + boolean isRoot = isDeviceRooted(); + // adb install [-lrtsdg] + String cmd = "adb install %s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, params, filePath), isRoot); + // 判断是否成功 + return result.isSuccess4("success"); + } + + /** + * 静默安装应用 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final String filePath) { + return installAppSilent(FileUtils.getFileByPath(filePath), null); + } + + /** + * 静默安装应用 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file) { + return installAppSilent(file, null); + } + + /** + * 静默安装应用 + * @param filePath 文件路径 + * @param params 安装选项 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final String filePath, final String params) { + return installAppSilent(FileUtils.getFileByPath(filePath), params, isDeviceRooted()); + } + + /** + * 静默安装应用 + * @param file 文件 + * @param params 安装选项 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file, final String params) { + return installAppSilent(file, params, isDeviceRooted()); + } + + /** + * 静默安装应用 + * @param file 文件 + * @param params 安装选项 + * @param isRooted 是否 root + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file, final String params, final boolean isRooted) { + if (!FileUtils.isFileExists(file)) return false; + String filePath = '"' + file.getAbsolutePath() + '"'; + String command = "LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install " + (params == null ? "" : params + " ") + filePath; + ShellUtils.CommandResult result = ShellUtils.execCmd(command, isRooted); + return result.isSuccess4("success"); + } + + // = + + /** + * 卸载应用 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallApp(final String packageName) { + return uninstallApp(packageName, false); + } + + /** + * 卸载应用 + * @param packageName 应用包名 + * @param isKeepData -k 参数可选, 表示卸载应用但保留数据和缓存目录 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallApp(final String packageName, final boolean isKeepData) { + if (CommUtils.isEmpty(packageName)) return false; + boolean isRoot = isDeviceRooted(); + // adb uninstall [-k] + String cmd = "adb uninstall "; + if (isKeepData) { + cmd += " -k "; + } + cmd += packageName; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, isRoot); + // 判断是否成功 + return result.isSuccess4("success"); + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName) { + return uninstallAppSilent(packageName, false, isDeviceRooted()); + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @param isKeepData -k 参数可选, 表示卸载应用但保留数据和缓存目录 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName, final boolean isKeepData) { + return uninstallAppSilent(packageName, isKeepData, isDeviceRooted()); + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @param isKeepData -k 参数可选, 表示卸载应用但保留数据和缓存目录 + * @param isRooted 是否 root + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName, final boolean isKeepData, final boolean isRooted) { + if (CommUtils.isEmpty(packageName)) return false; + String command = "LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm uninstall " + (isKeepData ? "-k " : "") + packageName; + ShellUtils.CommandResult result = ShellUtils.execCmd(command, isRooted); + return result.isSuccess4("success"); + } + + // =========== + // = dumpsys = + // =========== + + /** + * 获取对应包名应用启动的 Activity + *
+     *     android.intent.category.LAUNCHER (android.intent.action.MAIN)
+     * 
+ * @param packageName 应用包名 + * @return package.xx.Activity.className + */ + public static String getActivityToLauncher(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + String cmd = "dumpsys package %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, packageName), true); + if (result.isSuccess3()) { + String mainStr = "android.intent.action.MAIN:"; + int start = result.successMsg.indexOf(mainStr); + // 防止都为 null + if (start != -1) { + try { + // 进行裁剪字符串 + String subData = result.successMsg.substring(start + mainStr.length()); + // 进行拆分 + String[] arrays = subData.split(NEW_LINE_STR); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + // 存在包名才处理 + if (str.indexOf(packageName) != -1) { + String[] splitArys = str.split(REGEX_SPACE); + for (String strData : splitArys) { + if (!TextUtils.isEmpty(strData)) { + // 属于 packageName/ 前缀的 + if (strData.indexOf(packageName + "/") != -1) { + // 防止属于 packageName/.xx.Main_Activity + if (strData.indexOf("/.") != -1) { + // packageName/.xx.Main_Activity + // packageName/packageName.xx.Main_Activity + strData = strData.replace("/", "/" + packageName); + } + return strData; + } + } + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityToLauncher " + packageName); + } + } + } + return null; + } + + // =================== + // = 获取当前 Window = + // =================== + + /** + * 获取当前显示的 Window + *
+     *     adb shell dumpsys window -h
+     * 
+ * @return package.xx.Activity.className + */ + public static String getWindowCurrent() { + String cmd = "dumpsys window w | grep \\/ | grep name="; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + try { + String nameStr = "name="; + String[] arrays = result.successMsg.split(NEW_LINE_STR); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + int start = str.indexOf(nameStr); + if (start != -1) { + try { + String subData = str.substring(start + nameStr.length()); + if (subData.indexOf(")") != -1) { + return subData.substring(0, subData.length() - 1); + } + return subData; + } catch (Exception e) { + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getWindowCurrent"); + } + } + return null; + } + + /** + * 获取当前显示的 Window + * @return package/package.xx.Activity.className + */ + public static String getWindowCurrent2() { + String cmd = "dumpsys window windows | grep Current"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + try { + // 拆分换行, 并循环 + String[] arrays = result.successMsg.split(NEW_LINE_STR); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + String[] splitArys = str.split(REGEX_SPACE); + if (splitArys != null && splitArys.length != 0) { + for (String splitStr : splitArys) { + if (!TextUtils.isEmpty(splitStr)) { + int start = splitStr.indexOf("/"); + int lastIndex = splitStr.lastIndexOf("}"); + if (start != -1 && lastIndex != -1) { + // 获取裁剪数据 + String strData = splitStr.substring(0, lastIndex); + // 防止属于 packageName/.xx.Main_Activity + if (strData.indexOf("/.") != -1) { + // packageName/.xx.Main_Activity + // packageName/packageName.xx.Main_Activity + strData = strData.replace("/", "/" + splitStr.substring(0, start)); + } + return strData; + } + } + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getWindowCurrent2"); + } + } + return null; + } + + /** + * 获取对应包名显示的 Window + * @param packageName 应用包名 + * @return package/package.xx.Activity.className + */ + public static String getWindowCurrentToPackage(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + String cmd = "dumpsys window windows | grep %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, packageName), true); + if (result.isSuccess3()) { + try { + // 拆分换行, 并循环 + String[] arrays = result.successMsg.split(NEW_LINE_STR); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + String[] splitArys = str.split(REGEX_SPACE); + if (splitArys != null && splitArys.length != 0) { + for (String splitStr : splitArys) { + if (!TextUtils.isEmpty(splitStr)) { + int start = splitStr.indexOf("/"); + int lastIndex = splitStr.lastIndexOf("}"); + if (start != -1 && lastIndex != -1 && splitStr.indexOf(packageName) == 0) { + // 获取裁剪数据 + String strData = splitStr.substring(0, lastIndex); + // 防止属于 packageName/.xx.Main_Activity + if (strData.indexOf("/.") != -1) { + // packageName/.xx.Main_Activity + // packageName/packageName.xx.Main_Activity + strData = strData.replace("/", "/" + packageName); + } + return strData; + } + } + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getWindowCurrentToPackage"); + } + } + return null; + } + + // ===================== + // = 获取当前 Activity = + // ===================== + + /** + * 获取当前显示的 Activity + * @return package.xx.Activity.className + */ + public static String getActivityCurrent() { + String cmd = "dumpsys activity activities | grep mFocusedActivity"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + cmd = "dumpsys activity activities | grep mResumedActivity"; + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + try { + // 拆分换行, 并循环 + String[] arrays = result.successMsg.split(NEW_LINE_STR); + for (String str : arrays) { + if (!TextUtils.isEmpty(str)) { + String[] splitArys = str.split(REGEX_SPACE); + if (splitArys != null && splitArys.length != 0) { + for (String splitStr : splitArys) { + if (!TextUtils.isEmpty(splitStr)) { + int start = splitStr.indexOf("/"); + if (start != -1) { + // 获取裁剪数据 + String strData = splitStr; + // 防止属于 packageName/.xx.Main_Activity + if (strData.indexOf("/.") != -1) { + // packageName/.xx.Main_Activity + // packageName/packageName.xx.Main_Activity + strData = strData.replace("/", "/" + splitStr.substring(0, start)); + } + return strData; + } + } + } + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityCurrent"); + } + } + return null; + } + + /** + * 获取 Activity 栈 + * @return 当前全部 Activity 栈信息 + */ + public static String getActivitys() { + return getActivitys(null); + } + + /** + * 获取 Activity 栈 + * @param append 追加筛选条件 + * @return 当前全部 Activity 栈信息 + */ + public static String getActivitys(final String append) { + String cmd = "dumpsys activity activities"; + if (!CommUtils.isEmpty(append)) { + cmd += " " + append.trim(); + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取对应包名的 Activity 栈 + * @param packageName 应用包名 + * @return 对应包名的 Activity 栈信息 + */ + public static String getActivitysToPackage(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + return getActivitys("| grep " + packageName); + } + + /** + * 获取对应包名的 Activity 栈 ( 处理成 List) - 最新的 Activity 越靠后 + * @param packageName 应用包名 + * @return 对应包名的 Activity 栈信息集合 + */ + public static List getActivitysToPackageLists(final String packageName) { + // 获取对应包名的 Activity 数据结果 + String result = getActivitysToPackage(packageName); + // 防止数据为 null + if (!TextUtils.isEmpty(result)) { + try { + List lists = new ArrayList<>(); + String[] dataSplit = result.split(NEW_LINE_STR); + // 拆分后, 数据长度 + int splitLength = dataSplit.length; + // 获取 Activity 栈字符串 + String activities = null; + // 判断最后一行是否符合条件 + if (dataSplit[splitLength - 1].indexOf("Activities=") != -1) { + activities = dataSplit[splitLength - 1]; + } else { + for (String str : dataSplit) { + if (str.indexOf("Activities=") != -1) { + activities = str; + break; + } + } + } + // 进行特殊处理 Activities=[ActivityRecord{xx},ActivityRecord{xx}]; + int startIndex = activities.indexOf("Activities=["); + activities = activities.substring(startIndex + "Activities=[".length(), activities.length() - 1); + // 再次进行拆分 + String[] activityArys = activities.split("ActivityRecord"); + for (String data : activityArys) { + try { + String[] splitArys = data.split(REGEX_SPACE); + if (splitArys != null && splitArys.length != 0) { + for (String splitStr : splitArys) { + int start = splitStr.indexOf(packageName + "/"); + if (start != -1) { + // 获取裁剪数据 + String strData = splitStr; + // 防止属于 packageName/.xx.XxxActivity + if (strData.indexOf("/.") != -1) { + // packageName/.xx.XxxActivity + // packageName/packageName.xx.XxxActivity + strData = strData.replace("/", "/" + splitStr.substring(0, start)); + } + // 保存数据 + lists.add(strData); + } + } + } + } catch (Exception e) { + } + } + return lists; + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivitysToPackageLists"); + } + } + return null; + } + + // = + + /** + * 判断 Activity 栈顶是否重复 + * @param packageName 应用包名 + * @param activity Activity Name + * @return {@code true} yes, {@code false} no + */ + public static boolean isActivityTopRepeat(final String packageName, final String activity) { + if (TextUtils.isEmpty(packageName)) { + return false; + } else if (TextUtils.isEmpty(activity)) { + return false; + } + // 判断是否重复 + boolean isRepeat = false; + // 获取 + List lists = getActivitysToPackageLists(packageName); + // 数据长度 + int length = 0; + if (lists != null) { + length = lists.size(); + } + // 防止数据为 null + if (length >= 2) { // 两个页面以上, 才能够判断是否重复 + try { + if (lists.get(length - 1).endsWith(activity)) { + // 倒序遍历, 越后面是 Activity 栈顶 + for (int i = length - 2; i >= 0; i--) { + String data = lists.get(i); + // 判断是否该页面结尾 + return data.endsWith(activity); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "isActivityTopRepeat"); + } + } + return isRepeat; + } + + /** + * 判断 Activity 栈顶是否重复 + * @param packageName 应用包名 + * @param activitys Activity Name 集合 + * @return {@code true} yes, {@code false} no + */ + public static boolean isActivityTopRepeat(final String packageName, final List activitys) { + if (TextUtils.isEmpty(packageName)) { + return false; + } else if (activitys == null || activitys.size() == 0) { + return false; + } + // 判断是否重复 + boolean isRepeat = false; + // 获取 + List lists = getActivitysToPackageLists(packageName); + // 数据长度 + int length = 0; + if (lists != null) { + length = lists.size(); + } + // 防止数据为 null + if (length >= 2) { // 两个页面以上, 才能够判断是否重复 + // 循环判断 + for (String activity : activitys) { + try { + if (lists.get(length - 1).endsWith(activity)) { + // 倒序遍历, 越后面是 Activity 栈顶 + for (int i = length - 2; i >= 0; i--) { + String data = lists.get(i); + // 判断是否该页面结尾 + return data.endsWith(activity); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "isActivityTopRepeat"); + } + } + } + return isRepeat; + } + + // = + + /** + * 获取 Activity 栈顶重复总数 + * @param packageName 应用包名 + * @param activity Activity Name + * @return 指定 Activity 在栈顶重复总数 + */ + public static int getActivityTopRepeatCount(final String packageName, final String activity) { + if (TextUtils.isEmpty(packageName)) { + return 0; + } else if (TextUtils.isEmpty(activity)) { + return 0; + } + // 重复数量 + int number = 0; + // 获取 + List lists = getActivitysToPackageLists(packageName); + // 数据长度 + int length = 0; + if (lists != null) { + length = lists.size(); + } + // 防止数据为 null + if (length >= 2) { // 两个页面以上, 才能够判断是否重复 + try { + if (lists.get(length - 1).endsWith(activity)) { + // 倒序遍历, 越后面是 Activity 栈顶 + for (int i = length - 2; i >= 0; i--) { + String data = lists.get(i); + // 判断是否该页面结尾 + if (data.endsWith(activity)) { + number++; + } else { + break; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityTopRepeatCount"); + } + } + return number; + } + + /** + * 获取 Activity 栈顶重复总数 + * @param packageName 应用包名 + * @param activitys Activity Name 集合 + * @return 指定 Activity 在栈顶重复总数 + */ + public static int getActivityTopRepeatCount(final String packageName, final List activitys) { + if (TextUtils.isEmpty(packageName)) { + return 0; + } else if (activitys == null || activitys.size() == 0) { + return 0; + } + // 获取 + List lists = getActivitysToPackageLists(packageName); + // 数据长度 + int length = 0; + if (lists != null) { + length = lists.size(); + } + // 防止数据为 null + if (length >= 2) { // 两个页面以上, 才能够判断是否重复 + // 循环判断 + for (String activity : activitys) { + try { + // 重复数量 + int number = 0; + // 判断是否对应页面结尾 + if (lists.get(length - 1).endsWith(activity)) { + // 倒序遍历, 越后面是 Activity 栈顶 + for (int i = length - 2; i >= 0; i--) { + String data = lists.get(i); + // 判断是否该页面结尾 + if (data.endsWith(activity)) { + number++; + } else { + break; + } + } + // 进行判断处理 + return number; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityTopRepeatCount"); + } + } + } + return 0; + } + + // ======================= + // = 正在运行的 Services = + // ======================= + + /** + * 查看正在运行的 Services + * @return 运行中的 Services 信息 + */ + public static String getServices() { + return getServices(null); + } + + /** + * 查看正在运行的 Services + * @param packageName 应用包名, 参数不是必须的, 指定 表示查看与某个包名相关的 Services, + * 不指定表示查看所有 Services, 不一定要给出完整的包名, + * 比如运行 adb shell dumpsys activity services org.mazhuang + * 那么包名 org.mazhuang.demo1、org.mazhuang.demo2 和 org.mazhuang123 等相关的 Services 都会列出来 + * @return 运行中的 Services 信息 + */ + public static String getServices(final String packageName) { + String cmd = "dumpsys activity services" + ((CommUtils.isEmpty(packageName) ? "" : " " + packageName)); + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + // ====== + // = am = + // ====== + + /** + * 启动自身应用 + * @return {@code true} success, {@code false} fail + */ + public static boolean startSelfApp() { + return startSelfApp(false); + } + + /** + * 启动自身应用 + * @param closeActivity 是否关闭 Activity 所属的 APP 进程后再启动 Activity + * @return {@code true} success, {@code false} fail + */ + public static boolean startSelfApp(final boolean closeActivity) { + try { + // 获取包名 + String packageName = AppUtils.getPackageName(); + // 获取 Launcher Activity + String activity = ActivityUtils.getLauncherActivity(); + // 跳转应用启动页 ( 启动应用 ) + return startActivity(packageName + "/" + activity, closeActivity); + } catch (Exception e) { + ALog.eTag(TAG, e, "startSelfApp"); + } + return false; + } + + /** + * 跳转页面 Activity + * @param packageAndLauncher package/package.xx.Activity.className + * @param closeActivity 是否关闭 Activity 所属的 APP 进程后再启动 Activity + * @return {@code true} success, {@code false} fail + */ + public static boolean startActivity(final String packageAndLauncher, final boolean closeActivity) { + return startActivity(packageAndLauncher, null, closeActivity); + } + + /** + * 跳转页面 Activity + * @param packageAndLauncher package/package.xx.Activity.className + * @param append 追加的信息, 例如传递参数等 + * @param closeActivity 是否关闭 Activity 所属的 APP 进程后再启动 Activity + * @return {@code true} success, {@code false} fail + */ + public static boolean startActivity(final String packageAndLauncher, final String append, final boolean closeActivity) { + if (CommUtils.isEmpty(packageAndLauncher)) return false; + try { + // am start [options] + String cmd = "am start %s"; + if (closeActivity) { + cmd = String.format(cmd, "-S " + packageAndLauncher); + } else { + cmd = String.format(cmd, packageAndLauncher); + } + // 判断是否追加 + if (!CommUtils.isEmpty(append)) { + cmd += " " + append.trim(); + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "startActivity"); + } + return false; + } + + /** + * 启动服务 + * @param packageAndService package/package.xx.Service.className + * @return {@code true} success, {@code false} fail + */ + public static boolean startService(final String packageAndService) { + return startService(packageAndService, null); + } + + /** + * 启动服务 + * @param packageAndService package/package.xx.Service.className + * @param append 追加的信息, 例如传递参数等 + * @return {@code true} success, {@code false} fail + */ + public static boolean startService(final String packageAndService, final String append) { + if (CommUtils.isEmpty(packageAndService)) return false; + try { + // am startservice [options] + String cmd = "am startservice %s"; + // 进行格式化 + cmd = String.format(cmd, packageAndService); + // 判断是否追加 + if (!CommUtils.isEmpty(append)) { + cmd += " " + append.trim(); + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "startService"); + } + return false; + } + + /** + * 停止服务 + * @param packageAndService package/package.xx.Service.className + * @return {@code true} success, {@code false} fail + */ + public static boolean stopService(final String packageAndService) { + return stopService(packageAndService, null); + } + + /** + * 停止服务 + * @param packageAndService package/package.xx.Service.className + * @param append 追加的信息, 例如传递参数等 + * @return {@code true} success, {@code false} fail + */ + public static boolean stopService(final String packageAndService, final String append) { + if (CommUtils.isEmpty(packageAndService)) return false; + try { + // am stopservice [options] + String cmd = "am stopservice %s"; + // 进行格式化 + cmd = String.format(cmd, packageAndService); + // 判断是否追加 + if (!CommUtils.isEmpty(append)) { + cmd += " " + append.trim(); + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + return result.isSuccess3(); + } catch (Exception e) { + ALog.eTag(TAG, e, "stopService"); + } + return false; + } + + /** + * 发送广播 ( 向所有组件发送 ) + *
+     *     向所有组件广播 BOOT_COMPLETED
+     *     adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
+     * 
+ * @param broadcast 广播 INTENT + * @return {@code true} success, {@code false} fail + */ + public static boolean sendBroadcastToAll(final String broadcast) { + if (CommUtils.isEmpty(broadcast)) return false; + try { + // am broadcast [options] + String cmd = "am broadcast -a %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, broadcast), true); + return result.isSuccess3(); + } catch (Exception e) { + ALog.eTag(TAG, e, "sendBroadcastAll"); + } + return false; + } + + /** + * 发送广播 + *
+     *     只向 org.mazhuang.boottimemeasure/.BootCompletedReceiver 广播 BOOT_COMPLETED
+     *     adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n org.mazhuang.boottimemeasure/.BootCompletedReceiver
+     * 
+ * @param packageAndBroadcast package/package.xx.Receiver.className + * @param broadcast 广播 INTENT + * @return {@code true} success, {@code false} fail + */ + public static boolean sendBroadcast(final String packageAndBroadcast, final String broadcast) { + if (CommUtils.isEmpty(packageAndBroadcast)) return false; + if (CommUtils.isEmpty(broadcast)) return false; + try { + // am broadcast [options] + String cmd = "am broadcast -a %s -n %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, broadcast, packageAndBroadcast), true); + return result.isSuccess3(); + } catch (Exception e) { + ALog.eTag(TAG, e, "sendBroadcast"); + } + return false; + } + + // = + + /** + * 销毁进程 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean kill(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + String cmd = "am force-stop %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, packageName), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "kill"); + } + return false; + } + + /** + * 收紧内存 + * @param pid 进程 ID + * @param level HIDDEN、RUNNING_MODERATE、BACKGROUND、RUNNING_LOW、MODERATE、RUNNING_CRITICAL、COMPLETE + * @return {@code true} success, {@code false} fail + */ + public static boolean sendTrimMemory(final int pid, final String level) { + if (CommUtils.isEmpty(level)) return false; + try { + String cmd = "am send-trim-memory %s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, pid, level), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "sendTrimMemory"); + } + return false; + } + +// // ============ +// // = 文件管理 = +// // ============ +// +// /** +// * 复制设备里的文件到电脑 +// * @param remote 设备里的文件路径 +// * @param local 电脑上的目录 +// * @return {@code true} success, {@code false} fail +// */ +// public static boolean pull(final String remote, final String local) { +// if (CommUtils.isEmpty(remote)) return false; +// try { +// // adb pull <设备里的文件路径> [电脑上的目录] +// String cmd = "adb pull %s"; +// // 判断是否存到默认地址 +// if (!CommUtils.isEmpty(local)) { +// cmd += " " + local; +// } +// // 执行 shell +// ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, remote), true); +// return result.isSuccess2(); +// } catch (Exception e) { +// ALog.eTag(TAG, e, "pull"); +// } +// return false; +// } +// +// /** +// * 复制电脑里的文件到设备 +// * @param local 电脑上的文件路径 +// * @param remote 设备里的目录 +// * @return {@code true} success, {@code false} fail +// */ +// public static boolean push(final String local, final String remote) { +// if (CommUtils.isEmpty(local)) return false; +// if (CommUtils.isEmpty(remote)) return false; +// try { +// // adb push <电脑上的文件路径> <设备里的目录> +// String cmd = "adb push %s %s"; +// // 执行 shell +// ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, local, remote), true); +// return result.isSuccess2(); +// } catch (Exception e) { +// ALog.eTag(TAG, e, "push"); +// } +// return false; +// } + + // ========= + // = Input = + // ========= + + // =============================== + // = tap - 模拟 touch 屏幕的事件 = + // =============================== + + /** + * 点击某个区域 + * @param x X 轴坐标 + * @param y Y 轴坐标 + * @return {@code true} success, {@code false} fail + */ + public static boolean tap(final float x, final float y) { + try { + // input [touchscreen|touchpad|touchnavigation] tap + // input [ 屏幕、触摸板、导航键 ] tap + String cmd = "input touchscreen tap %s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, (int) x, (int) y), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "tap"); + } + return false; + } + + // ==================== + // = swipe - 滑动事件 = + // ==================== + + /** + * 按压某个区域 ( 点击 ) + * @param x X 轴坐标 + * @param y Y 轴坐标 + * @return {@code true} success, {@code false} fail + */ + public static boolean swipeClick(final float x, final float y) { + return swipe(x, y, x, y, 100L); + } + + /** + * 按压某个区域 time 大于一定时间变成长按 + * @param x X 轴坐标 + * @param y Y 轴坐标 + * @param time 按压时间 + * @return {@code true} success, {@code false} fail + */ + public static boolean swipeClick(final float x, final float y, final long time) { + return swipe(x, y, x, y, time); + } + + /** + * 滑动到某个区域 + * @param x X 轴坐标 + * @param y Y 轴坐标 + * @param toX 滑动到 X 轴坐标 + * @param toY 滑动到 Y 轴坐标 + * @param time 滑动时间 ( 毫秒 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean swipe(final float x, final float y, final float toX, final float toY, final long time) { + try { + // input [touchscreen|touchpad|touchnavigation] swipe [duration(ms)] + String cmd = "input touchscreen swipe %s %s %s %s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, (int) x, (int) y, (int) toX, (int) toY, time), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "swipe"); + } + return false; + } + + // =================== + // = text - 模拟输入 = + // =================== + + /** + * 输入文本 - 不支持中文 + * @param txt 文本内容 + * @return {@code true} success, {@code false} fail + */ + public static boolean text(final String txt) { + if (CommUtils.isEmpty(txt)) return false; + try { + // input text + String cmd = "input text %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, txt), true); // false 可以执行 + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "text"); + } + return false; + } + + // ======================= + // = keyevent - 按键操作 = + // ======================= + + /** + * 触发某些按键 + * @param keyCode KeyEvent.xxx 如: KeyEvent.KEYCODE_BACK ( 返回键 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean keyevent(final int keyCode) { + try { + // input keyevent + String cmd = "input keyevent %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, keyCode), true); // false 可以执行 + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "keyevent"); + } + return false; + } + + // ============ + // = 实用功能 = + // ============ + + /** + * 屏幕截图 + * @param path 保存路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean screencap(final String path) { + return screencap(path, 0); + } + + /** + * 屏幕截图 + * @param path 保存路径 + * @param displayId -d display-id 指定截图的显示屏编号 ( 有多显示屏的情况下 ) 默认 0 + * @return {@code true} success, {@code false} fail + */ + public static boolean screencap(final String path, final int displayId) { + if (CommUtils.isEmpty(path)) return false; + try { + String cmd = "screencap -p -d %s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, Math.max(displayId, 0), path), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "screencap"); + } + return false; + } + + /** + * 录制屏幕 ( 以 mp4 格式保存 ) + * @param path 保存路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean screenrecord(final String path) { + return screenrecord(path, null, -1, -1); + } + + /** + * 录制屏幕 ( 以 mp4 格式保存 ) + * @param path 保存路径 + * @param time 录制时长, 单位秒 ( 默认 / 最长 180 秒 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean screenrecord(final String path, final int time) { + return screenrecord(path, null, -1, time); + } + + /** + * 录制屏幕 ( 以 mp4 格式保存到 ) + * @param path 保存路径 + * @param size 视频的尺寸, 比如 1280x720, 默认是屏幕分辨率 + * @param time 录制时长, 单位秒 ( 默认 / 最长 180 秒 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean screenrecord(final String path, final String size, final int time) { + return screenrecord(path, size, -1, time); + } + + /** + * 录制屏幕 ( 以 mp4 格式保存到 ) + * @param path 保存路径 + * @param size 视频的尺寸, 比如 1280x720, 默认是屏幕分辨率 + * @param bitRate 视频的比特率, 默认是 4Mbps + * @param time 录制时长, 单位秒 ( 默认 / 最长 180 秒 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean screenrecord(final String path, final String size, final int bitRate, final int time) { + if (CommUtils.isEmpty(path)) return false; + try { + StringBuilder builder = new StringBuilder(); + builder.append("screenrecord"); + if (!CommUtils.isEmpty(size)) { + builder.append(" --size " + size); + } + if (bitRate > 0) { + builder.append(" --bit-rate " + bitRate); + } + if (time > 0) { + builder.append(" --time-limit " + time); + } + builder.append(" " + path); + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(builder.toString(), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "screenrecord"); + } + return false; + } + + /** + * 查看连接过的 Wifi 密码 + * @return 连接过的 Wifi 密码 + */ + public static String wifiConf() { + try { + String cmd = "cat /data/misc/wifi/*.conf"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmd, true); + if (result.isSuccess3()) { + return result.successMsg; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "wifiConf"); + } + return null; + } + + /** + * 开启 / 关闭 Wifi + * @param open 是否开启 + * @return {@code true} success, {@code false} fail + */ + public static boolean wifiSwitch(final boolean open) { + String cmd = "svc wifi %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, open ? "enable" : "disable"), true); + return result.isSuccess2(); + } + + /** + * 设置系统时间 + * @param time yyyyMMdd.HHmmss 20160823.131500 + * 表示将系统日期和时间更改为 2016 年 08 月 23 日 13 点 15 分 00 秒 + * @return {@code true} success, {@code false} fail + */ + public static boolean setSystemTime(final String time) { + if (CommUtils.isEmpty(time)) return false; + try { + String cmd = "date -s %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, time), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "setSystemTime"); + } + return false; + } + + /** + * 设置系统时间 + * @param time MMddHHmmyyyy.ss 082313152016.00 + * 表示将系统日期和时间更改为 2016 年 08 月 23 日 13 点 15 分 00 秒 + * @return {@code true} success, {@code false} fail + */ + public static boolean setSystemTime2(final String time) { + if (CommUtils.isEmpty(time)) return false; + try { + String cmd = "date %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, time), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "setSystemTime2"); + } + return false; + } + + /** + * 设置系统时间 + * @param time 时间戳转换 MMddHHmmyyyy.ss + * @return {@code true} success, {@code false} fail + */ + public static boolean setSystemTime2(final long time) { + if (time < 0) return false; + try { + String cmd = "date %s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, new SimpleDateFormat("MMddHHmmyyyy.ss").format(time)), true); + return result.isSuccess2(); + } catch (Exception e) { + ALog.eTag(TAG, e, "setSystemTime2"); + } + return false; + } + + // ================ + // = 刷机相关命令 = + // ================ + + /** + * 关机 ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean shutdown() { + try { + ShellUtils.execCmd("reboot -p", true); + Intent intent = new Intent("android.intent.action.ACTION_REQUEST_SHUTDOWN"); + intent.putExtra("android.intent.extra.KEY_CONFIRM", false); + return AppUtils.startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "shutdown"); + } + return false; + } + + /** + * 重启设备 ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean reboot() { + try { + ShellUtils.execCmd("reboot", true); + Intent intent = new Intent(Intent.ACTION_REBOOT); + intent.putExtra("nowait", 1); + intent.putExtra("interval", 1); + intent.putExtra("window", 0); + return AppUtils.sendBroadcast(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "reboot"); + } + return false; + } + + /** + * 重启设备 ( 需要 root 权限 ) - 并进行特殊的引导模式 (recovery、Fastboot) + * @param reason 传递给内核来请求特殊的引导模式, 如 "recovery" + * 重启到 Fastboot 模式 bootloader + * @return {@code true} success, {@code false} fail + */ + public static boolean reboot(final String reason) { + if (CommUtils.isEmpty(reason)) return false; + try { + AppUtils.getPowerManager().reboot(reason); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "reboot"); + } + return false; + } + + /** + * 重启引导到 recovery ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean rebootToRecovery() { + ShellUtils.CommandResult result = ShellUtils.execCmd("reboot recovery", true); + return result.isSuccess2(); + } + + /** + * 重启引导到 bootloader ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean rebootToBootloader() { + ShellUtils.CommandResult result = ShellUtils.execCmd("reboot bootloader", true); + return result.isSuccess2(); + } + + // ============ + // = 滑动方法 = + // ============ + + /** + * 发送事件滑动 + * @param x X 轴坐标 + * @param y Y 轴坐标 + * @param toX 滑动到 X 轴坐标 + * @param toY 滑动到 Y 轴坐标 + * @param number 循环次数 + */ + public static void sendEventSlide(final float x, final float y, final float toX, final float toY, final int number) { + List lists = new ArrayList<>(); + // = 开头 = + lists.add("sendevent /dev/input/event1 3 57 109"); + lists.add("sendevent /dev/input/event1 3 53 " + x); + lists.add("sendevent /dev/input/event1 3 54 " + y); + // 发送 touch 事件 ( 必须使用 0 0 0 配对 ) + lists.add("sendevent /dev/input/event1 1 330 1"); + lists.add("sendevent /dev/input/event1 0 0 0"); + + // 判断方向 ( 手势是否从左到右 ) - View 往左滑, 手势操作往右滑 + boolean isLeftToRight = toX > x; + // 判断方向 ( 手势是否从上到下 ) - View 往上滑, 手势操作往下滑 + boolean isTopToBottom = toY > y; + + // 计算差数 + float diffX = isLeftToRight ? (toX - x) : (x - toX); + float diffY = isTopToBottom ? (toY - y) : (y - toY); + + if (!isLeftToRight) { + diffX = -diffX; + } + + if (!isTopToBottom) { + diffY = -diffY; + } + + // 平均值 + float averageX = diffX / number; + float averageY = diffY / number; + // 上次位置 + int oldX = (int) x; + int oldY = (int) y; + + // 循环处理 + for (int i = 0; i <= number; i++) { + if (averageX != 0f) { + // 进行判断处理 + int calcX = (int) (x + averageX * i); + if (oldX != calcX) { + oldX = calcX; + lists.add("sendevent /dev/input/event1 3 53 " + calcX); + } + } + + if (averageY != 0f) { + // 进行判断处理 + int calcY = (int) (y + averageY * i); + if (oldY != calcY) { + oldY = calcY; + lists.add("sendevent /dev/input/event1 3 54 " + calcY); + } + } + // 每次操作结束发送 + lists.add("sendevent /dev/input/event1 0 0 0"); + } + // = 结尾 = + lists.add("sendevent /dev/input/event1 3 57 4294967295"); + // 释放 touch 事件 ( 必须使用 0 0 0 配对 ) + lists.add("sendevent /dev/input/event1 1 330 0"); + lists.add("sendevent /dev/input/event1 0 0 0"); + + // 执行 shell + ShellUtils.execCmd(lists, true); + } + + // ================ + // = 查看设备信息 = + // ================ + + /** + * 获取 SDK 版本 + * @return SDK 版本 + */ + public static String getSDKVersion() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.build.version.sdk", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 Android 系统版本 + * @return Android 系统版本 + */ + public static String getAndroidVersion() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.build.version.release", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取设备型号 - 如 RedmiNote4X + * @return 设备型号 + */ + public static String getModel() { + // android.os.Build 内部有信息 android.os.Build.MODEL + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.product.model", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取设备品牌 + * @return 设备品牌 + */ + public static String getBrand() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.product.brand", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取设备名 + * @return 设备名 + */ + public static String getDeviceName() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.product.name", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 CPU 支持的 abi 列表 + * @return CPU 支持的 abi 列表 + */ + public static String getCpuAbiList() { + ShellUtils.CommandResult result = ShellUtils.execCmd("cat /system/build.prop | grep ro.product.cpu.abi", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取每个应用程序的内存上限 + * @return 每个应用程序的内存上限 + */ + public static String getAppHeapsize() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop dalvik.vm.heapsize", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取电池状况 + * @return 电池状况 + */ + public static String getBattery() { + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys battery", true); + if (result.isSuccess3()) { // scale 代表最大电量, level 代表当前电量 + return result.successMsg; + } + return null; + } + + /** + * 获取屏幕密度 + * @return 屏幕密度 + */ + public static String getDensity() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop ro.sf.lcd_density", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取屏幕分辨率 + * @return 屏幕分辨率 + */ + public static String getScreenSize() { + ShellUtils.CommandResult result = ShellUtils.execCmd("wm size", true); + if (result.isSuccess3()) { + // 正常返回 Physical size: 1080 x 1920 + // 如果使用命令修改过, 那输出可能是 + // Physical size: 1080 x 1920 + // Override size: 480 x 1024 + // 表明设备的屏幕分辨率原本是 1080px * 1920px, 当前被修改为 480px * 1024px + return result.successMsg; + } + return null; + } + + /** + * 获取显示屏参数 + * @return 显示屏参数 + */ + public static String getDisplays() { + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys window displays", true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 Android id + * @return Android id + */ + public static String getAndroidId() { + ShellUtils.CommandResult result = ShellUtils.execCmd("settings get secure android_id", true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 IMEI 码 + * @return IMEI 码 + */ + public static String getIMEI() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ShellUtils.CommandResult result = ShellUtils.execCmd("service call iphonesubinfo 1", true); + if (result.isSuccess3()) { + try { + int index = 0; + StringBuilder builder = new StringBuilder(); + String subStr = result.successMsg.replaceAll("\\.", ""); + subStr = subStr.substring(subStr.indexOf("'") + 1, subStr.indexOf("')")); + // 添加数据 + builder.append(subStr.substring(0, subStr.indexOf("'"))); + // 从指定索引开始 + index = subStr.indexOf("'", builder.toString().length() + 1); + // 再次裁剪 + subStr = subStr.substring(index + 1); + // 添加数据 + builder.append(subStr.substring(0, subStr.indexOf("'"))); + // 从指定索引开始 + index = subStr.indexOf("'", builder.toString().length() + 1); + // 再次裁剪 + subStr = subStr.substring(index + 1); + // 最后进行添加 + builder.append(subStr.split(REGEX_SPACE)[0]); + // 返回对应的数据 + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getIMEI"); + } + } + } else { + // 在 Android 4.4 及以下版本可通过如下命令获取 IMEI + ShellUtils.CommandResult result = ShellUtils.execCmd("dumpsys iphonesubinfo", true); + if (result.isSuccess3()) { // 返回值中的 Device ID 就是 IMEI + try { + String[] splitArys = result.successMsg.split(NEW_LINE_STR); + for (String str : splitArys) { + if (!TextUtils.isEmpty(str)) { + if (str.toLowerCase().indexOf("device") != -1) { + // 进行拆分 + String[] arrays = str.split(REGEX_SPACE); + return arrays[arrays.length - 1]; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getIMEI"); + } + } + } + return null; + } + + /** + * 获取 IP 地址 + * @return IP 地址 + */ + public static String getIPAddress() { + boolean isRoot = false; + ShellUtils.CommandResult result = ShellUtils.execCmd("ifconfig | grep Mask", isRoot); + if (result.isSuccess3()) { + return result.successMsg; + } else { // 如果设备连着 Wifi, 可以使用如下命令来查看局域网 IP + result = ShellUtils.execCmd("ifconfig wlan0", isRoot); + if (result.isSuccess3()) { + return result.successMsg; + } else { + // 可以看到网络连接名称、启用状态、IP 地址和 Mac 地址等信息 + result = ShellUtils.execCmd("netcfg", isRoot); + if (result.isSuccess3()) { + return result.successMsg; + } + } + } + return null; + } + + /** + * 获取 Mac 地址 + * @return Mac 地址 + */ + public static String getMac() { + ShellUtils.CommandResult result = ShellUtils.execCmd("cat /sys/class/net/wlan0/address", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取 CPU 信息 + * @return CPU 信息 + */ + public static String getCPU() { + ShellUtils.CommandResult result = ShellUtils.execCmd("cat /proc/cpuinfo", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 获取内存信息 + * @return 内存信息 + */ + public static String getMeminfo() { + ShellUtils.CommandResult result = ShellUtils.execCmd("cat /proc/meminfo", false); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + // ============ + // = 修改设置 = + // ============ + + /** + * 设置屏幕大小 + * @param width 屏幕宽度 + * @param height 屏幕高度 + * @return {@code true} success, {@code false} fail + */ + public static boolean setScreenSize(final int width, final int height) { + String cmd = "wm size %sx%s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, width, height), true); + return result.isSuccess2(); + } + + /** + * 恢复原分辨率命令 + * @return {@code true} success, {@code false} fail + */ + public static boolean resetScreen() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("wm size reset", true); + return result.isSuccess2(); + } + + /** + * 设置屏幕密度 + * @param density 屏幕密度 + * @return {@code true} success, {@code false} fail + */ + public static boolean setDensity(final int density) { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("wm density " + density, true); + return result.isSuccess2(); + } + + /** + * 恢复原屏幕密度 + * @return {@code true} success, {@code false} fail + */ + public static boolean resetDensity() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("wm density reset", true); + return result.isSuccess2(); + } + + /** + * 显示区域 ( 设置留白边距 ) + * @param left left padding + * @param top top padding + * @param right right padding + * @param bottom bottom padding + * @return {@code true} success, {@code false} fail + */ + public static boolean setOverscan(final int left, final int top, final int right, final int bottom) { + String cmd = "wm overscan %s,%s,%s,%s"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(String.format(cmd, left, top, right, bottom), true); + return result.isSuccess2(); + } + + /** + * 恢复原显示区域 + * @return {@code true} success, {@code false} fail + */ + public static boolean resetOverscan() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("wm overscan reset", true); + return result.isSuccess2(); + } + + /** + * 获取亮度是否为自动获取 ( 自动调节亮度 ) + * @return 1 开启、0 未开启、-1 未知 + */ + public static int getScreenBrightnessMode() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings get system screen_brightness_mode", true); + if (result.isSuccess3()) { + try { + return Integer.parseInt(result.successMsg); + } catch (Exception e) { + } + } + return -1; + } + + /** + * 设置亮度是否为自动获取 ( 自动调节亮度 ) + * @param isAuto 是否自动调节 + * @return {@code true} success, {@code false} fail + */ + public static boolean setScreenBrightnessMode(final boolean isAuto) { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings put system screen_brightness_mode " + (isAuto ? 1 : 0), true); + return result.isSuccess3(); + } + + /** + * 获取屏幕亮度值 + * @return 屏幕亮度值 + */ + public static String getScreenBrightness() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings get system screen_brightness", true); + if (result.isSuccess3()) { + String suc = result.successMsg; + if (suc.startsWith("\"")) { + suc = suc.substring(1); + } + if (suc.endsWith("\"")) { + suc = suc.substring(0, suc.length() - 1); + } + return suc; + } + return null; + } + + /** + * 更改屏幕亮度值 ( 亮度值在 0-255 之间 ) + * @param brightness 亮度值 + * @return {@code true} success, {@code false} fail + */ + public static boolean setScreenBrightness(@IntRange(from = 0, to = 255) final int brightness) { + if (brightness < 0) { + return false; + } else if (brightness > 255) { + return false; + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings put system screen_brightness " + brightness, true); + return result.isSuccess2(); + } + + /** + * 获取自动锁屏休眠时间 ( 单位毫秒 ) + * @return 自动锁屏休眠时间 + */ + public static String getScreenOffTimeout() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings get system screen_off_timeout", true); + if (result.isSuccess3()) { + return result.successMsg; + } + return null; + } + + /** + * 设置自动锁屏休眠时间 ( 单位毫秒 ) + *
+     *     设置永不休眠 Integer.MAX_VALUE
+     * 
+ * @param time 休眠时间 ( 单位毫秒 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean setScreenOffTimeout(final long time) { + if (time <= 0) { + return false; + } + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings put system screen_off_timeout " + time, true); + return result.isSuccess2(); + } + + /** + * 获取日期时间选项中通过网络获取时间的状态 + * @return 1 允许、0 不允许、-1 未知 + */ + public static int getGlobalAutoTime() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings get global auto_time", true); + if (result.isSuccess3()) { + try { + return Integer.parseInt(result.successMsg); + } catch (Exception e) { + } + } + return -1; + } + + /** + * 修改日期时间选项中通过网络获取时间的状态, 设置是否开启 + * @param isOpen 是否设置通过网络获取时间 + * @return {@code true} success, {@code false} fail + */ + public static boolean setGlobalAutoTime(final boolean isOpen) { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings put global auto_time " + (isOpen ? 1 : 0), true); + return result.isSuccess3(); + } + + /** + * 关闭 USB 调试模式 + * @return {@code true} success, {@code false} fail + */ + public static boolean disableADB() { + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd("settings put global adb_enabled 0", true); + return result.isSuccess2(); + } + + /** + * 允许访问非 SDK API + *
+     *     不需要设备获得 Root 权限
+     * 
+ * @return 执行结果 + */ + public static int putHiddenApi() { + String[] cmds = new String[2]; + cmds[0] = "settings put global hidden_api_policy_pre_p_apps 1"; + cmds[1] = "settings put global hidden_api_policy_p_apps 1"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmds, true); + return result.result; + } + + /** + * 禁止访问非 SDK API + *
+     *     不需要设备获得 Root 权限
+     * 
+ * @return 执行结果 + */ + public static int deleteHiddenApi() { + String[] cmds = new String[2]; + cmds[0] = "settings delete global hidden_api_policy_pre_p_apps"; + cmds[1] = "settings delete global hidden_api_policy_p_apps"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmds, true); + return result.result; + } + + /** + * 开启无障碍辅助功能 + * @param packageName 应用包名 + * @param accessibilityServiceName 无障碍服务名 + * @return {@code true} success, {@code false} fail + */ + public static boolean openAccessibility(final String packageName, final String accessibilityServiceName) { + if (CommUtils.isEmpty(packageName)) return false; + if (CommUtils.isEmpty(accessibilityServiceName)) return false; + + String cmd = "settings put secure enabled_accessibility_services %s/%s"; + // 格式化 shell 命令 + String[] cmds = new String[2]; + cmds[0] = String.format(cmd, packageName, accessibilityServiceName); + cmds[1] = "settings put secure accessibility_enabled 1"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmds, true); + return result.isSuccess2(); + } + + /** + * 关闭无障碍辅助功能 + * @param packageName 应用包名 + * @param accessibilityServiceName 无障碍服务名 + * @return {@code true} success, {@code false} fail + */ + public static boolean closeAccessibility(final String packageName, final String accessibilityServiceName) { + if (CommUtils.isEmpty(packageName)) return false; + if (CommUtils.isEmpty(accessibilityServiceName)) return false; + + String cmd = "settings put secure enabled_accessibility_services %s/%s"; + // 格式化 shell 命令 + String[] cmds = new String[2]; + cmds[0] = String.format(cmd, packageName, accessibilityServiceName); + cmds[1] = "settings put secure accessibility_enabled 0"; + // 执行 shell + ShellUtils.CommandResult result = ShellUtils.execCmd(cmds, true); + return result.isSuccess2(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/ActivityUtils.java b/app/src/main/java/com/example/baseframe/utils/ActivityUtils.java new file mode 100644 index 0000000..b7698f4 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ActivityUtils.java @@ -0,0 +1,989 @@ +package com.example.baseframe.utils; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.util.Pair; +import androidx.fragment.app.FragmentActivity; + +import com.blankj.ALog; +import com.example.baseframe.api.App; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.UUID; + +/** + * detail: Activity 工具类 ( 包含 Activity 控制管理 ) + * @author Ttt + *
+ *     转场动画
+ *     @see 
+ *     @see 
+ *     ActivityOptionsCompat.makeScaleUpAnimation(source, startX, startY, startWidth, startHeight)
+ *     @see 
+ * 
+ */ +public final class ActivityUtils { + + private ActivityUtils() { + } + + // 日志 TAG + private static final String TAG = ActivityUtils.class.getSimpleName(); + + // ===================== + // = Activity 判断处理 = + // ===================== + + /** + * 通过 Context 获取 Activity + * @param context {@link Context} + * @return {@link Activity} + */ + public static Activity getActivity(final Context context) { + if (context != null) { + try { + return (Activity) context; + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivity"); + } + } + return null; + } + + /** + * 获取 View context 所属的 Activity + * @param view {@link View} + * @return {@link Activity} + */ + public static Activity getActivity(final View view) { + if (view != null) { + try { + Context context = view.getContext(); + while (context instanceof ContextWrapper) { + if (context instanceof Activity) { + return (Activity) context; + } + context = ((ContextWrapper) context).getBaseContext(); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivity"); + } + } + return null; + } + + // = + + /** + * 判断 Activity 是否关闭 + * @param activity {@link Activity} + * @return {@code true} yes, {@code false} no + */ + public static boolean isFinishing(final Activity activity) { + if (activity != null) { + return activity.isFinishing(); + } + return false; + } + + /** + * 判断 Activity 是否关闭 + * @param context {@link Context} + * @return {@code true} yes, {@code false} no + */ + public static boolean isFinishing(final Context context) { + if (context != null) { + try { + return ((Activity) context).isFinishing(); + } catch (Exception e) { + ALog.eTag(TAG, e, "isFinishing"); + } + } + return false; + } + + // = + + /** + * 判断是否存在指定的 Activity + * @param className Activity.class.getCanonicalName() + * @return {@code true} 存在, {@code false} 不存在 + */ + public static boolean isActivityExists(final String className) { + return isActivityExists(AppUtils.getPackageName(), className); + } + + /** + * 判断是否存在指定的 Activity + * @param packageName 应用包名 + * @param className Activity.class.getCanonicalName() + * @return {@code true} 存在, {@code false} 不存在 + */ + public static boolean isActivityExists(final String packageName, final String className) { + if (packageName == null || className == null) return false; + boolean result = true; + try { + PackageManager packageManager = AppUtils.getPackageManager(); + Intent intent = new Intent(); + intent.setClassName(packageName, className); + if (packageManager.resolveActivity(intent, 0) == null) { + result = false; + } else if (intent.resolveActivity(packageManager) == null) { + result = false; + } else { + List lists = packageManager.queryIntentActivities(intent, 0); + if (lists.size() == 0) { + result = false; + } + } + } catch (Exception e) { + result = false; + ALog.eTag(TAG, e, "isActivityExists"); + } + return result; + } + + // ===================== + // = Activity 获取操作 = + // ===================== + + /** + * 回到桌面 ( 同点击 Home 键效果 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean startHomeActivity() { + try { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_HOME); + return AppUtils.startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "startHomeActivity"); + } + return false; + } + + /** + * 获取 Launcher activity + * @return package.xx.Activity.className + */ + public static String getLauncherActivity() { + try { + return getLauncherActivity(AppUtils.getPackageName()); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLauncherActivity"); + } + return null; + } + + /** + * 获取 Launcher activity + * @param packageName 应用包名 + * @return package.xx.Activity.className + */ + public static String getLauncherActivity(final String packageName) { + if (packageName == null) return null; + try { + Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + List lists = AppUtils.getPackageManager().queryIntentActivities(intent, 0); + for (ResolveInfo resolveinfo : lists) { + if (resolveinfo != null && resolveinfo.activityInfo != null) { + if (resolveinfo.activityInfo.packageName.equals(packageName)) { + return resolveinfo.activityInfo.name; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getLauncherActivity"); + } + return null; + } + + /** + * 获取 Activity 对应的 icon + * @param clazz Activity.class + * @return {@link Drawable} Activity 对应的 icon + */ + public static Drawable getActivityIcon(final Class clazz) { + if (clazz == null) return null; + try { + return getActivityIcon(new ComponentName(App.getInstance(), clazz)); + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityIcon"); + } + return null; + } + + /** + * 获取 Activity 对应的 icon + * @param componentName {@link ComponentName} + * @return {@link Drawable} Activity 对应的 icon + */ + public static Drawable getActivityIcon(final ComponentName componentName) { + if (componentName == null) return null; + try { + return AppUtils.getPackageManager().getActivityIcon(componentName); + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityIcon"); + } + return null; + } + + /** + * 获取 Activity 对应的 logo + * @param clazz Activity.class + * @return {@link Drawable} Activity 对应的 logo + */ + public static Drawable getActivityLogo(final Class clazz) { + if (clazz == null) return null; + try { + return getActivityLogo(new ComponentName(App.getInstance(), clazz)); + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityLogo"); + } + return null; + } + + /** + * 获取 Activity 对应的 logo + * @param componentName {@link ComponentName} + * @return {@link Drawable} Activity 对应的 logo + */ + public static Drawable getActivityLogo(final ComponentName componentName) { + if (componentName == null) return null; + try { + return AppUtils.getPackageManager().getActivityLogo(componentName); + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityLogo"); + } + return null; + } + + /** + * 获取对应包名应用启动的 Activity + * @return package.xx.Activity.className + */ + public static String getActivityToLauncher() { + return getActivityToLauncher(AppUtils.getPackageName()); + } + + /** + * 获取对应包名应用启动的 Activity + *
+     *     android.intent.category.LAUNCHER (android.intent.action.MAIN)
+     * 
+ * @param packageName 应用包名 + * @return package.xx.Activity.className + */ + public static String getActivityToLauncher(final String packageName) { + if (packageName == null) return null; + try { + // 创建一个类别为 CATEGORY_LAUNCHER 的该包名的 Intent + Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null); + resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER); + resolveIntent.setPackage(packageName); + // 通过 AppUtils.getPackageManager() 的 queryIntentActivities 方法遍历 + List lists = AppUtils.getPackageManager().queryIntentActivities(resolveIntent, 0); + for (ResolveInfo resolveinfo : lists) { + if (resolveinfo != null && resolveinfo.activityInfo != null) { + // resolveinfo.activityInfo.packageName; // packageName + // 这个就是该 APP 的 LAUNCHER 的 Activity [ 组织形式: packageName.mainActivityname ] + return resolveinfo.activityInfo.name; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getActivityToLauncher"); + } + return null; + } + + /** + * 获取系统桌面信息 + * @return {@link ResolveInfo} + */ + public static ResolveInfo getLauncherCategoryHomeToResolveInfo() { + try { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_HOME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return AppUtils.getPackageManager().resolveActivity(intent, 0); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLauncherCategoryHomeToResolveInfo"); + } + return null; + } + + /** + * 获取系统桌面信息 - packageName + *
+     *     注: 存在多个桌面时且未指定默认桌面时, 该方法返回 Null, 使用时需处理这个情况
+     * 
+ * @return packageName + */ + public static String getLauncherCategoryHomeToPackageName() { + ResolveInfo resolveinfo = getLauncherCategoryHomeToResolveInfo(); + if (resolveinfo != null && resolveinfo.activityInfo != null) { + // 有多个桌面程序存在, 且未指定默认项时 + if (resolveinfo.activityInfo.packageName.equals("android")) { + return null; + } else { + return resolveinfo.activityInfo.packageName; + } + } + return null; + } + + /** + * 获取系统桌面信息 - activityName + * @return activityName + */ + public static String getLauncherCategoryHomeToActivityName() { + ResolveInfo resolveinfo = getLauncherCategoryHomeToResolveInfo(); + if (resolveinfo != null && resolveinfo.activityInfo != null) { + // 有多个桌面程序存在, 且未指定默认项时 + if (resolveinfo.activityInfo.packageName.equals("android")) { + return null; + } else { + return resolveinfo.activityInfo.name; + } + } + return null; + } + + /** + * 获取系统桌面信息 - package/activityName + * @return package/activityName + */ + public static String getLauncherCategoryHomeToPackageAndName() { + ResolveInfo resolveinfo = getLauncherCategoryHomeToResolveInfo(); + if (resolveinfo != null && resolveinfo.activityInfo != null) { + // 有多个桌面程序存在, 且未指定默认项时 + if (resolveinfo.activityInfo.packageName.equals("android")) { + return null; + } else { + // 判断是否. 开头 + String name = resolveinfo.activityInfo.name; + if (name != null) { + // 判断是否 . 开头 + if (name.startsWith(".")) { + name = resolveinfo.activityInfo.packageName + name; + } + return resolveinfo.activityInfo.packageName + "/" + name; + } + } + } + return null; + } + + // ============ + // = 转场动画 = + // ============ + + /** + * 设置跳转动画 + * @param context {@link Context} + * @param enterAnim 进入动画 + * @param exitAnim 退出动画 + * @return {@link Bundle} + */ + public static Bundle getOptionsBundle(final Context context, final int enterAnim, final int exitAnim) { + try { + return ActivityOptionsCompat.makeCustomAnimation(context, enterAnim, exitAnim).toBundle(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getOptionsBundle"); + } + return null; + } + + /** + * 设置跳转动画 + * @param activity {@link Activity} + * @param sharedElements 转场动画 View + * @return {@link Bundle} + */ + public static Bundle getOptionsBundle(final Activity activity, final View[] sharedElements) { + if (activity == null) return null; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + int len = sharedElements.length; + @SuppressWarnings("unchecked") + Pair[] pairs = new Pair[len]; + for (int i = 0; i < len; i++) { + pairs[i] = Pair.create(sharedElements[i], sharedElements[i].getTransitionName()); + } + return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, pairs).toBundle(); + } + return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, null, null).toBundle(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getOptionsBundle"); + } + return null; + } + + // ===================== + // = Activity 管理控制 = + // ===================== + + // ActivityUtils 实例 + private static final ActivityUtils INSTANCE = new ActivityUtils(); + + /** + * 获取 ActivityUtils 管理实例 + * @return {@link ActivityUtils} + */ + public static ActivityUtils getManager() { + return INSTANCE; + } + + // =================== + // = Activity 栈处理 = + // =================== + + // Activity 栈 ( 后进先出 ) + private final Stack mActivityStacks = new Stack<>(); + + /** + * 获取 Activity 栈 + * @return {@link Stack} + */ + public Stack getActivityStacks() { + return mActivityStacks; + } + + /** + * 添加 Activity + * @param activity {@link Activity} + * @return {@link ActivityUtils} + */ + public ActivityUtils addActivity(final Activity activity) { + if (activity != null) { + synchronized (mActivityStacks) { + if (mActivityStacks.contains(activity)) { + return this; + } + mActivityStacks.add(activity); + } + } + return this; + } + + /** + * 移除 Activity + * @param activity {@link Activity} + * @return {@link ActivityUtils} + */ + public ActivityUtils removeActivity(final Activity activity) { + if (activity != null) { + synchronized (mActivityStacks) { + int index = mActivityStacks.indexOf(activity); + if (index == -1) { + return this; + } + try { + mActivityStacks.remove(index); + } catch (Exception e) { + ALog.eTag(TAG, e, "removeActivity"); + } + } + } + return this; + } + + /** + * 移除多个 Activity + * @param activitys Activity[] + * @return {@link ActivityUtils} + */ + public ActivityUtils removeActivity(final Activity... activitys) { + if (activitys != null && activitys.length != 0) { + for (int i = 0, len = activitys.length; i < len; i++) { + removeActivity(activitys[i]); + } + } + return this; + } + + /** + * 获取最后一个 ( 当前 ) Activity + * @return 最后一个 ( 当前 ) {@link Activity} + */ + public Activity currentActivity() { + return mActivityStacks.lastElement(); + } + + /** + * 关闭最后一个 ( 当前 ) Activity + * @return {@link ActivityUtils} + */ + public ActivityUtils finishActivity() { + return finishActivity(mActivityStacks.lastElement()); + } + + /** + * 检测是否包含指定的 Activity + * @param clazzs Class(Activity)[] + * @return {@code true} yes, {@code false} no + */ + public boolean existActivitys(final Class... clazzs) { + if (clazzs != null && clazzs.length != 0) { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + try { + // 进行遍历判断 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + if (activity != null && !activity.isFinishing()) { + for (int i = 0, len = clazzs.length; i < len; i++) { + if (clazzs[i] != null && activity.getClass().getName().equals(clazzs[i].getName())) { + return true; + } + } + } + } + } finally { + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + } + } + return false; + } + + /** + * 关闭指定 Activity + * @param activity {@link Activity} + * @return {@link ActivityUtils} + */ + public ActivityUtils finishActivity(final Activity activity) { + // 先移除 Activity + removeActivity(activity); + // Activity 不为 null, 并且属于未销毁状态 + if (activity != null && !activity.isFinishing()) { + activity.finish(); + } + return this; + } + + /** + * 关闭多个 Activity + * @param activitys Activity[] + * @return {@link ActivityUtils} + */ + public ActivityUtils finishActivity(final Activity... activitys) { + if (activitys != null && activitys.length != 0) { + for (int i = 0, len = activitys.length; i < len; i++) { + finishActivity(activitys[i]); + } + } + return this; + } + + /** + * 关闭指定类名 Activity + * @param clazz Activity.class + * @return {@link ActivityUtils} + */ + public ActivityUtils finishActivity(final Class clazz) { + if (clazz != null) { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + // 清空全部, 便于后续操作处理 + mActivityStacks.clear(); + // 进行遍历移除 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + // 判断是否想要关闭的 Activity + if (activity != null) { + if (activity.getClass() == clazz) { + // 如果 Activity 没有 finish 则进行 finish + if (!activity.isFinishing()) { + activity.finish(); + } + // 删除对应的 Item + iterator.remove(); + } + } else { + // 删除对应的 Item + iterator.remove(); + } + } + // 把不符合条件的保存回去 + mActivityStacks.addAll(stack); + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + } + return this; + } + + /** + * 结束多个类名 Activity + * @param clazzs Class(Activity)[] + * @return {@link ActivityUtils} + */ + public ActivityUtils finishActivity(final Class... clazzs) { + if (clazzs != null && clazzs.length != 0) { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + // 清空全部, 便于后续操作处理 + mActivityStacks.clear(); + // 判断是否销毁 + boolean isRemove; + // 进行遍历移除 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + // 判断是否想要关闭的 Activity + if (activity != null) { + // 默认不需要销毁 + isRemove = false; + // 循环判断 + for (int i = 0, len = clazzs.length; i < len; i++) { + // 判断是否相同 + if (activity.getClass() == clazzs[i]) { + isRemove = true; + break; + } + } + // 判断是否销毁 + if (isRemove) { + // 如果 Activity 没有 finish 则进行 finish + if (!activity.isFinishing()) { + activity.finish(); + } + // 删除对应的 Item + iterator.remove(); + } + } else { + // 删除对应的 Item + iterator.remove(); + } + } + // 把不符合条件的保存回去 + mActivityStacks.addAll(stack); + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + } + return this; + } + + /** + * 结束全部 Activity 除忽略的 Activity 外 + * @param clazz Activity.class + * @return {@link ActivityUtils} + */ + public ActivityUtils finishAllActivityToIgnore(final Class clazz) { + if (clazz != null) { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + // 清空全部, 便于后续操作处理 + mActivityStacks.clear(); + // 进行遍历移除 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + // 判断是否想要关闭的 Activity + if (activity != null) { + if (!(activity.getClass() == clazz)) { + // 如果 Activity 没有 finish 则进行 finish + if (!activity.isFinishing()) { + activity.finish(); + } + // 删除对应的 Item + iterator.remove(); + } + } else { + // 删除对应的 Item + iterator.remove(); + } + } + // 把不符合条件的保存回去 + mActivityStacks.addAll(stack); + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + } + return this; + } + + /** + * 结束全部 Activity 除忽略的 Activity 外 + * @param clazzs Class(Activity)[] + * @return {@link ActivityUtils} + */ + public ActivityUtils finishAllActivityToIgnore(final Class... clazzs) { + if (clazzs != null && clazzs.length != 0) { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + // 清空全部, 便于后续操作处理 + mActivityStacks.clear(); + // 判断是否销毁 + boolean isRemove; + // 进行遍历移除 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + // 判断是否想要关闭的 Activity + if (activity != null) { + // 默认需要销毁 + isRemove = true; + // 循环判断 + for (int i = 0, len = clazzs.length; i < len; i++) { + // 判断是否相同 + if (activity.getClass() == clazzs[i]) { + isRemove = false; + break; + } + } + // 判断是否销毁 + if (isRemove) { + // 如果 Activity 没有 finish 则进行 finish + if (!activity.isFinishing()) { + activity.finish(); + } + // 删除对应的 Item + iterator.remove(); + } + } else { + // 删除对应的 Item + iterator.remove(); + } + } + // 把不符合条件的保存回去 + mActivityStacks.addAll(stack); + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + } + return this; + } + + /** + * 结束所有 Activity + * @return {@link ActivityUtils} + */ + public ActivityUtils finishAllActivity() { + synchronized (mActivityStacks) { + // 保存新的堆栈, 防止出现同步问题 + Stack stack = new Stack<>(); + stack.addAll(mActivityStacks); + // 清空全部, 便于后续操作处理 + mActivityStacks.clear(); + // 进行遍历移除 + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + Activity activity = iterator.next(); + if (activity != null && !activity.isFinishing()) { + activity.finish(); + // 删除对应的 Item + iterator.remove(); + } + } + // 移除数据, 并且清空内存 + stack.clear(); + stack = null; + } + return this; + } + + // = + + /** + * 退出应用程序 + * @return {@link ActivityUtils} + */ + public ActivityUtils exitApplication() { + try { + finishAllActivity(); + // 退出 JVM (Java 虚拟机 ) 释放所占内存资源, 0 表示正常退出、非 0 的都为异常退出 + System.exit(0); + // 从操作系统中结束掉当前程序的进程 + android.os.Process.killProcess(android.os.Process.myPid()); + } catch (Exception e) { + ALog.eTag(TAG, e, "exitApplication"); + // = + System.exit(-1); + } + return this; + } + + /** + * 重启 APP + * @return {@link ActivityUtils} + */ + public ActivityUtils restartApplication() { + try { + Intent intent = AppUtils.getPackageManager().getLaunchIntentForPackage(AppUtils.getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + AppUtils.startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "restartApplication"); + } + return this; + } + + // ============ + // = 跳转回传 = + // ============ + + // 跳转回传回调 Map + private static final Map sResultCallbackMaps = new HashMap<>(); + + /** + * Activity 跳转回传 + * @param resultCallback Activity 跳转回传回调 + * @return {@code true} success, {@code false} fail + */ + public static boolean startActivityForResult(final ResultCallback resultCallback) { + return ActivityUtils.ResultActivity.start(resultCallback); + } + + /** + * detail: Activity 跳转回传回调 + * @author Ttt + */ + public interface ResultCallback { + + /** + * 跳转 Activity 操作 + *
+         *     跳转失败, 必须返回 false 内部会根据返回值关闭 ResultActivity
+         *     必须返回正确的值, 表示是否跳转成功
+         * 
+ * @param activity {@link Activity} + * @return {@code true} success, {@code false} fail + */ + boolean onStartActivityForResult(Activity activity); + + /** + * 回传处理 + * @param result resultCode 是否等于 {@link Activity#RESULT_OK} + * @param resultCode resultCode + * @param data 回传数据 + */ + void onActivityResult(boolean result, int resultCode, Intent data); + } + + /** + * detail: 回传结果处理 Activity + * @author Ttt + */ + public static class ResultActivity extends FragmentActivity { + + // 日志 TAG + private static final String TAG = ResultActivity.class.getSimpleName(); + // 传参 UUID Key + private static final String EXTRA_UUID = "uuid"; + // 跳转回传回调 + private ResultCallback mResultCallback; + // 跳转回传回调 + private Integer mUUIDHash; + + /** + * 跳转回传结果处理 Activity 内部方法 + * @param resultCallback Activity 跳转回传回调 + * @return {@code true} success, {@code false} fail + */ + protected static boolean start(final ResultCallback resultCallback) { + int uuid = -1; + boolean result = false; + if (resultCallback != null) { + uuid = randomUUIDToHashCode(); + while (sResultCallbackMaps.containsKey(uuid)) { + uuid = randomUUIDToHashCode(); + } + sResultCallbackMaps.put(uuid, resultCallback); + try { + Intent intent = new Intent(App.getInstance(), ResultActivity.class); + intent.putExtra(EXTRA_UUID, uuid); + result = AppUtils.startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "start"); + } + } + if (!result && uuid != -1) { + sResultCallbackMaps.remove(uuid); + } + return result; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + boolean result = false; // 跳转结果 + try { + mUUIDHash = getIntent().getIntExtra(EXTRA_UUID, -1); + mResultCallback = sResultCallbackMaps.get(mUUIDHash); + result = mResultCallback.onStartActivityForResult(this); + } catch (Exception e) { + ALog.eTag(TAG, e, "onCreate"); + } + if (!result) { + if (mResultCallback != null) { + mResultCallback.onActivityResult(false, Activity.RESULT_CANCELED, null); + } + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mResultCallback != null) { + mResultCallback.onActivityResult(resultCode == Activity.RESULT_OK, resultCode, data); + } + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // 移除操作 + sResultCallbackMaps.remove(mUUIDHash); + } + } + + /** + * 获取随机唯一数 HashCode + * @return 随机 UUID hashCode + */ + public static int randomUUIDToHashCode() { + return UUID.randomUUID().hashCode(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/AnimationsContainer.java b/app/src/main/java/com/example/baseframe/utils/AnimationsContainer.java new file mode 100644 index 0000000..fba56c5 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/AnimationsContainer.java @@ -0,0 +1,315 @@ +package com.example.baseframe.utils; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.os.Handler; +import android.widget.ImageView; + +import androidx.annotation.ArrayRes; + +import java.io.InputStream; +import java.lang.ref.SoftReference; + + +/** + * 帧动画加载工具类 + * Android帧动画很多图片时一次性加载会造成ui卡顿,所以就有了这份代码。通过handle队列和Bitmap复用 + * 每次加载一张并显示,可以解决帧动画卡顿问题。 + * Created by yzh on 2020/1/8 14:21. + */ +public class AnimationsContainer { + + private static AnimationsContainer mInstance; + + public static AnimationsContainer getInstance() { + if(mInstance==null){ + synchronized (AnimationsContainer.class){ + if(mInstance==null){ + mInstance = new AnimationsContainer(); + } + } + } + return mInstance; + } + + /** + * 创建一个 Imageview 的播放动画类 + * @param imageView 要播放动画的ImageView + * @param resId 资源id数组 R.array.dj_end_anim @drawable/dj_end_sh00 + */ + public FramesSequenceAnimation create(ImageView imageView, @ArrayRes int resId) { + return new FramesSequenceAnimation(imageView, CommUtils.getArrays(resId)); + } + + + /** + * 循环读取帧---循环播放帧 + */ + public class FramesSequenceAnimation { + private int[] mFrames; // 帧数组 + private int mIndex; // 当前帧 + private boolean mShouldRun; // 开始/停止播放用 + private boolean mIsRunning; // 动画是否正在播放,防止重复播放 + private SoftReference mSoftReferenceImageView; // 软引用ImageView,以便及时释放掉 + private Handler mHandler; + private long mDelayMillis = 58; + private boolean isLoop = false; + private boolean isGoBack = true; + private OnAnimationListener mOnAnimationListener; //播放停止监听 + + private Bitmap mBitmap = null; + private BitmapFactory.Options mBitmapOptions;//Bitmap管理类,可有效减少Bitmap的OOM问题 + + public FramesSequenceAnimation(ImageView imageView, int[] frames) { + mHandler = new Handler(); + mFrames = frames; + mIndex = -1; + mSoftReferenceImageView = new SoftReference(imageView); + mShouldRun = false; + mIsRunning = false; + setResource(imageView); + + // 当图片大小类型相同时进行复用,避免频繁GC + if (Build.VERSION.SDK_INT >= 11) { + Bitmap bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); + int width = bmp.getWidth(); + int height = bmp.getHeight(); + Bitmap.Config config = bmp.getConfig(); + mBitmap = Bitmap.createBitmap(width, height, config); + mBitmapOptions = new BitmapFactory.Options(); + //设置Bitmap内存复用 + mBitmapOptions.inBitmap = mBitmap;//Bitmap复用内存块,类似对象池,避免不必要的内存分配和回收 + mBitmapOptions.inMutable = true;//解码时返回可变Bitmap + mBitmapOptions.inSampleSize = 1;//缩放比例 + } + } + + private void setResource(ImageView imageView) { + // imageView.setImageResource(mFrames[0]); + //这里不能使用image.setImageResource(会不流畅) 因为源码中也是创建了bitmap 所以这里自己创建 + Bitmap bitmap = readBitMap(imageView.getContext(),mFrames[0]); + imageView.setImageBitmap(bitmap); + } + + /** + * 循环读取下一帧 + */ + private int getNext() { + mIndex++; + if (mIndex < mFrames.length) { + return mFrames[mIndex]; + } else if (mIndex >= mFrames.length && isLoop) { + return mFrames[mIndex = 0]; + } else if (isGoBack) { + end(); + return mFrames[mIndex = 0]; + } else { + end(); + return mIndex = -1; + } + } + + /** + * How long this animation should last. The duration cannot be negative. + * 与setDelayMillis(long)方法二选一 + * + * @param duration Duration in milliseconds + * @throws IllegalArgumentException if the duration is < 0 + * @attr ref android.R.styleable#Animation_duration + * @see #setDelayMillis(long) + */ + public FramesSequenceAnimation setDuration(long duration) { + if (mFrames.length == 0) { + throw new IllegalArgumentException("Animation frame length == 0"); + } + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + mDelayMillis = duration / mFrames.length; + return this; + } + + /** + * 直接设置每帧间隔时间,与 setDuration(long) 方法二选一 + * + * @param delayMillis 每帧间隔时间,数值推荐:58,单位:毫秒 + * @throws IllegalArgumentException if the duration is < 0 + * @attr ref android.R.styleable#Animation_duration + * @see #setDuration(long) + */ + public FramesSequenceAnimation setDelayMillis(long delayMillis) { + if (delayMillis < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + delayMillis); + } + mDelayMillis = delayMillis; + return this; + } + + /** + * 是否循环播放 + * + * @param isLoop + */ + public synchronized FramesSequenceAnimation setLoop(boolean isLoop) { + this.isLoop = isLoop; + return this; + } + + /** + * 是否返回第一帧 + * + * @param isGoBack + */ + public synchronized FramesSequenceAnimation setGoBack(boolean isGoBack) { + this.isGoBack = isGoBack; + return this; + } + + /** + * 播放动画,同步锁防止多线程读帧时,数据安全问题 + */ + public synchronized void start() { + mShouldRun = true; + if (mIsRunning) + return; + + Runnable runnable = new Runnable() { + @Override + public void run() { + ImageView imageView = mSoftReferenceImageView.get(); + if (!mShouldRun || imageView == null) { + mIsRunning = false; + return; + } + + mIsRunning = true; + //新开任务到队尾去读下一帧 + mHandler.postDelayed(this, mDelayMillis); + + if (imageView.isShown()) { + int imageRes = getNext(); + if (imageRes == -1) { + return; + } + if (mBitmap != null) { // so Build.VERSION.SDK_INT >= 11 + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions); + } catch (Exception e) { + e.printStackTrace(); + } + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + imageView.setImageResource(imageRes); + mBitmap.recycle(); + mBitmap = null; + } + } else { + imageView.setImageResource(imageRes); + } + } + + } + }; + + mHandler.post(runnable); + + if (mOnAnimationListener != null) { + mOnAnimationListener.onAnimationStart(FramesSequenceAnimation.this); + } + } + + /** + * 暂停播放,下次start会继续播放 + */ + public synchronized void stop() { + if (!mShouldRun) { + return; + } + mShouldRun = false; + if (mOnAnimationListener != null) { + mOnAnimationListener.onAnimationStopOrCancel(FramesSequenceAnimation.this); + } + } + + /** + * 取消播放,下次start会从头播放 + */ + public synchronized void cancel() { + if (!mShouldRun) { + return; + } + mShouldRun = false; + mIndex = 0; + if (mOnAnimationListener != null) { + mOnAnimationListener.onAnimationStopOrCancel(FramesSequenceAnimation.this); + } + } + + /** + * 播放结束 + */ + private synchronized void end() { + if (!mShouldRun) { + return; + } + mShouldRun = false; + if (mOnAnimationListener != null) { + mOnAnimationListener.onAnimationEnd(FramesSequenceAnimation.this); + } + } + + /** + * 直接返回第一帧 + */ + public synchronized void goBackStart() { + if (mFrames.length == 0) { + throw new IllegalArgumentException("Animation frame length == 0"); + } + mIndex = 0; + ImageView imageView = mSoftReferenceImageView.get(); + // imageView.setImageResource(mFrames[mIndex]); + setResource(imageView); + } + + /** + * 设置停止播放监听 + * + * @param listener + */ + public FramesSequenceAnimation setOnAnimStopListener(OnAnimationListener listener) { + this.mOnAnimationListener = listener; + return this; + } + } + + + public static Bitmap readBitMap(Context context, int resId) { + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inPreferredConfig = Bitmap.Config.RGB_565; + opt.inPurgeable = true; + opt.inInputShareable = true; + InputStream is = context.getResources().openRawResource(resId); + return BitmapFactory.decodeStream(is, null, opt); + } + + + /** + * 停止播放监听 + */ + public interface OnAnimationListener { + void onAnimationStart(FramesSequenceAnimation animation); + + void onAnimationEnd(FramesSequenceAnimation animation); + + void onAnimationStopOrCancel(FramesSequenceAnimation animation); + } +} + diff --git a/app/src/main/java/com/example/baseframe/utils/AppUtils.java b/app/src/main/java/com/example/baseframe/utils/AppUtils.java new file mode 100644 index 0000000..782532e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/AppUtils.java @@ -0,0 +1,1356 @@ +package com.example.baseframe.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.AppOpsManager; +import android.app.KeyguardManager; +import android.app.NotificationManager; +import android.app.usage.UsageStatsManager; +import android.content.BroadcastReceiver; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutManager; +import android.content.pm.Signature; +import android.graphics.drawable.Drawable; +import android.hardware.SensorManager; +import android.location.LocationManager; +import android.media.AudioManager; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.PowerManager; +import android.os.Vibrator; +import android.os.storage.StorageManager; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.RequiresPermission; + +import com.blankj.ALog; +import com.example.baseframe.api.App; +import com.example.baseframe.utils.encrypt.EncryptUtils; + +import java.io.File; +import java.util.List; + +/** + * detail: APP (Android) 工具类 + * @author Ttt + *
+ *     MimeType
+ *     @see 
+ *     

+ * 所需权限 + * + *
+ */ +public final class AppUtils { + + private AppUtils() { + } + + // 日志 TAG + private static final String TAG = AppUtils.class.getSimpleName(); + + /** + * 获取 WindowManager + * @return {@link WindowManager} + */ + public static WindowManager getWindowManager() { + return getSystemService(Context.WINDOW_SERVICE); + } + + /** + * 获取 AudioManager + * @return {@link AudioManager} + */ + public static AudioManager getAudioManager() { + return getSystemService(Context.AUDIO_SERVICE); + } + + /** + * 获取 SensorManager + * @return {@link SensorManager} + */ + public static SensorManager getSensorManager() { + return getSystemService(Context.SENSOR_SERVICE); + } + + /** + * 获取 StorageManager + * @return {@link StorageManager} + */ + public static StorageManager getStorageManager() { + return getSystemService(Context.STORAGE_SERVICE); + } + + /** + * 获取 WifiManager + * @return {@link WifiManager} + */ + @SuppressLint("WifiManagerLeak") + public static WifiManager getWifiManager() { + return getSystemService(Context.WIFI_SERVICE); + } + + /** + * 获取 ConnectivityManager + * @return {@link ConnectivityManager} + */ + public static ConnectivityManager getConnectivityManager() { + return getSystemService(Context.CONNECTIVITY_SERVICE); + } + + /** + * 获取 TelephonyManager + * @return {@link TelephonyManager} + */ + public static TelephonyManager getTelephonyManager() { + return getSystemService(Context.TELEPHONY_SERVICE); + } + + /** + * 获取 AppOpsManager + * @return {@link AppOpsManager} + */ + public static AppOpsManager getAppOpsManager() { + return getSystemService(Context.APP_OPS_SERVICE); + } + + /** + * 获取 NotificationManager + * @return {@link NotificationManager} + */ + public static NotificationManager getNotificationManager() { + return getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * 获取 ShortcutManager + * @return {@link ShortcutManager} + */ + public static ShortcutManager getShortcutManager() { + return getSystemService(Context.SHORTCUT_SERVICE); + } + + /** + * 获取 ActivityManager + * @return {@link ActivityManager} + */ + public static ActivityManager getActivityManager() { + return getSystemService(Context.ACTIVITY_SERVICE); + } + + /** + * 获取 PowerManager + * @return {@link PowerManager} + */ + public static PowerManager getPowerManager() { + return getSystemService(Context.POWER_SERVICE); + } + + /** + * 获取 KeyguardManager + * @return {@link KeyguardManager} + */ + public static KeyguardManager getKeyguardManager() { + return getSystemService(Context.KEYGUARD_SERVICE); + } + + /** + * 获取 InputMethodManager + * @return {@link InputMethodManager} + */ + public static InputMethodManager getInputMethodManager() { + return getSystemService(Context.INPUT_METHOD_SERVICE); + } + + /** + * 获取 ClipboardManager + * @return {@link ClipboardManager} + */ + public static ClipboardManager getClipboardManager() { + return getSystemService(Context.CLIPBOARD_SERVICE); + } + + /** + * 获取 UsageStatsManager + * @return {@link UsageStatsManager} + */ + public static UsageStatsManager getUsageStatsManager() { + return getSystemService(Context.USAGE_STATS_SERVICE); + } + + /** + * 获取 AlarmManager + * @return {@link AlarmManager} + */ + public static AlarmManager getAlarmManager() { + return getSystemService(Context.ALARM_SERVICE); + } + + /** + * 获取 LocationManager + * @return {@link LocationManager} + */ + public static LocationManager getLocationManager() { + return getSystemService(Context.LOCATION_SERVICE); + } + + /** + * 获取 Vibrator + * @return {@link Vibrator} + */ + public static Vibrator getVibrator() { + return getSystemService(Context.VIBRATOR_SERVICE); + } + + /** + * 获取 SystemService + * @param name 服务名 + * @param 泛型 + * @return SystemService Object + */ + public static T getSystemService(final String name) { + if (CommUtils.isEmpty(name)) return null; + try { + return (T) App.getInstance().getSystemService(name); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSystemService"); + } + return null; + } + + /** + * 获取 PackageManager + * @return {@link PackageManager} + */ + public static PackageManager getPackageManager() { + try { + return App.getInstance().getPackageManager(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getPackageManager"); + } + return null; + } + + /** + * 获取 ApplicationInfo + * @return {@link ApplicationInfo} + */ + public static ApplicationInfo getApplicationInfo() { + try { + return App.getInstance().getApplicationInfo(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getApplicationInfo"); + } + return null; + } + + /** + * 获取 ApplicationInfo + * @param packageName 应用包名 + * @param flags application flags + * @return {@link ApplicationInfo} + */ + public static ApplicationInfo getApplicationInfo(final String packageName, final int flags) { + try { + return App.getInstance().getPackageManager().getApplicationInfo(packageName, flags); + } catch (Exception e) { + ALog.eTag(TAG, e, "getApplicationInfo - " + packageName); + } + return null; + } + + /** + * 获取 PackageInfo + * @param flags package flags + * @return {@link ApplicationInfo} + */ + public static PackageInfo getPackageInfo(final int flags) { + return getPackageInfo(getPackageName(), flags); + } + + /** + * 获取 PackageInfo + * @param packageName 应用包名 + * @param flags package flags + * @return {@link ApplicationInfo} + */ + public static PackageInfo getPackageInfo(final String packageName, final int flags) { + try { + return App.getInstance().getPackageManager().getPackageInfo(packageName, flags); + } catch (Exception e) { + ALog.eTag(TAG, e, "getPackageInfo - " + packageName); + } + return null; + } + + /** + * 获取 SharedPreferences + * @param fileName 文件名 + * @return {@link SharedPreferences} + */ + public static SharedPreferences getSharedPreferences(final String fileName) { + return getSharedPreferences(fileName, Context.MODE_PRIVATE); + } + + /** + * 获取 SharedPreferences + * @param fileName 文件名 + * @param mode SharedPreferences 操作模式 + * @return {@link SharedPreferences} + */ + public static SharedPreferences getSharedPreferences(final String fileName, final int mode) { + try { + return App.getInstance().getSharedPreferences(fileName, mode); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSharedPreferences - " + fileName); + } + return null; + } + + // ============ + // = APP 相关 = + // ============ + + /** + * 根据名称清除数据库 + * @param dbName 数据库名 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteDatabase(final String dbName) { + try { + return App.getInstance().deleteDatabase(dbName); + } catch (Exception e) { + ALog.eTag(TAG, e, "deleteDatabase"); + } + return false; + } + + /** + * 获取 APP 包名 + * @return APP 包名 + */ + public static String getPackageName() { + try { + return App.getInstance().getPackageName(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getPackageName"); + } + return null; + } + + /** + * 获取 APP 图标 + * @return {@link Drawable} + */ + public static Drawable getAppIcon() { + return getAppIcon(getPackageName()); + } + + /** + * 获取 APP 图标 + * @param packageName 应用包名 + * @return {@link Drawable} + */ + public static Drawable getAppIcon(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + return packageInfo == null ? null : packageInfo.applicationInfo.loadIcon(packageManager); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppIcon"); + return null; + } + } + + /** + * 获取 APP 应用名 + * @return APP 应用名 + */ + public static String getAppName() { + return getAppName(getPackageName()); + } + + /** + * 获取 APP 应用名 + * @param packageName 应用包名 + * @return APP 应用名 + */ + public static String getAppName(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + return packageInfo == null ? null : packageInfo.applicationInfo.loadLabel(packageManager).toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppName"); + return null; + } + } + + /** + * 获取 APP versionName + * @return APP versionName + */ + public static String getAppVersionName() { + return getAppVersionName(getPackageName()); + } + + /** + * 获取 APP versionName + * @param packageName 应用包名 + * @return APP versionName + */ + public static String getAppVersionName(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + PackageInfo packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + return packageInfo == null ? null : packageInfo.versionName; + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppVersionName"); + return null; + } + } + + /** + * 获取 APP versionCode + * @return APP versionCode + */ + public static long getAppVersionCode() { + return getAppVersionCode(getPackageName()); + } + + /** + * 获取 APP versionCode + * @param packageName 应用包名 + * @return APP versionCode + */ + public static long getAppVersionCode(final String packageName) { + if (CommUtils.isEmpty(packageName)) return -1; + try { + PackageInfo packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return packageInfo.getLongVersionCode(); + } else { + return packageInfo.versionCode; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppVersionCode"); + return -1; + } + } + + /** + * 获取 APP 安装包路径 /data/data/packageName/.apk + * @return APP 安装包路径 + */ + public static String getAppPath() { + return getAppPath(getPackageName()); + } + + /** + * 获取 APP 安装包路径 /data/data/packageName/.apk + * @param packageName 应用包名 + * @return APP 安装包路径 + */ + public static String getAppPath(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + PackageInfo packageInfo = getPackageInfo(packageName, 0); + return packageInfo == null ? null : packageInfo.applicationInfo.sourceDir; + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppPath"); + return null; + } + } + + // = + + /** + * 获取 APP Signature + * @return {@link Signature} 数组 + */ + public static Signature[] getAppSignature() { + return getAppSignature(getPackageName()); + } + + /** + * 获取 APP Signature + * @param packageName 应用包名 + * @return {@link Signature} 数组 + */ + public static Signature[] getAppSignature(final String packageName) { + if (CommUtils.isEmpty(packageName)) return null; + try { + PackageInfo packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + return packageInfo == null ? null : packageInfo.signatures; + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppSignature"); + return null; + } + } + + // = + + /** + * 获取 APP 签名 MD5 值 + * @return APP 签名 MD5 值 + */ + public static String getAppSignatureMD5() { + return getAppSignatureMD5(getPackageName()); + } + + /** + * 获取 APP 签名 MD5 值 + * @param packageName 应用包名 + * @return APP 签名 MD5 值 + */ + public static String getAppSignatureMD5(final String packageName) { + return getAppSignatureHash(packageName, "MD5"); + } + + /** + * 获取 APP 签名 SHA1 值 + * @return APP 签名 SHA1 值 + */ + public static String getAppSignatureSHA1() { + return getAppSignatureSHA1(getPackageName()); + } + + /** + * 获取 APP 签名 SHA1 值 + * @param packageName 应用包名 + * @return APP 签名 SHA1 值 + */ + public static String getAppSignatureSHA1(final String packageName) { + return getAppSignatureHash(packageName, "SHA1"); + } + + /** + * 获取 APP 签名 SHA256 值 + * @return APP 签名 SHA256 值 + */ + public static String getAppSignatureSHA256() { + return getAppSignatureSHA256(getPackageName()); + } + + /** + * 获取 APP 签名 SHA256 值 + * @param packageName 应用包名 + * @return APP 签名 SHA256 值 + */ + public static String getAppSignatureSHA256(final String packageName) { + return getAppSignatureHash(packageName, "SHA256"); + } + + /** + * 获取应用签名 Hash 值 + * @param packageName 应用包名 + * @param algorithm 算法 + * @return 对应算法处理后的签名信息 + */ + public static String getAppSignatureHash(final String packageName, final String algorithm) { + if (CommUtils.isEmpty(packageName)) return null; + try { + Signature[] signature = getAppSignature(packageName); + if (signature == null || signature.length == 0) return null; + return StringUtil.colonSplit(ConvertUtils.toHexString(EncryptUtils.hashTemplate(signature[0].toByteArray(), algorithm))); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAppSignatureHash - packageName: " + packageName + ", algorithm: " + algorithm); + return null; + } + } + + // = + + /** + * 判断 APP 是否 debug 模式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppDebug() { + return isAppDebug(getPackageName()); + } + + /** + * 判断 APP 是否 debug 模式 + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppDebug(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + ApplicationInfo appInfo = getApplicationInfo(packageName, 0); + return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } catch (Exception e) { + ALog.eTag(TAG, e, "isAppDebug"); + return false; + } + } + + /** + * 判断 APP 是否 release 模式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppRelease() { + return isAppRelease(getPackageName()); + } + + /** + * 判断 APP 是否 release 模式 + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppRelease(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + ApplicationInfo appInfo = getApplicationInfo(packageName, 0); + return !(appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0); + } catch (Exception e) { + ALog.eTag(TAG, e, "isAppRelease"); + return false; + } + } + + // = + + /** + * 判断 APP 是否系统 app + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppSystem() { + return isAppSystem(getPackageName()); + } + + /** + * 判断 APP 是否系统 app + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAppSystem(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + ApplicationInfo appInfo = getApplicationInfo(packageName, 0); + return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } catch (Exception e) { + ALog.eTag(TAG, e, "isAppSystem"); + return false; + } + } + + /** + * 判断 APP 是否在前台 + * @return {@code true} yes, {@code false} no + */ + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + public static boolean isAppForeground() { + return isAppForeground(getPackageName()); + } + + /** + * 判断 APP 是否在前台 + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + public static boolean isAppForeground(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + List lists = getActivityManager().getRunningAppProcesses(); + if (lists != null && lists.size() > 0) { + for (ActivityManager.RunningAppProcessInfo appProcess : lists) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return appProcess.processName.equals(packageName); + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "isAppForeground"); + } + return false; + } + + // = + + /** + * 判断是否安装了 APP + * @param action Action + * @param category Category + * @return {@code true} yes, {@code false} no + */ + public static boolean isInstalledApp(final String action, final String category) { + try { + Intent intent = new Intent(action); + intent.addCategory(category); + ResolveInfo resolveinfo = getPackageManager().resolveActivity(intent, 0); + return resolveinfo != null; + } catch (Exception e) { + ALog.eTag(TAG, e, "isInstalledApp"); + return false; + } + } + + /** + * 判断是否安装了 APP + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + @SuppressWarnings("unused") + public static boolean isInstalledApp(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + ApplicationInfo appInfo = getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + return appInfo != null; + } catch (Exception e) { // 未安装, 则会抛出异常 + ALog.eTag(TAG, e, "isInstalledApp"); + return false; + } + } + + /** + * 判断是否安装了 APP + * @param packageName 应用包名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isInstalledApp2(final String packageName) { + return !CommUtils.isEmpty(packageName) && IntentUtils.getLaunchAppIntent(packageName) != null; + } + + // ================= + // = Activity 跳转 = + // ================= + + /** + * Activity 跳转 + * @param intent {@link Intent} + * @return {@code true} success, {@code false} fail + */ + public static boolean startActivity(final Intent intent) { + if (intent == null) return false; + try { + App.getInstance().startActivity(IntentUtils.getIntent(intent, true)); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "startActivity"); + } + return false; + } + + /** + * Activity 跳转回传 + * @param activity {@link Activity} + * @param intent {@link Intent} + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean startActivityForResult(final Activity activity, final Intent intent, final int requestCode) { + if (activity == null || intent == null) return false; + try { + activity.startActivityForResult(intent, requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "startActivityForResult"); + } + return false; + } + + + + // ======== + // = 广播 = + // ======== + + /** + * 注册广播监听 + * @param receiver {@linkBroadcastReceiver} + * @param filter {@link IntentFilter} + * @return {@code true} success, {@code false} fail + */ + public static boolean registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) { + if (receiver == null || filter == null) return false; + try { + App.getInstance().registerReceiver(receiver, filter); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "registerReceiver"); + } + return false; + } + + /** + * 注销广播监听 + * @param receiver {@linkBroadcastReceiver} + * @return {@code true} success, {@code false} fail + */ + public static boolean unregisterReceiver(final BroadcastReceiver receiver) { + if (receiver == null) return false; + try { + App.getInstance().unregisterReceiver(receiver); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "unregisterReceiver"); + } + return false; + } + + // ============ + // = 发送广播 = + // ============ + + /** + * 发送广播 + * @param intent {@link Intent} + * @return {@code true} success, {@code false} fail + */ + public static boolean sendBroadcast(final Intent intent) { + if (intent == null) return false; + try { + App.getInstance().sendBroadcast(intent); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "sendBroadcast"); + } + return false; + } + + /** + * 发送广播 + * @param intent {@link Intent} + * @param receiverPermission 广播权限 + * @return {@code true} success, {@code false} fail + */ + public static boolean sendBroadcast(final Intent intent, final String receiverPermission) { + if (intent == null || receiverPermission == null) return false; + try { + App.getInstance().sendBroadcast(intent, receiverPermission); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "sendBroadcast"); + } + return false; + } + + // ======== + // = 服务 = + // ======== + + /** + * 启动服务 + * @param intent {@link Intent} + * @return {@code true} success, {@code false} fail + */ + public static boolean startService(final Intent intent) { + if (intent == null) return false; + try { + App.getInstance().startService(intent); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "startService"); + } + return false; + } + + /** + * 停止服务 + * @param intent {@link Intent} + * @return {@code true} success, {@code false} fail + */ + public static boolean stopService(final Intent intent) { + if (intent == null) return false; + try { + App.getInstance().stopService(intent); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "stopService"); + } + return false; + } + + // ============== + // = 安装、卸载 = + // ============== + + /** + * 安装 APP( 支持 8.0) 的意图 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final String filePath) { + return installApp(FileUtils.getFileByPath(filePath)); + } + + /** + * 安装 APP( 支持 8.0) 的意图 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final File file) { + if (!FileUtils.isFileExists(file)) return false; + try { + return startActivity(IntentUtils.getInstallAppIntent(file, true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "installApp"); + return false; + } + } + + /** + * 安装 APP( 支持 8.0) 的意图 + * @param activity {@link Activity} + * @param filePath 文件路径 + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final Activity activity, final String filePath, final int requestCode) { + return installApp(activity, FileUtils.getFileByPath(filePath), requestCode); + } + + /** + * 安装 APP( 支持 8.0) 的意图 + * @param activity {@link Activity} + * @param file 文件 + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean installApp(final Activity activity, final File file, final int requestCode) { + if (!FileUtils.isFileExists(file)) return false; + try { + activity.startActivityForResult(IntentUtils.getInstallAppIntent(file), requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "installApp"); + return false; + } + } + + // = + + /** + * 静默安装应用 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final String filePath) { + return installAppSilent(filePath, null); + } + + /** + * 静默安装应用 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file) { + return installAppSilent(file, null); + } + + /** + * 静默安装应用 + * @param filePath 文件路径 + * @param params 安装参数 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final String filePath, final String params) { + return installAppSilent(FileUtils.getFileByPath(filePath), params, ADBUtils.isDeviceRooted()); + } + + /** + * 静默安装应用 + * @param file 文件 + * @param params 安装参数 + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file, final String params) { + return installAppSilent(file, params, ADBUtils.isDeviceRooted()); + } + + /** + * 静默安装应用 + * @param file 文件 + * @param params 安装参数 + * @param isRooted 是否 root + * @return {@code true} success, {@code false} fail + */ + public static boolean installAppSilent(final File file, final String params, final boolean isRooted) { + if (!FileUtils.isFileExists(file)) return false; + String filePath = '"' + file.getAbsolutePath() + '"'; + String command = "LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install " + (params == null ? "" : params + " ") + filePath; + ShellUtils.CommandResult result = ShellUtils.execCmd(command, isRooted); + return result.isSuccess4("success"); + } + + // = + + /** + * 卸载应用 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallApp(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + return startActivity(IntentUtils.getUninstallAppIntent(packageName, true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "uninstallApp"); + return false; + } + } + + /** + * 卸载应用 + * @param activity {@link Activity} + * @param packageName 应用包名 + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallApp(final Activity activity, final String packageName, final int requestCode) { + if (CommUtils.isEmpty(packageName)) return false; + try { + activity.startActivityForResult(IntentUtils.getUninstallAppIntent(packageName), requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "uninstallApp"); + return false; + } + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName) { + return uninstallAppSilent(packageName, false, ADBUtils.isDeviceRooted()); + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @param isKeepData true 表示卸载应用但保留数据和缓存目录 + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName, final boolean isKeepData) { + return uninstallAppSilent(packageName, isKeepData, ADBUtils.isDeviceRooted()); + } + + /** + * 静默卸载应用 + * @param packageName 应用包名 + * @param isKeepData true 表示卸载应用但保留数据和缓存目录 + * @param isRooted 是否 root + * @return {@code true} success, {@code false} fail + */ + public static boolean uninstallAppSilent(final String packageName, final boolean isKeepData, final boolean isRooted) { + if (CommUtils.isEmpty(packageName)) return false; + String command = "LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm uninstall " + (isKeepData ? "-k " : "") + packageName; + ShellUtils.CommandResult result = ShellUtils.execCmd(command, isRooted); + return result.isSuccess4("success"); + } + + // ============ + // = 操作相关 = + // ============ + + /** + * 打开 APP + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean launchApp(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + return startActivity(IntentUtils.getLaunchAppIntent(packageName, true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "launchApp"); + } + return false; + } + + /** + * 打开 APP + * @param activity {@link Activity} + * @param packageName 应用包名 + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean launchApp(final Activity activity, final String packageName, final int requestCode) { + if (CommUtils.isEmpty(packageName)) return false; + try { + activity.startActivityForResult(IntentUtils.getLaunchAppIntent(packageName), requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "launchApp"); + } + return false; + } + + // = + + /** + * 跳转到 APP 设置详情页面 + * @return {@code true} success, {@code false} fail + */ + public static boolean launchAppDetailsSettings() { + return launchAppDetailsSettings(getPackageName()); + } + + /** + * 跳转到 APP 设置详情页面 + * @param packageName 应用包名 + * @return {@code true} success, {@code false} fail + */ + public static boolean launchAppDetailsSettings(final String packageName) { + if (CommUtils.isEmpty(packageName)) return false; + try { + return startActivity(IntentUtils.getLaunchAppDetailsSettingsIntent(packageName, true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "launchAppDetailsSettings"); + } + return false; + } + + /** + * 跳转到 APP 应用商城详情页面 + * @param marketPkg 应用商店包名, 如果为 "" 则由系统弹出应用商店列表供用户选择, 否则调转到目标市场的应用详情界面, 某些应用商店可能会失败 + * @return {@code true} success, {@code false} fail + */ + public static boolean launchAppDetails(final String marketPkg) { + return launchAppDetails(getPackageName(), marketPkg); + } + + /** + * 跳转到 APP 应用商城详情页面 + * @param packageName 应用包名 + * @param marketPkg 应用商店包名, 如果为 "" 则由系统弹出应用商店列表供用户选择, 否则调转到目标市场的应用详情界面, 某些应用商店可能会失败 + * @return {@code true} success, {@code false} fail + */ + public static boolean launchAppDetails(final String packageName, final String marketPkg) { + if (CommUtils.isEmpty(packageName)) return false; + try { + return startActivity(IntentUtils.getLaunchAppDetailIntent(packageName, marketPkg, true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "launchAppDetails"); + } + return false; + } + + // ============ + // = 其他功能 = + // ============ + + /** + * 打开文件 + * @param filePath 文件路径 + * @param dataType 数据类型 + * @return {@code true} success, {@code false} fail + */ + public static boolean openFile(final String filePath, final String dataType) { + return openFile(FileUtils.getFileByPath(filePath), dataType); + } + + /** + * 打开文件 + * @param file 文件 + * @param dataType 数据类型 + * @return {@code true} success, {@code false} fail + */ + public static boolean openFile(final File file, final String dataType) { + if (!FileUtils.isFileExists(file)) return false; + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 临时授权 ( 必须 ) + intent.setDataAndType(CommUtils.getUriForFile(file), dataType); + return startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "openFile"); + } + return false; + } + + // = + + /** + * 打开文件 - 指定应用 + * @param filePath 文件路径 + * @param packageName 应用包名 + * @param className Activity.class.getCanonicalName() + * @return {@code true} success, {@code false} fail + */ + public static boolean openFileByApp(final String filePath, final String packageName, final String className) { + return openFileByApp(FileUtils.getFileByPath(filePath), packageName, className); + } + + /** + * 打开文件 - 指定应用 + * @param file 文件 + * @param packageName 应用包名 + * @param className Activity.class.getCanonicalName() + * @return {@code true} success, {@code false} fail + */ + public static boolean openFileByApp(final File file, final String packageName, final String className) { + if (!FileUtils.isFileExists(file)) return false; + try { + Intent intent = new Intent(); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.fromFile(file)); + intent.setClassName(packageName, className); + return startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "openFile"); + } + return false; + } + + // = + + /** + * 打开 PDF 文件 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean openPDFFile(final String filePath) { + return openPDFFile(FileUtils.getFileByPath(filePath)); + } + + /** + * 打开 PDF 文件 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean openPDFFile(final File file) { + return openFile(file, "application/pdf"); + } + + // = + + /** + * 打开 Word 文件 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean openWordFile(final String filePath) { + return openWordFile(FileUtils.getFileByPath(filePath)); + } + + /** + * 打开 Word 文件 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean openWordFile(final File file) { + return openFile(file, "application/msword"); + } + + // = + + /** + * 调用 WPS 打开 office 文档 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean openOfficeByWPS(final String filePath) { + return openOfficeByWPS(FileUtils.getFileByPath(filePath)); + } + + /** + * 调用 WPS 打开 office 文档 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean openOfficeByWPS(final File file) { + String wpsPackage = "cn.wps.moffice_eng"; // 普通版与英文版一样 + // String wpsActivity = "cn.wps.moffice.documentmanager.PreStartActivity"; + String wpsActivity2 = "cn.wps.moffice.documentmanager.PreStartActivity2"; + // 打开文件 + return openFileByApp(file, wpsPackage, wpsActivity2); + } + + // ============ + // = 系统页面 = + // ============ + + /** + * 跳转到系统设置页面 + * @return {@code true} success, {@code false} fail + */ + public static boolean startSysSetting() { + try { + return startActivity(IntentUtils.getIntent(new Intent(Settings.ACTION_SETTINGS), true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "startSysSetting"); + } + return false; + } + + /** + * 跳转到系统设置页面 + * @param activity {@link Activity} + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean startSysSetting(final Activity activity, final int requestCode) { + try { + activity.startActivityForResult(new Intent(Settings.ACTION_SETTINGS), requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "startSysSetting"); + } + return false; + } + + /** + * 打开网络设置界面 + * @return {@code true} success, {@code false} fail + */ + public static boolean openWirelessSettings() { + try { + return startActivity(IntentUtils.getIntent(new Intent(Settings.ACTION_WIRELESS_SETTINGS), true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "openWirelessSettings"); + } + return false; + } + + /** + * 打开网络设置界面 + * @param activity {@link Activity} + * @param requestCode 请求 code + * @return {@code true} success, {@code false} fail + */ + public static boolean openWirelessSettings(final Activity activity, final int requestCode) { + try { + activity.startActivityForResult(new Intent(Settings.ACTION_WIRELESS_SETTINGS), requestCode); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "openWirelessSettings"); + } + return false; + } + + /** + * 打开 GPS 设置界面 + * @return {@code true} success, {@code false} fail + */ + public static boolean openGpsSettings() { + try { + return startActivity(IntentUtils.getIntent(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), true)); + } catch (Exception e) { + ALog.eTag(TAG, e, "openGpsSettings"); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/ArithUtils.java b/app/src/main/java/com/example/baseframe/utils/ArithUtils.java new file mode 100644 index 0000000..44d2236 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ArithUtils.java @@ -0,0 +1,218 @@ +package com.example.baseframe.utils; + +import java.math.BigDecimal; + +/** + * 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精 确的浮点数运算,包括加减乘除和四舍五入。 + */ +public class ArithUtils { + // 默认除法运算精度 + private static final int DEF_DIV_SCALE = 10; + + // 这个类不能实例化 + private ArithUtils () { + + } + + /** + * 提供精确的加法运算。 + * + * @param v1 + * 被加数 + * @param v2 + * 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + public static String addStr(String v1, String v2) { + if(!StringUtil.isNumeric(v1) || !StringUtil.isNumeric(v2)) + return ""; + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.add(b2).toString(); + } + + /** + * 提供精确的减法运算。 + * + * @param v1 + * 被减数 + * @param v2 + * 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + public static String subStr(String v1, String v2) { + if(!StringUtil.isNumeric(v1) || !StringUtil.isNumeric(v2)) + return ""; + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.subtract(b2).toString(); + } + /** + * 提供精确的乘法运算。 + * + * @param v1 + * 被乘数 + * @param v2 + * 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + public static String mulStr(String v1, String v2) { + if(!StringUtil.isNumeric(v1) || !StringUtil.isNumeric(v2)) + return ""; + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.multiply(b2).toString(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位,以后的数字四舍五入。 + * + * @param v1 + * 被除数 + * @param v2 + * 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) { + return div(v1, v2, DEF_DIV_SCALE); + } + + public static String divStr(String v1, String v2) { + return divStr(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 定精度,以后的数字四舍五入。 + * + * @param v1 + * 被除数 + * @param v2 + * 除数 + * @param scale + * 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); + } + + public static String divStr(String v1, String v2, int scale) { + if(!StringUtil.isNumeric(v1) || !StringUtil.isNumeric(v2)) + return ""; + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * + * @param v + * 需要四舍五入的数字 + * @param scale + * 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + String input = Double.toString(v); + + BigDecimal b = new BigDecimal(input); + BigDecimal one = new BigDecimal("1"); + return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + + * 需要四舍五入的数字 + * @param scale + * 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static String roundStr(String input, int scale) { + if(!StringUtil.isNumeric(input)) + return ""; + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(input); + BigDecimal one = new BigDecimal("1"); + return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).toString(); + } + + + /** + * 提供精确的小数位保留几位小数处理。 + * + * @param v + * 需要留几位小数的数字 + * 小数点后保留几位 + * @return 保留几位小数处理的结果 + */ + public static double roundSub(double v) { + String input = Double.toString(v); + if (input.indexOf(".") < input.length() - 3) { + input = input.substring(0, input.indexOf(".") + 3); + return Double.valueOf(input); + } + return v; + } + public static double roundSub(String input) { + if (input.indexOf(".") < input.length() - 3) { + input = input.substring(0, input.indexOf(".") + 3); + return Double.valueOf(input); + } + return Double.valueOf(input); + } + + public static String roundSubStr(String input) { + if (input.indexOf(".") < input.length() - 3) { + input = input.substring(0, input.indexOf(".") + 3); + } + return input; + } + + + /** + * 提供高精度double转换String出现的科学计数法问题。 + * double 到了一定位数会变成e,科学计数 + * @param v 数字 + * @return 转换的结果 + */ + public static String doubleToString(double v) { + java.text.NumberFormat nf = java.text.NumberFormat.getInstance(); + nf.setGroupingUsed(false); + return nf.format(v); + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/ArrayUtils.java b/app/src/main/java/com/example/baseframe/utils/ArrayUtils.java new file mode 100644 index 0000000..d766d2d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ArrayUtils.java @@ -0,0 +1,3665 @@ +package com.example.baseframe.utils; + +import com.blankj.ALog; + + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +/** + * detail: Array 数组工具类 + * @author Ttt + *
+ *     // 升序
+ *     Arrays.sort(arrays);
+ *     // 降序 - 只能对对象数组降序
+ *     Arrays.sort(arrays, Collections.reverseOrder());
+ * 
+ */ +public final class ArrayUtils { + + private ArrayUtils() { + } + + // 日志 TAG + private static final String TAG = ArrayUtils.class.getSimpleName(); + + // ========= + // = Array = + // ========= + + /** + * 判断数组是否为 null + * @param objects object[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final Object[] objects) { + return objects == null || objects.length == 0; + } + + /** + * 判断数组是否为 null + * @param ints int[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final int[] ints) { + return ints == null || ints.length == 0; + } + + /** + * 判断数组是否为 null + * @param bytes byte[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final byte[] bytes) { + return bytes == null || bytes.length == 0; + } + + /** + * 判断数组是否为 null + * @param chars char[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final char[] chars) { + return chars == null || chars.length == 0; + } + + /** + * 判断数组是否为 null + * @param shorts short[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final short[] shorts) { + return shorts == null || shorts.length == 0; + } + + /** + * 判断数组是否为 null + * @param longs long[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final long[] longs) { + return longs == null || longs.length == 0; + } + + /** + * 判断数组是否为 null + * @param floats float[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final float[] floats) { + return floats == null || floats.length == 0; + } + + /** + * 判断数组是否为 null + * @param doubles double[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final double[] doubles) { + return doubles == null || doubles.length == 0; + } + + /** + * 判断数组是否为 null + * @param booleans boolean[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final boolean[] booleans) { + return booleans == null || booleans.length == 0; + } + + /** + * 判断数组是否为 null + * @param object Array[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isEmpty(final Object object) { + return object == null || length(object) == 0; + } + + // = + + /** + * 判断数组是否不为 null + * @param objects object[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final Object[] objects) { + return objects != null && objects.length != 0; + } + + /** + * 判断数组是否不为 null + * @param ints int[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final int[] ints) { + return ints != null && ints.length != 0; + } + + /** + * 判断数组是否不为 null + * @param bytes byte[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final byte[] bytes) { + return bytes != null && bytes.length != 0; + } + + /** + * 判断数组是否不为 null + * @param chars char[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final char[] chars) { + return chars != null && chars.length != 0; + } + + /** + * 判断数组是否不为 null + * @param shorts short[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final short[] shorts) { + return shorts != null && shorts.length != 0; + } + + /** + * 判断数组是否不为 null + * @param longs long[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final long[] longs) { + return longs != null && longs.length != 0; + } + + /** + * 判断数组是否不为 null + * @param floats float[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final float[] floats) { + return floats != null && floats.length != 0; + } + + /** + * 判断数组是否不为 null + * @param doubles double[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final double[] doubles) { + return doubles != null && doubles.length != 0; + } + + /** + * 判断数组是否不为 null + * @param booleans boolean[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final boolean[] booleans) { + return booleans != null && booleans.length != 0; + } + + /** + * 判断数组是否不为 null + * @param object Array[] + * @return {@code true} yes, {@code false} no + */ + public static boolean isNotEmpty(final Object object) { + return object != null && length(object) != 0; + } + + // ============ + // = 判断长度 = + // ============ + + /** + * 获取数组长度 + * @param objects object[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final Object[] objects) { + return length(objects, 0); + } + + /** + * 获取数组长度 + * @param ints int[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final int[] ints) { + return length(ints, 0); + } + + /** + * 获取数组长度 + * @param bytes byte[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final byte[] bytes) { + return length(bytes, 0); + } + + /** + * 获取数组长度 + * @param chars char[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final char[] chars) { + return length(chars, 0); + } + + /** + * 获取数组长度 + * @param shorts short[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final short[] shorts) { + return length(shorts, 0); + } + + /** + * 获取数组长度 + * @param longs long[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final long[] longs) { + return length(longs, 0); + } + + /** + * 获取数组长度 + * @param floats float[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final float[] floats) { + return length(floats, 0); + } + + /** + * 获取数组长度 + * @param doubles double[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final double[] doubles) { + return length(doubles, 0); + } + + /** + * 获取数组长度 + * @param booleans boolean[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final boolean[] booleans) { + return length(booleans, 0); + } + + /** + * 获取数组长度 + * @param object Array[] + * @return 如果数据为 null, 则返回默认长度, 如果不为 null, 则返回 array[].length + */ + public static int length(final Object object) { + return length(object, 0); + } + + // = + + /** + * 获取数组长度 + * @param objects object[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final Object[] objects, final int defaultLength) { + return objects != null ? objects.length : defaultLength; + } + + /** + * 获取数组长度 + * @param ints int[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final int[] ints, final int defaultLength) { + return ints != null ? ints.length : defaultLength; + } + + /** + * 获取数组长度 + * @param bytes byte[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final byte[] bytes, final int defaultLength) { + return bytes != null ? bytes.length : defaultLength; + } + + /** + * 获取数组长度 + * @param chars char[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final char[] chars, final int defaultLength) { + return chars != null ? chars.length : defaultLength; + } + + /** + * 获取数组长度 + * @param shorts short[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final short[] shorts, final int defaultLength) { + return shorts != null ? shorts.length : defaultLength; + } + + /** + * 获取数组长度 + * @param longs long[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final long[] longs, final int defaultLength) { + return longs != null ? longs.length : defaultLength; + } + + /** + * 获取数组长度 + * @param floats float[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final float[] floats, final int defaultLength) { + return floats != null ? floats.length : defaultLength; + } + + /** + * 获取数组长度 + * @param doubles double[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final double[] doubles, final int defaultLength) { + return doubles != null ? doubles.length : defaultLength; + } + + /** + * 获取数组长度 + * @param booleans boolean[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final boolean[] booleans, final int defaultLength) { + return booleans != null ? booleans.length : defaultLength; + } + + /** + * 获取数组长度 + * @param object Array[] + * @param defaultLength 数组为 null 时, 返回的长度 + * @return 如果数据为 null, 则返回 defaultLength, 如果不为 null, 则返回 array[].length + */ + public static int length(final Object object, final int defaultLength) { + if (object != null) { + Class clazz = object.getClass(); + // 判断是否数组类型 + if (clazz.isArray()) { + try { + // = 基本数据类型 = + if (clazz.isAssignableFrom(int[].class)) { + return ((int[]) object).length; + } else if (clazz.isAssignableFrom(boolean[].class)) { + return ((boolean[]) object).length; + } else if (clazz.isAssignableFrom(long[].class)) { + return ((long[]) object).length; + } else if (clazz.isAssignableFrom(double[].class)) { + return ((double[]) object).length; + } else if (clazz.isAssignableFrom(float[].class)) { + return ((float[]) object).length; + } else if (clazz.isAssignableFrom(byte[].class)) { + return ((byte[]) object).length; + } else if (clazz.isAssignableFrom(char[].class)) { + return ((char[]) object).length; + } else if (clazz.isAssignableFrom(short[].class)) { + return ((short[]) object).length; + } + return ((Object[]) object).length; + } catch (Exception e) { + ALog.eTag(TAG, e, "length"); + } + } + } + return defaultLength; + } + + // = + + /** + * 判断数组长度是否等于期望长度 + * @param objects object[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final Object[] objects, final int length) { + return objects != null && objects.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param ints int[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final int[] ints, final int length) { + return ints != null && ints.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param bytes byte[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final byte[] bytes, final int length) { + return bytes != null && bytes.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param chars char[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final char[] chars, final int length) { + return chars != null && chars.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param shorts short[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final short[] shorts, final int length) { + return shorts != null && shorts.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param longs long[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final long[] longs, final int length) { + return longs != null && longs.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param floats float[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final float[] floats, final int length) { + return floats != null && floats.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param doubles double[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final double[] doubles, final int length) { + return doubles != null && doubles.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param booleans boolean[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final boolean[] booleans, final int length) { + return booleans != null && booleans.length == length; + } + + /** + * 判断数组长度是否等于期望长度 + * @param object Array[] + * @param length 期望长度 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLength(final Object object, final int length) { + return object != null && length(object) == length; + } + + // ================ + // = 获取长度总和 = + // ================ + + /** + * 获取数组长度总和 + * @param objects Array[] + * @return 数组长度总和 + */ + public static int getCount(final Object... objects) { + if (objects == null) return 0; + int count = 0; + for (Object object : objects) { + count += length(object); + } + return count; + } + + // ============ + // = 数据获取 = + // ============ + + /** + * 获取数组对应索引数据 + * @param array 数组 + * @param pos 索引 + * @param 泛型 + * @return 数组指定索引的值 + */ + public static T get(final Object array, final int pos) { + if (array == null || pos < 0) return null; + try { + return (T) Array.get(array, pos); + } catch (Exception e) { + ALog.eTag(TAG, e, "getByArray"); + } + return null; + } + + /** + * 获取数组对应索引数据 + * @param array 数组 + * @param pos 索引 + * @param 泛型 + * @return 数组指定索引的值 + */ + public static T get(final T[] array, final int pos) { + return get(array, pos, null); + } + + /** + * 获取数组对应索引数据 + * @param ints int[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static int get(final int[] ints, final int pos) { + return get(ints, pos, -1); + } + + /** + * 获取数组对应索引数据 + * @param bytes byte[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static byte get(final byte[] bytes, final int pos) { + return get(bytes, pos, (byte) -1); + } + + /** + * 获取数组对应索引数据 + * @param chars char[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static char get(final char[] chars, final int pos) { + return get(chars, pos, (char) -1); + } + + /** + * 获取数组对应索引数据 + * @param shorts short[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static short get(final short[] shorts, final int pos) { + return get(shorts, pos, (short) -1); + } + + /** + * 获取数组对应索引数据 + * @param longs long[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static long get(final long[] longs, final int pos) { + return get(longs, pos, -1L); + } + + /** + * 获取数组对应索引数据 + * @param floats float[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static float get(final float[] floats, final int pos) { + return get(floats, pos, -1f); + } + + /** + * 获取数组对应索引数据 + * @param doubles double[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static double get(final double[] doubles, final int pos) { + return get(doubles, pos, -1d); + } + + /** + * 获取数组对应索引数据 + * @param booleans boolean[] + * @param pos 索引 + * @return 数组指定索引的值 + */ + public static boolean get(final boolean[] booleans, final int pos) { + return get(booleans, pos, false); + } + + // = + + /** + * 获取数组对应索引数据 + * @param array 数组 + * @param pos 索引 + * @param defaultValue 默认值 + * @param 泛型 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static T get(final T[] array, final int pos, final T defaultValue) { + if (array != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return array[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param ints int[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static int get(final int[] ints, final int pos, final int defaultValue) { + if (ints != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return ints[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param bytes byte[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static byte get(final byte[] bytes, final int pos, final byte defaultValue) { + if (bytes != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return bytes[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param chars char[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static char get(final char[] chars, final int pos, final char defaultValue) { + if (chars != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return chars[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param shorts short[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static short get(final short[] shorts, final int pos, final short defaultValue) { + if (shorts != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return shorts[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param longs long[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static long get(final long[] longs, final int pos, final long defaultValue) { + if (longs != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return longs[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param floats float[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static float get(final float[] floats, final int pos, final float defaultValue) { + if (floats != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return floats[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param doubles double[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static double get(final double[] doubles, final int pos, final double defaultValue) { + if (doubles != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return doubles[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + /** + * 获取数组对应索引数据 + * @param booleans boolean[] + * @param pos 索引 + * @param defaultValue 默认值 + * @return 数组指定索引的值, 如果获取失败, 则返回 defaultValue + */ + public static boolean get(final boolean[] booleans, final int pos, final boolean defaultValue) { + if (booleans != null) { + // 防止索引为负数 + if (pos < 0) return defaultValue; + try { + return booleans[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "get - " + pos); + } + } + return defaultValue; + } + + // = + + /** + * 获取数组第一条数据 + * @param array 数组 + * @param 泛型 + * @return 数组索引为 0 的值 + */ + public static T getFirst(final T[] array) { + return get(array, 0); + } + + /** + * 获取数组第一条数据 + * @param ints int[] + * @return 数组索引为 0 的值 + */ + public static int getFirst(final int[] ints) { + return get(ints, 0); + } + + /** + * 获取数组第一条数据 + * @param bytes byte[] + * @return 数组索引为 0 的值 + */ + public static byte getFirst(final byte[] bytes) { + return get(bytes, 0); + } + + /** + * 获取数组第一条数据 + * @param chars char[] + * @return 数组索引为 0 的值 + */ + public static char getFirst(final char[] chars) { + return get(chars, 0); + } + + /** + * 获取数组第一条数据 + * @param shorts short[] + * @return 数组索引为 0 的值 + */ + public static short getFirst(final short[] shorts) { + return get(shorts, 0); + } + + /** + * 获取数组第一条数据 + * @param longs long[] + * @return 数组索引为 0 的值 + */ + public static long getFirst(final long[] longs) { + return get(longs, 0); + } + + /** + * 获取数组第一条数据 + * @param floats float[] + * @return 数组索引为 0 的值 + */ + public static float getFirst(final float[] floats) { + return get(floats, 0); + } + + /** + * 获取数组第一条数据 + * @param doubles double[] + * @return 数组索引为 0 的值 + */ + public static double getFirst(final double[] doubles) { + return get(doubles, 0); + } + + /** + * 获取数组第一条数据 + * @param booleans boolean[] + * @return 数组索引为 0 的值 + */ + public static boolean getFirst(final boolean[] booleans) { + return get(booleans, 0); + } + + // = + + /** + * 获取数组最后一条数据 + * @param array 数组 + * @param 泛型 + * @return 数组索引 length - 1 的值 + */ + public static T getLast(final T[] array) { + return get(array, length(array) - 1); + } + + /** + * 获取数组最后一条数据 + * @param ints int[] + * @return 数组索引 length - 1 的值 + */ + public static int getLast(final int[] ints) { + return get(ints, length(ints) - 1); + } + + /** + * 获取数组最后一条数据 + * @param bytes byte[] + * @return 数组索引 length - 1 的值 + */ + public static byte getLast(final byte[] bytes) { + return get(bytes, length(bytes) - 1); + } + + /** + * 获取数组最后一条数据 + * @param chars char[] + * @return 数组索引 length - 1 的值 + */ + public static char getLast(final char[] chars) { + return get(chars, length(chars) - 1); + } + + /** + * 获取数组最后一条数据 + * @param shorts short[] + * @return 数组索引 length - 1 的值 + */ + public static short getLast(final short[] shorts) { + return get(shorts, length(shorts) - 1); + } + + /** + * 获取数组最后一条数据 + * @param longs long[] + * @return 数组索引 length - 1 的值 + */ + public static long getLast(final long[] longs) { + return get(longs, length(longs) - 1); + } + + /** + * 获取数组最后一条数据 + * @param floats float[] + * @return 数组索引 length - 1 的值 + */ + public static float getLast(final float[] floats) { + return get(floats, length(floats) - 1); + } + + /** + * 获取数组最后一条数据 + * @param doubles double[] + * @return 数组索引 length - 1 的值 + */ + public static double getLast(final double[] doubles) { + return get(doubles, length(doubles) - 1); + } + + /** + * 获取数组最后一条数据 + * @param booleans boolean[] + * @return 数组索引 length - 1 的值 + */ + public static boolean getLast(final boolean[] booleans) { + return get(booleans, length(booleans) - 1); + } + + // =================== + // = 数据获取 - 特殊 = + // =================== + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param notNull 是否不允许值为 null + * @param offset 偏移量 + * @param 泛型 + * @return 对应索引的值 + */ + public static T get(final T[] array, final T value, final int number, final boolean notNull, final int offset) { + if (array != null) { + if (notNull && value == null) { + return null; + } + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + T t = array[i]; + // 判断是否一样 + if (equals(t, value)) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return null; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param notNull 是否不允许值为 null + * @param offset 偏移量 + * @param 泛型 + * @return 对应的索引 + */ + public static int getPosition(final T[] array, final T value, final int number, final boolean notNull, final int offset) { + if (array != null) { + if (notNull && value == null) { + return -1; + } + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + T t = array[i]; + // 判断是否一样 + if (equals(t, value)) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param 泛型 + * @return 对应索引的值 + */ + public static T get(final T[] array, final T value) { + return get(array, value, 0, false, 0); + } + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param 泛型 + * @return 对应索引的值 + */ + public static T get(final T[] array, final T value, final int number) { + return get(array, value, number, false, 0); + } + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param notNull 是否不允许值为 null + * @param 泛型 + * @return 对应索引的值 + */ + public static T get(final T[] array, final T value, final boolean notNull) { + return get(array, value, 0, notNull, 0); + } + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param notNull 是否不允许值为 null + * @param 泛型 + * @return 对应索引的值 + */ + public static T get(final T[] array, final T value, final int number, final boolean notNull) { + return get(array, value, number, notNull, 0); + } + + // = + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值, 不允许值为 null + * @param array 数组 + * @param value 值 + * @param 泛型 + * @return 对应索引的值 + */ + public static T getNotNull(final T[] array, final T value) { + return get(array, value, 0, true, 0); + } + + /** + * 根据指定 value 获取 value 所在位置 + 偏移量的值, 不允许值为 null + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param 泛型 + * @return 对应索引的值 + */ + public static T getNotNull(final T[] array, final T value, final int number) { + return get(array, value, number, true, 0); + } + + // = + + /** + * 根据指定 value 获取索引 + * @param array 数组 + * @param value 值 + * @param 泛型 + * @return 对应的索引 + */ + public static int getPosition(final T[] array, final T value) { + return getPosition(array, value, 0, false, 0); + } + + /** + * 根据指定 value 获取索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param 泛型 + * @return 对应的索引 + */ + public static int getPosition(final T[] array, final T value, final int number) { + return getPosition(array, value, number, false, 0); + } + + /** + * 根据指定 value 获取索引 + * @param array 数组 + * @param value 值 + * @param notNull 是否不允许值为 null + * @param 泛型 + * @return 对应的索引 + */ + public static int getPosition(final T[] array, final T value, final boolean notNull) { + return getPosition(array, value, 0, notNull, 0); + } + + /** + * 根据指定 value 获取索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param notNull 是否不允许值为 null + * @param 泛型 + * @return 对应的索引 + */ + public static int getPosition(final T[] array, final T value, final int number, final boolean notNull) { + return getPosition(array, value, number, notNull, 0); + } + + // = + + /** + * 根据指定 value 获取索引, 不允许值为 null + * @param array 数组 + * @param value 值 + * @param 泛型 + * @return 对应的索引 + */ + public static int getPositionNotNull(final T[] array, final T value) { + return getPosition(array, value, 0, true, 0); + } + + /** + * 根据指定 value 获取索引, 不允许值为 null + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param 泛型 + * @return 对应的索引 + */ + public static int getPositionNotNull(final T[] array, final T value, final int number) { + return getPosition(array, value, number, true, 0); + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static int get(final int[] array, final int value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + int valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final int[] array, final int value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + int valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static byte get(final byte[] array, final byte value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + byte valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (byte) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final byte[] array, final byte value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + byte valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static char get(final char[] array, final char value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + char valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (char) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final char[] array, final char value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + char valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static short get(final short[] array, final short value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + short valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (short) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final short[] array, final short value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + short valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static long get(final long[] array, final long value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + long valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (long) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final long[] array, final long value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + long valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static float get(final float[] array, final float value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + float valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (float) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final float[] array, final float value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + float valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static double get(final double[] array, final double value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + double valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return (double) -1; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final double[] array, final double value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + double valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // = + + /** + * 根据指定值获取 value 所在位置 + 偏移量的值 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应索引的值 + */ + public static boolean get(final boolean[] array, final boolean value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + boolean valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return array[i + offset]; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "get"); + } + } + return false; + } + + /** + * 根据指定值获取 value 所在位置 + 偏移量的索引 + * @param array 数组 + * @param value 值 + * @param number 符合条件次数 ( 从 0 开始, 0 = 1) + * @param offset 偏移量 + * @return 对应的索引 + */ + public static int getPosition(final boolean[] array, final boolean value, final int number, final int offset) { + if (array != null) { + try { + // 保存当前临时次数 + int temp = number; + // 进行循环判断 + for (int i = 0, len = array.length; i < len; i++) { + boolean valueI = array[i]; + // 判断是否一样 + if (valueI == value) { + if (temp - 1 < 0) { + return i + offset; + } + temp--; + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getPosition"); + } + } + return -1; + } + + // ============ + // = 转换处理 = + // ============ + + /** + * int[] 转换 Integer[] + * @param ints int[] + * @return {@link Integer[]} + */ + public static Integer[] intsToIntegers(final int[] ints) { + if (ints != null) { + int len = ints.length; + // 创建数组 + Integer[] array = new Integer[len]; + for (int i = 0; i < len; i++) { + array[i] = ints[i]; + } + return array; + } + return null; + } + + /** + * byte[] 转换 Byte[] + * @param bytes byte[] + * @return {@link Byte[]} + */ + public static Byte[] bytesToBytes(final byte[] bytes) { + if (bytes != null) { + int len = bytes.length; + // 创建数组 + Byte[] array = new Byte[len]; + for (int i = 0; i < len; i++) { + array[i] = bytes[i]; + } + return array; + } + return null; + } + + /** + * char[] 转换 Character[] + * @param chars char[] + * @return {@link Character[]} + */ + public static Character[] charsToCharacters(final char[] chars) { + if (chars != null) { + int len = chars.length; + // 创建数组 + Character[] array = new Character[len]; + for (int i = 0; i < len; i++) { + array[i] = chars[i]; + } + return array; + } + return null; + } + + /** + * short[] 转换 Short[] + * @param shorts short[] + * @return {@link Short[]} + */ + public static Short[] shortsToShorts(final short[] shorts) { + if (shorts != null) { + int len = shorts.length; + // 创建数组 + Short[] array = new Short[len]; + for (int i = 0; i < len; i++) { + array[i] = shorts[i]; + } + return array; + } + return null; + } + + /** + * long[] 转换 Long[] + * @param longs long[] + * @return {@link Long[]} + */ + public static Long[] longsToLongs(final long[] longs) { + if (longs != null) { + int len = longs.length; + // 创建数组 + Long[] array = new Long[len]; + for (int i = 0; i < len; i++) { + array[i] = longs[i]; + } + return array; + } + return null; + } + + /** + * float[] 转换 Float[] + * @param floats float[] + * @return {@link Float[]} + */ + public static Float[] floatsToFloats(final float[] floats) { + if (floats != null) { + int len = floats.length; + // 创建数组 + Float[] array = new Float[len]; + for (int i = 0; i < len; i++) { + array[i] = floats[i]; + } + return array; + } + return null; + } + + /** + * double[] 转换 Double[] + * @param doubles double[] + * @return {@link Double[]} + */ + public static Double[] doublesToDoubles(final double[] doubles) { + if (doubles != null) { + int len = doubles.length; + // 创建数组 + Double[] array = new Double[len]; + for (int i = 0; i < len; i++) { + array[i] = doubles[i]; + } + return array; + } + return null; + } + + /** + * boolean[] 转换 Boolean[] + * @param booleans boolean[] + * @return {@link Boolean[]} + */ + public static Boolean[] booleansToBooleans(final boolean[] booleans) { + if (booleans != null) { + int len = booleans.length; + // 创建数组 + Boolean[] array = new Boolean[len]; + for (int i = 0; i < len; i++) { + array[i] = booleans[i]; + } + return array; + } + return null; + } + + // = + + /** + * Integer[] 转换 int[] + * @param integers Integer[] + * @param defaultValue 转换失败使用得默认值 + * @return int[] + */ + public static int[] integersToInts(final Integer[] integers, final int defaultValue) { + if (integers != null) { + int len = integers.length; + // 创建数组 + int[] array = new int[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = integers[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Byte[] 转换 byte[] + * @param bytes Byte[] + * @param defaultValue 转换失败使用得默认值 + * @return byte[] + */ + public static byte[] bytesToBytes(final Byte[] bytes, final byte defaultValue) { + if (bytes != null) { + int len = bytes.length; + // 创建数组 + byte[] array = new byte[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = bytes[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Character[] 转换 char[] + * @param characters Character[] + * @param defaultValue 转换失败使用得默认值 + * @return char[] + */ + public static char[] charactersToChars(final Character[] characters, final char defaultValue) { + if (characters != null) { + int len = characters.length; + // 创建数组 + char[] array = new char[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = characters[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Short[] 转换 short[] + * @param shorts Short[] + * @param defaultValue 转换失败使用得默认值 + * @return short[] + */ + public static short[] shortsToShorts(final Short[] shorts, final short defaultValue) { + if (shorts != null) { + int len = shorts.length; + // 创建数组 + short[] array = new short[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = shorts[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Long[] 转换 long[] + * @param longs Long[] + * @param defaultValue 转换失败使用得默认值 + * @return long[] + */ + public static long[] longsToLongs(final Long[] longs, final long defaultValue) { + if (longs != null) { + int len = longs.length; + // 创建数组 + long[] array = new long[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = longs[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Float[] 转换 float[] + * @param floats Float[] + * @param defaultValue 转换失败使用得默认值 + * @return float[] + */ + public static float[] floatsToFloats(final Float[] floats, final float defaultValue) { + if (floats != null) { + int len = floats.length; + // 创建数组 + float[] array = new float[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = floats[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Double[] 转换 double[] + * @param doubles Double[] + * @param defaultValue 转换失败使用得默认值 + * @return double[] + */ + public static double[] doublesToDoubles(final Double[] doubles, final double defaultValue) { + if (doubles != null) { + int len = doubles.length; + // 创建数组 + double[] array = new double[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = doubles[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + /** + * Boolean[] 转换 boolean[] + * @param booleans Boolean[] + * @param defaultValue 转换失败使用得默认值 + * @return boolean[] + */ + public static boolean[] booleansToBooleans(final Boolean[] booleans, final boolean defaultValue) { + if (booleans != null) { + int len = booleans.length; + // 创建数组 + boolean[] array = new boolean[len]; + for (int i = 0; i < len; i++) { + try { + array[i] = booleans[i]; + } catch (Exception e) { + array[i] = defaultValue; + } + } + return array; + } + return null; + } + + // ============= + // = 转换 List = + // ============= + + /** + * 转换数组为集合 + * @param array 数组 + * @param 泛型 + * @return {@link List} + */ + public static List asList(final T[] array) { + if (array != null) { + try { + return new ArrayList<>(Arrays.asList(array)); + } catch (Exception e) { + ALog.eTag(TAG, e, "asList"); + } + } + return null; + } + + // = + + /** + * 转换数组为集合 + * @param ints int[] + * @return {@link List} + */ + public static List asList(final int[] ints) { + if (ints != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = ints.length; i < len; i++) { + lists.add(ints[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param bytes byte[] + * @return {@link List} + */ + public static List asList(final byte[] bytes) { + if (bytes != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = bytes.length; i < len; i++) { + lists.add(bytes[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param chars char[] + * @return {@link List} + */ + public static List asList(final char[] chars) { + if (chars != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = chars.length; i < len; i++) { + lists.add(chars[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param shorts short[] + * @return {@link List} + */ + public static List asList(final short[] shorts) { + if (shorts != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = shorts.length; i < len; i++) { + lists.add(shorts[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param longs long[] + * @return {@link List} + */ + public static List asList(final long[] longs) { + if (longs != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = longs.length; i < len; i++) { + lists.add(longs[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param floats float[] + * @return {@link List} + */ + public static List asList(final float[] floats) { + if (floats != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = floats.length; i < len; i++) { + lists.add(floats[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param doubles double[] + * @return {@link List} + */ + public static List asList(final double[] doubles) { + if (doubles != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = doubles.length; i < len; i++) { + lists.add(doubles[i]); + } + return lists; + } + return null; + } + + /** + * 转换数组为集合 + * @param booleans boolean[] + * @return {@link List} + */ + public static List asList(final boolean[] booleans) { + if (booleans != null) { + List lists = new ArrayList<>(); + for (int i = 0, len = booleans.length; i < len; i++) { + lists.add(booleans[i]); + } + return lists; + } + return null; + } + + // ============ + // = 快捷判断 = + // ============ + + /** + * 判断两个值是否一样 + * @param value1 第一个值 + * @param value2 第二个值 + * @param 泛型 + * @return {@code true} yes, {@code false} no + */ + public static boolean equals(final T value1, final T value2) { + return value1.equals(value2); + } + + // = + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @param 泛型 + * @return 拼接后的数组集合 + */ + public static T[] arraycopy(final T[] prefix, final T[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建集合 + List lists = new ArrayList<>(prefixLength + suffixLength); + // 进行判断处理 + if (prefixLength != 0) { + for (int i = 0; i < prefixLength; i++) { + lists.add(prefix[i]); + } + } + if (suffixLength != 0) { + for (int i = 0; i < suffixLength; i++) { + lists.add(suffix[i]); + } + } + if (prefix != null) { + return (T[]) Arrays.copyOf(lists.toArray(), lists.size(), prefix.getClass()); + } else { + return (T[]) Arrays.copyOf(lists.toArray(), lists.size(), suffix.getClass()); + } + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static int[] arraycopy(final int[] prefix, final int[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + int[] arrays = new int[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static byte[] arraycopy(final byte[] prefix, final byte[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + byte[] arrays = new byte[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static char[] arraycopy(final char[] prefix, final char[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + char[] arrays = new char[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static short[] arraycopy(final short[] prefix, final short[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + short[] arrays = new short[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static long[] arraycopy(final long[] prefix, final long[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + long[] arrays = new long[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static float[] arraycopy(final float[] prefix, final float[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + float[] arrays = new float[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static double[] arraycopy(final double[] prefix, final double[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + double[] arrays = new double[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + /** + * 拼接数组 + * @param prefix 第一个数组 + * @param suffix 第二个数组 + * @return 拼接后的数组 + */ + public static boolean[] arraycopy(final boolean[] prefix, final boolean[] suffix) { + // 获取数据长度 + int prefixLength = (prefix != null) ? prefix.length : 0; + int suffixLength = (suffix != null) ? suffix.length : 0; + // 数据都为 null, 则直接跳过 + if (prefixLength + suffixLength == 0) return null; + // 创建数组 + boolean[] arrays = new boolean[prefixLength + suffixLength]; + // 进行判断处理 + if (prefixLength != 0) { + System.arraycopy(prefix, 0, arrays, 0, prefixLength); + } + if (suffixLength != 0) { + System.arraycopy(suffix, 0, arrays, prefixLength, suffixLength); + } + return arrays; + } + + // = + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @param 泛型 + * @return 指定长度数组 + */ + public static T[] newarray(final int length, final T[] data) { + if (data != null && length > 0) { + List lists = new ArrayList<>(); + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + lists.add(data[i]); + } + } else { + for (int i = 0; i < dataLength; i++) { + lists.add(data[i]); + } + // 补充长度 + for (int i = 0, len = length - dataLength; i < len; i++) { + lists.add(null); + } + } + // 返回数据 + return (T[]) Arrays.copyOf(lists.toArray(), length, data.getClass()); + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static int[] newarray(final int length, final int[] data) { + if (data != null && length > 0) { + int[] arrays = new int[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static byte[] newarray(final int length, final byte[] data) { + if (data != null && length > 0) { + byte[] arrays = new byte[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static char[] newarray(final int length, final char[] data) { + if (data != null && length > 0) { + char[] arrays = new char[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static short[] newarray(final int length, final short[] data) { + if (data != null && length > 0) { + short[] arrays = new short[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static long[] newarray(final int length, final long[] data) { + if (data != null && length > 0) { + long[] arrays = new long[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static float[] newarray(final int length, final float[] data) { + if (data != null && length > 0) { + float[] arrays = new float[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static double[] newarray(final int length, final double[] data) { + if (data != null && length > 0) { + double[] arrays = new double[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + /** + * 创建指定长度数组 + * @param length 保留长度 + * @param data 待处理数组 + * @return 指定长度数组 + */ + public static boolean[] newarray(final int length, final boolean[] data) { + if (data != null && length > 0) { + boolean[] arrays = new boolean[length]; + // 获取数据长度 + int dataLength = data.length; + // 判断是否超过需要的长度 + if (dataLength >= length) { + for (int i = 0; i < length; i++) { + arrays[i] = data[i]; + } + } else { + for (int i = 0; i < dataLength; i++) { + arrays[i] = data[i]; + } + } + return arrays; + } + return null; + } + + // = + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @param 泛型 + * @return 裁剪后的数组 + */ + public static T[] subarray(final T[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + List lists = new ArrayList<>(length); + for (int i = off; i < off + length; i++) { + lists.add(data[i]); + } + return (T[]) Arrays.copyOf(lists.toArray(), length, data.getClass()); + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static int[] subarray(final int[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + int[] arrays = new int[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static byte[] subarray(final byte[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + byte[] arrays = new byte[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static char[] subarray(final char[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + char[] arrays = new char[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static short[] subarray(final short[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + short[] arrays = new short[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static long[] subarray(final long[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + long[] arrays = new long[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static float[] subarray(final float[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + float[] arrays = new float[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static double[] subarray(final double[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + double[] arrays = new double[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + /** + * 从数组上截取一段 + * @param data 数组 + * @param off 起始值 + * @param length 所需长度 + * @return 裁剪后的数组 + */ + public static boolean[] subarray(final boolean[] data, final int off, final int length) { + if (data == null || off < 0 || length < 0) return null; + try { + boolean[] arrays = new boolean[length]; + System.arraycopy(data, off, arrays, 0, length); + return arrays; + } catch (Exception e) { + ALog.eTag(TAG, e, "subarray"); + } + return null; + } + + // = + + /** + * 追加数组内容字符串 + * @param data 数组 + * @param 泛型 + * @return 追加数组内容字符串 + */ + public static String appendToString(final T[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + ((data[0] == null) ? "null" : data[0].toString()) + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + T t = data[i]; + builder.append("\"").append(((t == null) ? "null" : t.toString())).append("\","); + } + T end = data[len - 1]; + builder.append("\"" + ((end == null) ? "null" : end.toString()) + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final int[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final byte[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final char[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final short[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final long[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final float[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final double[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + /** + * 追加数组内容字符串 + * @param data 数组 + * @return 追加数组内容字符串 + */ + public static String appendToString(final boolean[] data) { + if (data != null) { + int len = data.length; + if (len != 0) { + if (len == 1) { + return "\"" + data[0] + "\""; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len - 1; i++) { + builder.append("\"").append(data[i]).append("\","); + } + builder.append("\"" + data[len - 1] + "\""); + return builder.toString(); + } + } + } + return ""; + } + + // ============== + // = 最小值索引 = + // ============== + + /** + * 获取数组中最小值索引 + * @param data 数组 + * @return 最小值索引 + */ + public static int getMinimumIndex(final int[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + int temp = data[index]; + for (int i = 1; i < len; i++) { + int value = data[i]; + if (value < temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最小值索引 + * @param data 数组 + * @return 最小值索引 + */ + public static int getMinimumIndex(final long[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + long temp = data[index]; + for (int i = 1; i < len; i++) { + long value = data[i]; + if (value < temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最小值索引 + * @param data 数组 + * @return 最小值索引 + */ + public static int getMinimumIndex(final float[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + float temp = data[index]; + for (int i = 1; i < len; i++) { + float value = data[i]; + if (value < temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最小值索引 + * @param data 数组 + * @return 最小值索引 + */ + public static int getMinimumIndex(final double[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + double temp = data[index]; + for (int i = 1; i < len; i++) { + double value = data[i]; + if (value < temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + // ============== + // = 最大值索引 = + // ============== + + /** + * 获取数组中最大值索引 + * @param data 数组 + * @return 最大值索引 + */ + public static int getMaximumIndex(final int[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + int temp = data[index]; + for (int i = 1; i < len; i++) { + int value = data[i]; + if (value > temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最大值索引 + * @param data 数组 + * @return 最大值索引 + */ + public static int getMaximumIndex(final long[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + long temp = data[index]; + for (int i = 1; i < len; i++) { + long value = data[i]; + if (value > temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最大值索引 + * @param data 数组 + * @return 最大值索引 + */ + public static int getMaximumIndex(final float[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + float temp = data[index]; + for (int i = 1; i < len; i++) { + float value = data[i]; + if (value > temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + /** + * 获取数组中最大值索引 + * @param data 数组 + * @return 最大值索引 + */ + public static int getMaximumIndex(final double[] data) { + if (data != null) { + int len = data.length; + if (len > 0) { + int index = 0; + double temp = data[index]; + for (int i = 1; i < len; i++) { + double value = data[i]; + if (value > temp) { + index = i; + temp = value; + } + } + return index; + } + } + return -1; + } + + // ============== + // = 获取最小值 = + // ============== + + /** + * 获取数组中最小值 + * @param data 数组 + * @return 最小值 + */ + public static int getMinimum(final int[] data) { + try { + return data[getMinimumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMinimum"); + } + return 0; + } + + /** + * 获取数组中最小值 + * @param data 数组 + * @return 最小值 + */ + public static long getMinimum(final long[] data) { + try { + return data[getMinimumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMinimum"); + } + return 0L; + } + + /** + * 获取数组中最小值 + * @param data 数组 + * @return 最小值 + */ + public static float getMinimum(final float[] data) { + try { + return data[getMinimumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMinimum"); + } + return 0f; + } + + /** + * 获取数组中最小值 + * @param data 数组 + * @return 最小值 + */ + public static double getMinimum(final double[] data) { + try { + return data[getMinimumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMinimum"); + } + return 0d; + } + + // ============== + // = 获取最大值 = + // ============== + + /** + * 获取数组中最大值 + * @param data 数组 + * @return 最大值 + */ + public static int getMaximum(final int[] data) { + try { + return data[getMaximumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMaximum"); + } + return 0; + } + + /** + * 获取数组中最大值 + * @param data 数组 + * @return 最大值 + */ + public static long getMaximum(final long[] data) { + try { + return data[getMaximumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMaximum"); + } + return 0L; + } + + /** + * 获取数组中最大值 + * @param data 数组 + * @return 最大值 + */ + public static float getMaximum(final float[] data) { + try { + return data[getMaximumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMaximum"); + } + return 0f; + } + + /** + * 获取数组中最大值 + * @param data 数组 + * @return 最大值 + */ + public static double getMaximum(final double[] data) { + try { + return data[getMaximumIndex(data)]; + } catch (Exception e) { + ALog.eTag(TAG, e, "getMaximum"); + } + return 0d; + } + + // ================ + // = 计算数组总和 = + // ================ + + /** + * 计算数组总和 + * @param data 数组 + * @return 数组总和 + */ + public static int sumarray(final int[] data) { + return sumarray(data, 0, length(data), 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @return 数组总和 + */ + public static int sumarray(final int[] data, final int end) { + return sumarray(data, 0, end, 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static int sumarray(final int[] data, final int end, final int extra) { + return sumarray(data, 0, end, extra); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param start 开始位置 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static int sumarray(final int[] data, final int start, final int end, final int extra) { + int total = 0; + if (data != null) { + for (int i = start; i < end; i++) { + try { + total += (data[i] + extra); + } catch (Exception e) { + ALog.eTag(TAG, e, "sumarray"); + } + } + } + return total; + } + + // = + + /** + * 计算数组总和 + * @param data 数组 + * @return 数组总和 + */ + public static long sumarray(final long[] data) { + return sumarray(data, 0, length(data), 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @return 数组总和 + */ + public static long sumarray(final long[] data, final int end) { + return sumarray(data, 0, end, 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static long sumarray(final long[] data, final int end, final long extra) { + return sumarray(data, 0, end, extra); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param start 开始位置 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static long sumarray(final long[] data, final int start, final int end, final long extra) { + long total = 0; + if (data != null) { + for (int i = start; i < end; i++) { + try { + total += (data[i] + extra); + } catch (Exception e) { + ALog.eTag(TAG, e, "sumarray"); + } + } + } + return total; + } + + // = + + /** + * 计算数组总和 + * @param data 数组 + * @return 数组总和 + */ + public static float sumarray(final float[] data) { + return sumarray(data, 0, length(data), 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @return 数组总和 + */ + public static float sumarray(final float[] data, final int end) { + return sumarray(data, 0, end, 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static float sumarray(final float[] data, final int end, final float extra) { + return sumarray(data, 0, end, extra); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param start 开始位置 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static float sumarray(final float[] data, final int start, final int end, final float extra) { + float total = 0; + if (data != null) { + for (int i = start; i < end; i++) { + try { + total += (data[i] + extra); + } catch (Exception e) { + ALog.eTag(TAG, e, "sumarray"); + } + } + } + return total; + } + + // = + + /** + * 计算数组总和 + * @param data 数组 + * @return 数组总和 + */ + public static double sumarray(final double[] data) { + return sumarray(data, 0, length(data), 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @return 数组总和 + */ + public static double sumarray(final double[] data, final int end) { + return sumarray(data, 0, end, 0); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static double sumarray(final double[] data, final int end, final double extra) { + return sumarray(data, 0, end, extra); + } + + /** + * 计算数组总和 + * @param data 数组 + * @param start 开始位置 + * @param end 结束位置 + * @param extra 额外值 + * @return 数组总和 + */ + public static double sumarray(final double[] data, final int start, final int end, final double extra) { + double total = 0; + if (data != null) { + for (int i = start; i < end; i++) { + try { + total += (data[i] + extra); + } catch (Exception e) { + ALog.eTag(TAG, e, "sumarray"); + } + } + } + return total; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/BarUtils.java b/app/src/main/java/com/example/baseframe/utils/BarUtils.java new file mode 100644 index 0000000..2a34e38 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/BarUtils.java @@ -0,0 +1,716 @@ +package com.example.baseframe.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Build; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.example.baseframe.api.App; + +import java.lang.reflect.Method; + +import static android.Manifest.permission.EXPAND_STATUS_BAR; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/09/23
+ *     desc  : utils about bar
+ * 
+ */ +public final class BarUtils { + + /////////////////////////////////////////////////////////////////////////// + // status bar + /////////////////////////////////////////////////////////////////////////// + + private static final String TAG_STATUS_BAR = "TAG_STATUS_BAR"; + private static final String TAG_OFFSET = "TAG_OFFSET"; + private static final int KEY_OFFSET = -123; + + private BarUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Return the status bar's height. + * + * @return the status bar's height + */ + public static int getStatusBarHeight() { + Resources resources = App.getInstance().getResources(); + int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); + return resources.getDimensionPixelSize(resourceId); + } + + /** + * Set the status bar's visibility. + * + * @param activity The activity. + * @param isVisible True to set status bar visible, false otherwise. + */ + public static void setStatusBarVisibility(@NonNull final Activity activity, + final boolean isVisible) { + setStatusBarVisibility(activity.getWindow(), isVisible); + } + + /** + * Set the status bar's visibility. + * + * @param window The window. + * @param isVisible True to set status bar visible, false otherwise. + */ + public static void setStatusBarVisibility(@NonNull final Window window, + final boolean isVisible) { + if (isVisible) { + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + showStatusBarView(window); + addMarginTopEqualStatusBarHeight(window); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + hideStatusBarView(window); + subtractMarginTopEqualStatusBarHeight(window); + } + } + + /** + * Return whether the status bar is visible. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarVisible(@NonNull final Activity activity) { + int flags = activity.getWindow().getAttributes().flags; + return (flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0; + } + + /** + * Set the status bar's light mode. + * + * @param activity The activity. + * @param isLightMode True to set status bar light mode, false otherwise. + */ + public static void setStatusBarLightMode(@NonNull final Activity activity, + final boolean isLightMode) { + setStatusBarLightMode(activity.getWindow(), isLightMode); + } + + /** + * Set the status bar's light mode. + * + * @param window The window. + * @param isLightMode True to set status bar light mode, false otherwise. + */ + public static void setStatusBarLightMode(@NonNull final Window window, + final boolean isLightMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + if (isLightMode) { + vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + decorView.setSystemUiVisibility(vis); + } + } + + /** + * Is the status bar light mode. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarLightMode(@NonNull final Activity activity) { + return isStatusBarLightMode(activity.getWindow()); + } + + /** + * Is the status bar light mode. + * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarLightMode(@NonNull final Window window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + return (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0; + } + return false; + } + + /** + * Add the top margin size equals status bar's height for view. + * + * @param view The view. + */ + public static void addMarginTopEqualStatusBarHeight(@NonNull View view) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + view.setTag(TAG_OFFSET); + Object haveSetOffset = view.getTag(KEY_OFFSET); + if (haveSetOffset != null && (Boolean) haveSetOffset) return; + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, + layoutParams.topMargin + getStatusBarHeight(), + layoutParams.rightMargin, + layoutParams.bottomMargin); + view.setTag(KEY_OFFSET, true); + } + + /** + * Subtract the top margin size equals status bar's height for view. + * + * @param view The view. + */ + public static void subtractMarginTopEqualStatusBarHeight(@NonNull View view) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + Object haveSetOffset = view.getTag(KEY_OFFSET); + if (haveSetOffset == null || !(Boolean) haveSetOffset) return; + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, + layoutParams.topMargin - getStatusBarHeight(), + layoutParams.rightMargin, + layoutParams.bottomMargin); + view.setTag(KEY_OFFSET, false); + } + + private static void addMarginTopEqualStatusBarHeight(final Window window) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET); + if (withTag == null) return; + addMarginTopEqualStatusBarHeight(withTag); + } + + private static void subtractMarginTopEqualStatusBarHeight(final Window window) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET); + if (withTag == null) return; + subtractMarginTopEqualStatusBarHeight(withTag); + } + + /** + * Set the status bar's color. + * + * @param activity The activity. + * @param color The status bar's color. + */ + public static View setStatusBarColor(@NonNull final Activity activity, + @ColorInt final int color) { + return setStatusBarColor(activity, color, false); + } + + /** + * Set the status bar's color. + * + * @param activity The activity. + * @param color The status bar's color. + * @param isDecor True to add fake status bar in DecorView, + * false to add fake status bar in ContentView. + */ + public static View setStatusBarColor(@NonNull final Activity activity, + @ColorInt final int color, + final boolean isDecor) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return null; + transparentStatusBar(activity); + return applyStatusBarColor(activity, color, isDecor); + } + + + /** + * Set the status bar's color. + * + * @param window The window. + * @param color The status bar's color. + */ + public static View setStatusBarColor(@NonNull final Window window, + @ColorInt final int color) { + return setStatusBarColor(window, color, false); + } + + /** + * Set the status bar's color. + * + * @param window The window. + * @param color The status bar's color. + * @param isDecor True to add fake status bar in DecorView, + * false to add fake status bar in ContentView. + */ + public static View setStatusBarColor(@NonNull final Window window, + @ColorInt final int color, + final boolean isDecor) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return null; + transparentStatusBar(window); + return applyStatusBarColor(window, color, isDecor); + } + + /** + * Set the status bar's color. + * + * @param fakeStatusBar The fake status bar view. + * @param color The status bar's color. + */ + public static void setStatusBarColor(@NonNull final View fakeStatusBar, + @ColorInt final int color) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + Activity activity = getActivityByView(fakeStatusBar); + if (activity == null) return; + transparentStatusBar(activity); + fakeStatusBar.setVisibility(View.VISIBLE); + ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = getStatusBarHeight(); + fakeStatusBar.setBackgroundColor(color); + } + + /** + * Set the custom status bar. + * + * @param fakeStatusBar The fake status bar view. + */ + public static void setStatusBarCustom(@NonNull final View fakeStatusBar) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + Activity activity = getActivityByView(fakeStatusBar); + if (activity == null) return; + transparentStatusBar(activity); + fakeStatusBar.setVisibility(View.VISIBLE); + ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams(); + if (layoutParams == null) { + layoutParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + getStatusBarHeight() + ); + fakeStatusBar.setLayoutParams(layoutParams); + } else { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = getStatusBarHeight(); + } + } + + /** + * Set the status bar's color for DrawerLayout. + *

DrawLayout must add {@code android:fitsSystemWindows="true"}

+ * + * @param drawer The DrawLayout. + * @param fakeStatusBar The fake status bar view. + * @param color The status bar's color. + */ + public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer, + @NonNull final View fakeStatusBar, + @ColorInt final int color) { + setStatusBarColor4Drawer(drawer, fakeStatusBar, color, false); + } + + /** + * Set the status bar's color for DrawerLayout. + *

DrawLayout must add {@code android:fitsSystemWindows="true"}

+ * + * @param drawer The DrawLayout. + * @param fakeStatusBar The fake status bar view. + * @param color The status bar's color. + * @param isTop True to set DrawerLayout at the top layer, false otherwise. + */ + public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer, + @NonNull final View fakeStatusBar, + @ColorInt final int color, + final boolean isTop) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + Activity activity = getActivityByView(fakeStatusBar); + if (activity == null) return; + transparentStatusBar(activity); + drawer.setFitsSystemWindows(false); + setStatusBarColor(fakeStatusBar, color); + for (int i = 0, count = drawer.getChildCount(); i < count; i++) { + drawer.getChildAt(i).setFitsSystemWindows(false); + } + if (isTop) { + hideStatusBarView(activity); + } else { + setStatusBarColor(activity, color, false); + } + } + + private static View applyStatusBarColor(final Activity activity, + final int color, + boolean isDecor) { + return applyStatusBarColor(activity.getWindow(), color, isDecor); + } + + private static View applyStatusBarColor(final Window window, + final int color, + boolean isDecor) { + ViewGroup parent = isDecor ? + (ViewGroup) window.getDecorView() : + (ViewGroup) window.findViewById(android.R.id.content); + View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(color); + } else { + fakeStatusBarView = createStatusBarView(window.getContext(), color); + parent.addView(fakeStatusBarView); + } + return fakeStatusBarView; + } + + private static void hideStatusBarView(final Activity activity) { + hideStatusBarView(activity.getWindow()); + } + + private static void hideStatusBarView(final Window window) { + ViewGroup decorView = (ViewGroup) window.getDecorView(); + View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView == null) return; + fakeStatusBarView.setVisibility(View.GONE); + } + + private static void showStatusBarView(final Window window) { + ViewGroup decorView = (ViewGroup) window.getDecorView(); + View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView == null) return; + fakeStatusBarView.setVisibility(View.VISIBLE); + } + + private static View createStatusBarView(final Context context, + final int color) { + View statusBarView = new View(context); + statusBarView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight())); + statusBarView.setBackgroundColor(color); + statusBarView.setTag(TAG_STATUS_BAR); + return statusBarView; + } + + public static void transparentStatusBar(final Activity activity) { + transparentStatusBar(activity.getWindow()); + } + + public static void transparentStatusBar(final Window window) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + int vis = window.getDecorView().getSystemUiVisibility(); + window.getDecorView().setSystemUiVisibility(option | vis); + window.setStatusBarColor(Color.TRANSPARENT); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + /////////////////////////////////////////////////////////////////////////// + // action bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Return the action bar's height. + * + * @return the action bar's height + */ + public static int getActionBarHeight() { + TypedValue tv = new TypedValue(); + if (App.getInstance().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + return TypedValue.complexToDimensionPixelSize( + tv.data, App.getInstance().getResources().getDisplayMetrics() + ); + } + return 0; + } + + /////////////////////////////////////////////////////////////////////////// + // notification bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Set the notification bar's visibility. + *

Must hold {@code }

+ * + * @param isVisible True to set notification bar visible, false otherwise. + */ + @RequiresPermission(EXPAND_STATUS_BAR) + public static void setNotificationBarVisibility(final boolean isVisible) { + String methodName; + if (isVisible) { + methodName = (Build.VERSION.SDK_INT <= 16) ? "expand" : "expandNotificationsPanel"; + } else { + methodName = (Build.VERSION.SDK_INT <= 16) ? "collapse" : "collapsePanels"; + } + invokePanels(methodName); + } + + private static void invokePanels(final String methodName) { + try { + @SuppressLint("WrongConstant") + Object service = App.getInstance().getSystemService("statusbar"); + @SuppressLint("PrivateApi") + Class statusBarManager = Class.forName("android.app.StatusBarManager"); + Method expand = statusBarManager.getMethod(methodName); + expand.invoke(service); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /////////////////////////////////////////////////////////////////////////// + // navigation bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Return the navigation bar's height. + * + * @return the navigation bar's height + */ + public static int getNavBarHeight() { + Resources res = App.getInstance().getResources(); + int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId != 0) { + return res.getDimensionPixelSize(resourceId); + } else { + return 0; + } + } + + /** + * Set the navigation bar's visibility. + * + * @param activity The activity. + * @param isVisible True to set navigation bar visible, false otherwise. + */ + public static void setNavBarVisibility(@NonNull final Activity activity, boolean isVisible) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + setNavBarVisibility(activity.getWindow(), isVisible); + + } + + /** + * Set the navigation bar's visibility. + * + * @param window The window. + * @param isVisible True to set navigation bar visible, false otherwise. + */ + public static void setNavBarVisibility(@NonNull final Window window, boolean isVisible) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; + final ViewGroup decorView = (ViewGroup) window.getDecorView(); + for (int i = 0, count = decorView.getChildCount(); i < count; i++) { + final View child = decorView.getChildAt(i); + final int id = child.getId(); + if (id != View.NO_ID) { + String resourceEntryName = App.getInstance() + .getResources() + .getResourceEntryName(id); + if ("navigationBarBackground".equals(resourceEntryName)) { + child.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); + } + } + } + final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + if (isVisible) { + decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~uiOptions); + } else { + decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | uiOptions); + } + } + + /** + * Return whether the navigation bar visible. + *

Call it in onWindowFocusChanged will get right result.

+ * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarVisible(@NonNull final Activity activity) { + return isNavBarVisible(activity.getWindow()); + } + + /** + * Return whether the navigation bar visible. + *

Call it in onWindowFocusChanged will get right result.

+ * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarVisible(@NonNull final Window window) { + boolean isVisible = false; + ViewGroup decorView = (ViewGroup) window.getDecorView(); + for (int i = 0, count = decorView.getChildCount(); i < count; i++) { + final View child = decorView.getChildAt(i); + final int id = child.getId(); + if (id != View.NO_ID) { + String resourceEntryName = App.getInstance() + .getResources() + .getResourceEntryName(id); + if ("navigationBarBackground".equals(resourceEntryName) + && child.getVisibility() == View.VISIBLE) { + isVisible = true; + break; + } + } + } + if (isVisible) { + int visibility = decorView.getSystemUiVisibility(); + isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + } + return isVisible; + } + + /** + * Set the navigation bar's color. + * + * @param activity The activity. + * @param color The navigation bar's color. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void setNavBarColor(@NonNull final Activity activity, @ColorInt final int color) { + setNavBarColor(activity.getWindow(), color); + } + + /** + * Set the navigation bar's color. + * + * @param window The window. + * @param color The navigation bar's color. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) { + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setNavigationBarColor(color); + } + + /** + * Return the color of navigation bar. + * + * @param activity The activity. + * @return the color of navigation bar + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static int getNavBarColor(@NonNull final Activity activity) { + return getNavBarColor(activity.getWindow()); + } + + /** + * Return the color of navigation bar. + * + * @param window The window. + * @return the color of navigation bar + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static int getNavBarColor(@NonNull final Window window) { + return window.getNavigationBarColor(); + } + + /** + * Return whether the navigation bar visible. + * + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isSupportNavBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + WindowManager wm = (WindowManager) App.getInstance().getSystemService(Context.WINDOW_SERVICE); + if (wm == null) return false; + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + Point realSize = new Point(); + display.getSize(size); + display.getRealSize(realSize); + return realSize.y != size.y || realSize.x != size.x; + } + boolean menu = ViewConfiguration.get(App.getInstance()).hasPermanentMenuKey(); + boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + return !menu && !back; + } + + /** + * Set the nav bar's light mode. + * + * @param activity The activity. + * @param isLightMode True to set nav bar light mode, false otherwise. + */ + public static void setNavBarLightMode(@NonNull final Activity activity, + final boolean isLightMode) { + setStatusBarLightMode(activity.getWindow(), isLightMode); + } + + /** + * Set the nav bar's light mode. + * + * @param window The window. + * @param isLightMode True to set nav bar light mode, false otherwise. + */ + public static void setNavBarLightMode(@NonNull final Window window, + final boolean isLightMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + if (isLightMode) { + vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } else { + vis &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + decorView.setSystemUiVisibility(vis); + } + } + + /** + * Is the nav bar light mode. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarLightMode(@NonNull final Activity activity) { + return isStatusBarLightMode(activity.getWindow()); + } + + /** + * Is the nav bar light mode. + * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarLightMode(@NonNull final Window window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + return (vis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0; + } + return false; + } + + private static Activity getActivityByView(@NonNull final View view) { + Context context = view.getContext(); + while (context instanceof ContextWrapper) { + if (context instanceof Activity) { + return (Activity) context; + } + context = ((ContextWrapper) context).getBaseContext(); + } + Log.e("BarUtils", "the view's Context is not an Activity."); + return null; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/Base64.java b/app/src/main/java/com/example/baseframe/utils/Base64.java new file mode 100644 index 0000000..ad84ad6 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/Base64.java @@ -0,0 +1,273 @@ +package com.example.baseframe.utils; + +public final class Base64 { + + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static private final boolean fDebug = false; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + if (fDebug) { + System.out.println("number of triplets = " + numberTriplets); + } + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + if (fDebug) { + System.out.println("b1= " + b1 + ", b2= " + b2 + ", b3= " + b3); + } + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + if (fDebug) { + System.out.println("val2 = " + val2); + System.out.println("k4 = " + (k << 4)); + System.out.println("vak = " + (val2 | (k << 4))); + } + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + if (fDebug) { + System.out.println("b1=" + b1); + System.out.println("b1<<2 = " + (b1 >> 2)); + } + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;//should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }//if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null;//if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {//Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)//last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)//last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { //No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/ButtonUtils.java b/app/src/main/java/com/example/baseframe/utils/ButtonUtils.java new file mode 100644 index 0000000..7161e89 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ButtonUtils.java @@ -0,0 +1,49 @@ +package com.example.baseframe.utils; + +import android.util.Log; + +/** + * Created by yzh + */ +public class ButtonUtils { + private static long lastClickTime = 0; + private static long DIFF = 500; + private static int lastButtonId = -1; + + /** + * 判断两次点击的间隔,如果小于500,则认为是多次无效点击 + * + * @return + */ + public static boolean isFastDoubleClick() { + return isFastDoubleClick(-1, DIFF); + } + + /** + * 判断两次点击的间隔,如果小于1000,则认为是多次无效点击 + * + * @return + */ + public static boolean isFastDoubleClick(int buttonId) { + return isFastDoubleClick(buttonId, DIFF); + } + + /** + * 判断两次点击的间隔,如果小于diff,则认为是多次无效点击 + * + * @param diff + * @return + */ + public static boolean isFastDoubleClick(int buttonId, long diff) { + long time = System.currentTimeMillis(); + long timeD = time - lastClickTime; + if (lastButtonId == buttonId && lastClickTime > 0 && timeD < diff) { + Log.v("isFastDoubleClick", "短时间内按钮多次触发"); + return true; + } + lastClickTime = time; + lastButtonId = buttonId; + return false; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/CheckNetwork.java b/app/src/main/java/com/example/baseframe/utils/CheckNetwork.java new file mode 100644 index 0000000..0a1e19a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/CheckNetwork.java @@ -0,0 +1,53 @@ +package com.example.baseframe.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import com.example.baseframe.api.App; + +/** + * 用于判断是不是联网状态 + * + * @author Dzy + */ +public class CheckNetwork { + + /** + * 判断网络是否连通 + */ + public static boolean isNetworkConnected() { + Context context= App.getInstance(); + try { + if(context!=null){ + @SuppressWarnings("static-access") + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + }else{ + /**如果context为空,就返回false,表示网络未连接*/ + return false; + } + }catch (Exception e){ + e.printStackTrace(); + return false; + } + + + } + + public static boolean isWifiConnected(Context context) { + if (context != null) { + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && (info.getType() == ConnectivityManager.TYPE_WIFI); + } else { + /**如果context为null就表示为未连接*/ + return false; + } + + } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/ClassUtil.java b/app/src/main/java/com/example/baseframe/utils/ClassUtil.java new file mode 100644 index 0000000..234f2d5 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ClassUtil.java @@ -0,0 +1,43 @@ +package com.example.baseframe.utils; + + +import androidx.lifecycle.AndroidViewModel; + + +import com.example.baseframe.base.BaseViewModel; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + + +public class ClassUtil { + + /** + * 获取泛型ViewModel的class对象 + * + * 如果 直接传进来的就是BaseViewModel,则不创建 + */ + public static Class getViewModel(Object obj) { + Class currentClass = obj.getClass(); + Class tClass = getGenericClass(currentClass, AndroidViewModel.class); +// if (tClass == null || tClass == AndroidViewModel.class || tClass == NoViewModel.class) { + if (tClass == null || tClass == BaseViewModel.class) { //如果 直接传进来的就是BaseViewModel,则不创建 + return null; + } + return tClass; + } + + private static Class getGenericClass(Class klass, Class filterClass) { + Type type = klass.getGenericSuperclass(); + if (type == null || !(type instanceof ParameterizedType)) return null; + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] types = parameterizedType.getActualTypeArguments(); + for (Type t : types) { + Class tClass = (Class) t; + if (filterClass.isAssignableFrom(tClass)) { + return tClass; + } + } + return null; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/ClickUtils.java b/app/src/main/java/com/example/baseframe/utils/ClickUtils.java new file mode 100644 index 0000000..3e19b85 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ClickUtils.java @@ -0,0 +1,513 @@ +package com.example.baseframe.utils; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.os.Build; +import android.util.StateSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2019/06/12
+ *     desc  : utils about click
+ * 
+ */ +public class ClickUtils { + + private static final int PRESSED_VIEW_SCALE_TAG = -1; + private static final float PRESSED_VIEW_SCALE_DEFAULT_VALUE = -0.06f; + + private static final int PRESSED_VIEW_ALPHA_TAG = -2; + private static final int PRESSED_VIEW_ALPHA_SRC_TAG = -3; + private static final float PRESSED_VIEW_ALPHA_DEFAULT_VALUE = 0.8f; + + private static final int PRESSED_BG_ALPHA_STYLE = 4; + private static final float PRESSED_BG_ALPHA_DEFAULT_VALUE = 0.9f; + + private static final int PRESSED_BG_DARK_STYLE = 5; + private static final float PRESSED_BG_DARK_DEFAULT_VALUE = 0.9f; + + private static final int DEBOUNCING_TAG = -7; + private static final long DEBOUNCING_DEFAULT_VALUE = 200; + + private ClickUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Apply scale animation for the views' click. + * + * @param views The views. + */ + public static void applyPressedViewScale(final View... views) { + applyPressedViewScale(views, null); + } + + /** + * Apply scale animation for the views' click. + * + * @param views The views. + * @param scaleFactors The factors of scale for the views. + */ + public static void applyPressedViewScale(final View[] views, final float[] scaleFactors) { + if (views == null || views.length == 0) { + return; + } + for (int i = 0; i < views.length; i++) { + if (scaleFactors == null || i >= scaleFactors.length) { + applyPressedViewScale(views[i], PRESSED_VIEW_SCALE_DEFAULT_VALUE); + } else { + applyPressedViewScale(views[i], scaleFactors[i]); + } + } + } + + /** + * Apply scale animation for the views' click. + * + * @param view The view. + * @param scaleFactor The factor of scale for the view. + */ + public static void applyPressedViewScale(final View view, final float scaleFactor) { + if (view == null) { + return; + } + view.setTag(PRESSED_VIEW_SCALE_TAG, scaleFactor); + view.setClickable(true); + view.setOnTouchListener(OnUtilsTouchListener.getInstance()); + } + + /** + * Apply alpha for the views' click. + * + * @param views The views. + */ + public static void applyPressedViewAlpha(final View... views) { + applyPressedViewAlpha(views, null); + } + + /** + * Apply alpha for the views' click. + * + * @param views The views. + * @param alphas The alphas for the views. + */ + public static void applyPressedViewAlpha(final View[] views, final float[] alphas) { + if (views == null || views.length == 0) return; + for (int i = 0; i < views.length; i++) { + if (alphas == null || i >= alphas.length) { + applyPressedViewAlpha(views[i], PRESSED_VIEW_ALPHA_DEFAULT_VALUE); + } else { + applyPressedViewAlpha(views[i], alphas[i]); + } + } + } + + + /** + * Apply scale animation for the views' click. + * + * @param view The view. + * @param alpha The alpha for the view. + */ + public static void applyPressedViewAlpha(final View view, final float alpha) { + if (view == null) { + return; + } + view.setTag(PRESSED_VIEW_ALPHA_TAG, alpha); + view.setTag(PRESSED_VIEW_ALPHA_SRC_TAG, view.getAlpha()); + view.setClickable(true); + view.setOnTouchListener(OnUtilsTouchListener.getInstance()); + } + + /** + * Apply alpha for the view's background. + * + * @param view The views. + */ + public static void applyPressedBgAlpha(View view) { + applyPressedBgAlpha(view, PRESSED_BG_ALPHA_DEFAULT_VALUE); + } + + /** + * Apply alpha for the view's background. + * + * @param view The views. + * @param alpha The alpha. + */ + public static void applyPressedBgAlpha(View view, float alpha) { + applyPressedBgStyle(view, PRESSED_BG_ALPHA_STYLE, alpha); + } + + /** + * Apply alpha of dark for the view's background. + * + * @param view The views. + */ + public static void applyPressedBgDark(View view) { + applyPressedBgDark(view, PRESSED_BG_DARK_DEFAULT_VALUE); + } + + /** + * Apply alpha of dark for the view's background. + * + * @param view The views. + * @param darkAlpha The alpha of dark. + */ + public static void applyPressedBgDark(View view, float darkAlpha) { + applyPressedBgStyle(view, PRESSED_BG_DARK_STYLE, darkAlpha); + } + + private static void applyPressedBgStyle(View view, int style, float value) { + if (view == null) return; + Drawable background = view.getBackground(); + Object tag = view.getTag(-style); + if (tag instanceof Drawable) { + ViewCompat.setBackground(view, (Drawable) tag); + } else { + background = createStyleDrawable(background, style, value); + ViewCompat.setBackground(view, background); + view.setTag(-style, background); + } + } + + private static Drawable createStyleDrawable(Drawable src, int style, float value) { + if (src == null) { + src = new ColorDrawable(0); + } + if (src.getConstantState() == null) return src; + + Drawable pressed = src.getConstantState().newDrawable().mutate(); + if (style == PRESSED_BG_ALPHA_STYLE) { + pressed = createAlphaDrawable(pressed, value); + } else if (style == PRESSED_BG_DARK_STYLE) { + pressed = createDarkDrawable(pressed, value); + } + + Drawable disable = src.getConstantState().newDrawable().mutate(); + disable = createAlphaDrawable(pressed, 0.5f); + + StateListDrawable drawable = new StateListDrawable(); + drawable.addState(new int[]{android.R.attr.state_pressed}, pressed); + drawable.addState(new int[]{-android.R.attr.state_enabled}, disable); + drawable.addState(StateSet.WILD_CARD, src); + return drawable; + } + + private static Drawable createAlphaDrawable(Drawable drawable, float alpha) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT + && !(drawable instanceof ColorDrawable)) { + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas myCanvas = new Canvas(bitmap); + drawable.setAlpha((int) (alpha * 255)); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + drawable.draw(myCanvas); + return new BitmapDrawable(Resources.getSystem(), bitmap); + } + drawable.setAlpha((int) (alpha * 255)); + return drawable; + } + + private static Drawable createDarkDrawable(Drawable drawable, float alpha) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT && !(drawable instanceof ColorDrawable)) { + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas myCanvas = new Canvas(bitmap); + drawable.setColorFilter(getDarkColorFilter(alpha)); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + drawable.draw(myCanvas); + return new BitmapDrawable(Resources.getSystem(), bitmap); + } + drawable.setColorFilter(getDarkColorFilter(alpha)); + return drawable; + } + + private static ColorMatrixColorFilter getDarkColorFilter(float darkAlpha) { + return new ColorMatrixColorFilter(new ColorMatrix(new float[]{ + darkAlpha, 0, 0, 0, 0, + 0, darkAlpha, 0, 0, 0, + 0, 0, darkAlpha, 0, 0, + 0, 0, 0, 2, 0 + })); + } + + /** + * Apply single debouncing for the view's click. + * + * @param view The view. + * @param listener The listener. + */ + public static void applySingleDebouncing(final View view, final View.OnClickListener listener) { + applySingleDebouncing(new View[]{view}, listener); + } + + /** + * Apply single debouncing for the view's click. + * + * @param view The view. + * @param duration The duration of debouncing. + * @param listener The listener. + */ + public static void applySingleDebouncing(final View view, @IntRange(from = 0) long duration, + final View.OnClickListener listener) { + applySingleDebouncing(new View[]{view}, duration, listener); + } + + /** + * Apply single debouncing for the views' click. + * + * @param views The views. + * @param listener The listener. + */ + public static void applySingleDebouncing(final View[] views, final View.OnClickListener listener) { + applySingleDebouncing(views, DEBOUNCING_DEFAULT_VALUE, listener); + } + + /** + * Apply single debouncing for the views' click. + * + * @param views The views. + * @param duration The duration of debouncing. + * @param listener The listener. + */ + public static void applySingleDebouncing(final View[] views, + @IntRange(from = 0) long duration, + final View.OnClickListener listener) { + applyDebouncing(views, false, duration, listener); + } + + /** + * Apply global debouncing for the view's click. + * + * @param view The view. + * @param listener The listener. + */ + public static void applyGlobalDebouncing(final View view, final View.OnClickListener listener) { + applyGlobalDebouncing(new View[]{view}, listener); + } + + /** + * Apply global debouncing for the view's click. + * + * @param view The view. + * @param duration The duration of debouncing. + * @param listener The listener. + */ + public static void applyGlobalDebouncing(final View view, @IntRange(from = 0) long duration, + final View.OnClickListener listener) { + applyGlobalDebouncing(new View[]{view}, duration, listener); + } + + + /** + * Apply global debouncing for the views' click. + * + * @param views The views. + * @param listener The listener. + */ + public static void applyGlobalDebouncing(final View[] views, final View.OnClickListener listener) { + applyGlobalDebouncing(views, DEBOUNCING_DEFAULT_VALUE, listener); + } + + /** + * Apply global debouncing for the views' click. + * + * @param views The views. + * @param duration The duration of debouncing. + * @param listener The listener. + */ + public static void applyGlobalDebouncing(final View[] views, + @IntRange(from = 0) long duration, + final View.OnClickListener listener) { + applyDebouncing(views, true, duration, listener); + } + + private static void applyDebouncing(final View[] views, + final boolean isGlobal, + @IntRange(from = 0) long duration, + final View.OnClickListener listener) { + if (views == null || views.length == 0 || listener == null) return; + for (View view : views) { + if (view == null) continue; + view.setOnClickListener(new OnDebouncingClickListener(isGlobal, duration) { + @Override + public void onDebouncingClick(View v) { + listener.onClick(v); + } + }); + } + } + + private static int dp2px(final float dpValue) { + final float scale = Resources.getSystem().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public static abstract class OnDebouncingClickListener implements View.OnClickListener { + + private static boolean mEnabled = true; + + private static final Runnable ENABLE_AGAIN = new Runnable() { + @Override + public void run() { + mEnabled = true; + } + }; + + private static boolean isValid(@NonNull final View view, final long duration) { + long curTime = System.currentTimeMillis(); + Object tag = view.getTag(DEBOUNCING_TAG); + if (!(tag instanceof Long)) { + view.setTag(DEBOUNCING_TAG, curTime); + return true; + } + long preTime = (Long) tag; + if (curTime - preTime <= duration) return false; + view.setTag(DEBOUNCING_TAG, curTime); + return true; + } + + private long mDuration; + private boolean mIsGlobal; + + public OnDebouncingClickListener() { + this(true, DEBOUNCING_DEFAULT_VALUE); + } + + public OnDebouncingClickListener(final boolean isGlobal) { + this(isGlobal, DEBOUNCING_DEFAULT_VALUE); + } + + public OnDebouncingClickListener(final long duration) { + this(true, duration); + } + + public OnDebouncingClickListener(final boolean isGlobal, final long duration) { + mIsGlobal = isGlobal; + mDuration = duration; + } + + public abstract void onDebouncingClick(View v); + + @Override + public final void onClick(View v) { + if (mIsGlobal) { + if (mEnabled) { + mEnabled = false; + v.postDelayed(ENABLE_AGAIN, mDuration); + onDebouncingClick(v); + } + } else { + if (isValid(v, mDuration)) { + onDebouncingClick(v); + } + } + } + } + + public static abstract class OnMultiClickListener implements View.OnClickListener { + + private static final long INTERVAL_DEFAULT_VALUE = 666; + + private final int mTriggerClickCount; + private final long mClickInterval; + + private long mLastClickTime; + private int mClickCount; + + public OnMultiClickListener(int triggerClickCount) { + this(triggerClickCount, INTERVAL_DEFAULT_VALUE); + } + + public OnMultiClickListener(int triggerClickCount, long clickInterval) { + this.mTriggerClickCount = triggerClickCount; + this.mClickInterval = clickInterval; + } + + public abstract void onTriggerClick(View v); + + public abstract void onBeforeTriggerClick(View v, int count); + + @Override + public void onClick(View v) { + if (mTriggerClickCount <= 1) { + onTriggerClick(v); + return; + } + long curTime = System.currentTimeMillis(); + + if (curTime - mLastClickTime < mClickInterval) { + mClickCount++; + if (mClickCount == mTriggerClickCount) { + onTriggerClick(v); + } else if (mClickCount < mTriggerClickCount) { + onBeforeTriggerClick(v, mClickCount); + } else { + mClickCount = 1; + onBeforeTriggerClick(v, mClickCount); + } + } else { + mClickCount = 1; + onBeforeTriggerClick(v, mClickCount); + } + mLastClickTime = curTime; + } + } + + private static class OnUtilsTouchListener implements View.OnTouchListener { + + public static OnUtilsTouchListener getInstance() { + return LazyHolder.INSTANCE; + } + + private OnUtilsTouchListener() {/**/} + + @Override + public boolean onTouch(final View v, MotionEvent event) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + processScale(v, true); + processAlpha(v, true); + } else if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL) { + processScale(v, false); + processAlpha(v, false); + } + return false; + } + + private void processScale(final View view, boolean isDown) { + Object tag = view.getTag(PRESSED_VIEW_SCALE_TAG); + if (!(tag instanceof Float)) return; + float value = isDown ? 1 + (Float) tag : 1; + view.animate() + .scaleX(value) + .scaleY(value) + .setDuration(200) + .start(); + } + + private void processAlpha(final View view, boolean isDown) { + Object tag = view.getTag(isDown ? PRESSED_VIEW_ALPHA_TAG : PRESSED_VIEW_ALPHA_SRC_TAG); + if (!(tag instanceof Float)) return; + view.setAlpha((Float) tag); + } + + private static class LazyHolder { + private static final OnUtilsTouchListener INSTANCE = new OnUtilsTouchListener(); + } + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/CloseUtils.java b/app/src/main/java/com/example/baseframe/utils/CloseUtils.java new file mode 100644 index 0000000..1343b6c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/CloseUtils.java @@ -0,0 +1,51 @@ +package com.example.baseframe.utils; + +import com.blankj.ALog; + +import java.io.Closeable; + +/** + * detail: 关闭 (IO 流 ) 工具类 + * @author Ttt + */ +public final class CloseUtils { + + private CloseUtils() { + } + + // 日志 TAG + private static final String TAG = CloseUtils.class.getSimpleName(); + + /** + * 关闭 IO + * @param closeables Closeable[] + */ + public static void closeIO(final Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception e) { + ALog.eTag(TAG, e, "closeIO"); + } + } + } + } + + /** + * 安静关闭 IO + * @param closeables Closeable[] + */ + public static void closeIOQuietly(final Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception ignore) { + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/CommUtils.java b/app/src/main/java/com/example/baseframe/utils/CommUtils.java new file mode 100644 index 0000000..5f8c3da --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/CommUtils.java @@ -0,0 +1,557 @@ +package com.example.baseframe.utils; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.ArrayRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.core.content.FileProvider; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.api.App; +import com.example.baseframe.listener.Listener; + + +import org.json.JSONObject; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.List; + +import static com.example.baseframe.utils.DensityUtil.getScreenWidth; + +/** + * @Description: 公用的一些方法 + * @Author: yzh + * @CreateDate: 2019/10/23 14:37 + */ +public class CommUtils { + private static final String TAG="CommUtils"; + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + * @param dpValue dp 值 + * @return int 转换后的值 + */ + public static int dip2px(float dpValue) { + final float scale = App.getInstance().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + * @param pxValue dp 值 + * @return int 转换后的值 + */ + public static int px2dip( float pxValue) { + final float scale = App.getInstance().getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + + /** + * 直接获取控件的宽、高 + * @param view + * @return int[] + */ + public static int getViewHeight(final View view){ + ViewTreeObserver vto2 = view.getViewTreeObserver(); + vto2.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + return view.getHeight(); + } + + /** + * 直接获取控件的宽、高 + * @param view + * @return int[] + */ + public static int getViewWidth(final View view){ + ViewTreeObserver vto2 = view.getViewTreeObserver(); + vto2.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + return view.getWidth(); + } + + public static void isMainThread() { + Log.e(TAG, "---是否在主线程:" + (Thread.currentThread() == Looper.getMainLooper().getThread())); + } + + /** + * 判断list 是否为空 + * @param list + * @param + * @return + */ + public static boolean isListNotNull(List list){ + if(list!=null&&!list.isEmpty()){ + return true; + } + return false; + } + /** + * 判断list 是否为空 + * @param list + * @param + * @return + */ + public static boolean isListNull(List list){ + if(list!=null&&!list.isEmpty()){ + return false; + } + return true; + } + + public static boolean isEmpty(String s) { + return s == null || s.length() == 0 || s.equals("null"); + } + + public static boolean isNoEmpty(String s) { + boolean bool=s == null || s.length() == 0 || s.equals("null"); + return !bool; + } + + public static boolean isJson(String json){ + try{ + JSONObject jsonObject = new JSONObject(json); + }catch(Exception e){ + //Log.e(TAG,"该字符串不是json!\n"+json); + return false; + } + return true; + } + + + + /** + * 设置某个View的margin + * + * @param view 需要设置的view + * @param isDp 需要设置的数值是否为DP + * @param left 左边距 + * @param right 右边距 + * @param top 上边距 + * @param bottom 下边距 + * @return + */ + public static ViewGroup.LayoutParams setViewMargin(View view, boolean isDp, int left, int right, int top, int bottom) { + if (view == null) { + return null; + } + + int leftPx = left; + int rightPx = right; + int topPx = top; + int bottomPx = bottom; + ViewGroup.LayoutParams params = view.getLayoutParams(); + ViewGroup.MarginLayoutParams marginParams = null; + //获取view的margin设置参数 + if (params instanceof ViewGroup.MarginLayoutParams) { + marginParams = (ViewGroup.MarginLayoutParams) params; + } else { + //不存在时创建一个新的参数 + marginParams = new ViewGroup.MarginLayoutParams(params); + } + + //根据DP与PX转换计算值 + if (isDp) { + leftPx = dip2px(left); + rightPx = dip2px(right); + topPx = dip2px(top); + bottomPx = dip2px(bottom); + } + //设置margin + marginParams.setMargins(leftPx, topPx, rightPx, bottomPx); + view.setLayoutParams(marginParams); + view.requestLayout(); + return marginParams; + } + + /** + * 在代码中为TextView 设置color资源 + * + * @param tv + * @param color 例如 R.color.oranges + */ + public static void setTextColor(TextView tv, int color) { + Resources resource = (Resources) tv.getContext().getResources(); +// ColorStateList csl = (ColorStateList) resource.getColorStateList(color); +// if(csl!=null){ +// } + tv.setTextColor(resource.getColor(color)); + } + /**editText设置文字后 光标设置为文字末尾**/ + public static void setTextSelectEnd(@NonNull EditText editText, @NonNull String str){ + if (editText!=null) + { + if(str!=null){ + editText.setText(str); + editText.setSelection(editText.getText().length()); + } + + } + } + /** + * 为textview 设值,避免空值情况 + * + * @param tv + * @param str + */ + public static void setTextValues(TextView tv, String str) { + if (tv != null && !TextUtils.isEmpty(str)) { + tv.setText(str); + } + } + public static void setTextValues(TextView tv, @StringRes int id) { + String str = tv.getContext().getString(id); + if (tv != null && !TextUtils.isEmpty(str)) { + tv.setText(str); + } + } + + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("status_bar_height" + , "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + if (result == 0) { + final float scale = context.getResources().getDisplayMetrics().density; + result = (int) (26 * scale + 0.5f); + } + return result; + } + + + /** + * 优化确认取消弹窗方法 2016-3-1 yzh update + * + * @param msg 弹框信息 + * @param leftName 左边按钮名称 + * @param rightName 右边按钮名称 (传null表示只显示一个按钮) + * @param leftlistener 左边按钮监听 (无需监听事件可传null) + * @param rightlistener 右边按钮监听 (无需监听事件可传null) + * @param color 设置按钮文字颜色 #FFFFFF (可传null取默认) + * @return + */ + public static Dialog showDialog(Context context, String title, String msg, String leftName, String rightName + , final Listener leftlistener, final Listener rightlistener, String... color) { + + final Dialog showDialog = new Dialog(context); + showDialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + showDialog.getWindow().setBackgroundDrawable(new ColorDrawable(0)); + + showDialog.getWindow().setGravity(Gravity.CENTER); + showDialog.setContentView(R.layout.comm_show_dialogs); + showDialog.setCancelable(false); + if (!((Activity) context).isFinishing()) { + showDialog.show(); + } + + LinearLayout lyrame; + Button btSure, btCancel; + TextView tvContent; + + lyrame =showDialog.findViewById(R.id.lyShow_Frame); + btSure = showDialog.findViewById(R.id.btShow_sure); + btCancel =showDialog.findViewById(R.id.btShow_cancle); + tvContent = showDialog.findViewById(R.id.tvShow_content); + lyrame.getLayoutParams().width = getScreenWidth() - dip2px(100); + + tvContent.setText(msg); + + if(msg.length()>=20){ + tvContent.setGravity(Gravity.LEFT); + }else{ + tvContent.setGravity(Gravity.CENTER); + } + if (color != null && color.length > 0) { + btSure.setTextColor(Color.parseColor(color[0])); + if (color.length >1) { + btCancel.setTextColor(Color.parseColor(color[1])); + } + + } else { + //默认颜色 + setTextColor(btSure, R.color.ui_blue); + setTextColor(btCancel,R.color.ui_blue); + } + + if(StringUtil.isNoEmpty(title)){ + TextView tvTitle =showDialog.findViewById(R.id.tvTitle); + tvTitle.setText(title); + } + + if (StringUtil.isEmpty(leftName)) { + btSure.setVisibility(View.GONE); + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } else { + btSure.setText(leftName); + btSure.setOnClickListener(v -> { + showDialog.dismiss(); + if (leftlistener != null) + leftlistener.onResult(); + }); + } + + if (StringUtil.isEmpty(rightName)) { + btCancel.setVisibility(View.GONE); + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } else { + btCancel.setText(rightName); + btCancel.setOnClickListener(v -> { + showDialog.dismiss(); + if (rightlistener != null) + rightlistener.onResult(); + }); + } + + if (TextUtils.isEmpty(rightName) || TextUtils.isEmpty(leftName)) { + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } + return showDialog; + } + + /** + * 点击按钮后需要手动关闭的弹窗 + * + * @param msg 弹框信息 + * @param leftName 左边按钮名称 + * @param rightName 右边按钮名称 (传null表示只显示一个按钮) + * @param leftlistener 左边按钮监听 (无需监听事件可传null) + * @param rightlistener 右边按钮监听 (无需监听事件可传null) + * @param color 设置按钮文字颜色 #FFFFFF (可传null取默认) + * @return + */ + public static TextView showDialogs(Context context, String title, String msg, String leftName, String rightName + , final Listener leftlistener, final Listener rightlistener, String... color) { + + final Dialog showDialog = new Dialog(context); + showDialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + showDialog.getWindow().setBackgroundDrawable(new ColorDrawable(0)); + + showDialog.getWindow().setGravity(Gravity.CENTER); + showDialog.setContentView(R.layout.comm_show_dialogs); + showDialog.setCancelable(false); + if (!((Activity) context).isFinishing()) { + showDialog.show(); + } + + LinearLayout lyrame; + Button btSure, btCancel; + TextView tvContent; + + lyrame =showDialog.findViewById(R.id.lyShow_Frame); + btSure = showDialog.findViewById(R.id.btShow_sure); + btCancel =showDialog.findViewById(R.id.btShow_cancle); + tvContent = showDialog.findViewById(R.id.tvShow_content); + lyrame.getLayoutParams().width = getScreenWidth() - dip2px(100); + tvContent.setText(msg); + + if(msg.length()>=20){ + tvContent.setGravity(Gravity.LEFT); + }else{ + tvContent.setGravity(Gravity.CENTER); + } + if (color != null && color.length > 0) { + btSure.setTextColor(Color.parseColor(color[0])); + if (color.length >1) { + btCancel.setTextColor(Color.parseColor(color[1])); + } + + } else { + //默认颜色 + setTextColor(btSure, R.color.ui_blue); + setTextColor(btCancel,R.color.ui_blue); + } + + if(StringUtil.isNoEmpty(title)){ + TextView tvTitle =showDialog.findViewById(R.id.tvTitle); + tvTitle.setText(title); + } + + if (StringUtil.isEmpty(leftName)) { + btSure.setVisibility(View.GONE); + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } else { + btSure.setText(leftName); + btSure.setOnClickListener(v -> { + // showDialog.dismiss(); + if (leftlistener != null) + leftlistener.onResult(); + }); + } + + if (StringUtil.isEmpty(rightName)) { + btCancel.setVisibility(View.GONE); + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } else { + btCancel.setText(rightName); + btCancel.setOnClickListener(v -> { + //showDialog.dismiss(); + if (rightlistener != null) + rightlistener.onResult(); + }); + } + + if (TextUtils.isEmpty(rightName) || TextUtils.isEmpty(leftName)) { + showDialog.findViewById(R.id.spit).setVisibility(View.GONE); + } + return tvContent; + } + + /** + * 调往系统APK安装界面(适配7.0) + * + * @return + */ + public static Intent getInstallAppIntent(String filePath) { + //apk文件的本地路径 + File apkfile = new File(filePath); + if (!apkfile.exists()) { + return null; + } + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri contentUri = getUriForFile(apkfile); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); + return intent; + } + + /** + * 将文件转换成uri + * + * @return + */ + public static Uri getUriForFile(File file) { + Uri fileUri = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + fileUri = FileProvider.getUriForFile(App.getInstance(), App.getInstance().getPackageName() + ".fileprovider", file); + } else { + fileUri = Uri.fromFile(file); + } + return fileUri; + } + + + /** + * 根据资源名称获取 资源id + * @param name + * @param type 资源类型 drawable、 raw + * @return + */ + + public static int getResId(String name,String type){ + Resources r=App.getInstance().getResources(); + int id = 0; + try { + id = r.getIdentifier(name,type, App.getInstance().getPackageName()); + //踩坑提示 如果是在插件化环境里 context.getPackageName() 获取的包名会变成宿主的包名,建议写死包名或者反射获取 + ALog.v("BaseApplication.getInstance().getPackageName()=="+App.getInstance().getPackageName()); + } catch (Exception e) { + e.printStackTrace(); + } + return id; + } + + + + /** + * 根据资源名称获取 资源id (通过反射) + * @param imageName + * @param mipmap 资源类型 例如 R.mipmap.class + * @return + */ + public static int getResId(String imageName,Class mipmap){ + //Class mipmap = R.raw.class; + //Class mipmap = R.mipmap.class; + try { + Field field = mipmap.getField(imageName); + int resId = field.getInt(imageName); + return resId; + } catch (NoSuchFieldException | IllegalAccessException e) {//如果没有在"mipmap"下找到imageName,将会返回0 + return 0; + } + + } + + /** + * 从arrays.xml中读取数组 + * @param resId + * @return + */ + public static int[] getArrays(@ArrayRes int resId){ + TypedArray array = App.getInstance().getResources().obtainTypedArray(resId); + int len = array.length(); + int[] intArray = new int[array.length()]; + + for(int i = 0; i < len; i++){ + intArray[i] = array.getResourceId(i, 0); + } + array.recycle(); + return intArray; + } + + + /** + * 将秒 格式化显示(例:4000秒 = 01:06:40) + * @return + */ + // 将秒转化成小时分钟秒 + public static String showTime(long miss){ + String hh=miss/3600>9?miss/3600+"":"0"+miss/3600; + String mm=(miss % 3600)/60>9?(miss % 3600)/60+"":"0"+(miss % 3600)/60; + String ss=(miss % 3600) % 60>9?(miss % 3600) % 60+"":"0"+(miss % 3600) % 60; + if(!"00".equals(hh)){ + return hh+":"+mm+":"+ss; + }else{ + return mm+":"+ss; + } + } + + + /** + * 将秒 格式化显示(例:4000秒 = 1:06:40)1小时6分钟40秒 + * @return + */ + public String showTimes(long miss){ + String hh=miss/3600>9?miss/3600+"":"0"+miss/3600; + String mm=(miss % 3600)/60>9?(miss % 3600)/60+"":"0"+(miss % 3600)/60; + String ss=(miss % 3600) % 60>9?(miss % 3600) % 60+"":"0"+(miss % 3600) % 60; + return hh+"小时"+mm+"分钟"+ss+"秒"; + } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/ConvertUtils.java b/app/src/main/java/com/example/baseframe/utils/ConvertUtils.java new file mode 100644 index 0000000..1c15e85 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ConvertUtils.java @@ -0,0 +1,1839 @@ +package com.example.baseframe.utils; + +import android.text.TextUtils; + +import com.blankj.ALog; + + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; + + +/** + * detail: 转换工具类 (Byte、Hex 等 ) + * @author Ttt + *
+ *     byte 是字节数据类型、有符号型的、占 1 个字节、大小范围为 [ -128 - 127]
+ *     当大于 127 时则开始缩进 127 = 127, 128 = -128, 129 = -127
+ *     char 是字符数据类型、无符号型的、占 2 个字节 (unicode 码 )、大小范围为 [0 - 65535]
+ *     

+ * Binary( 二进制 ) toBinaryString + * Oct( 八进制 ) + * Dec( 十进制 ) + * Hex( 十六进制 ) 以 0x 开始的数据表示十六进制 + *

+ * 位移加密: bytesBitwiseAND(byte[] bytes) + * @see
+ *
+ */ +public final class ConvertUtils { + + private ConvertUtils() { + } + + // 日志 TAG + private static final String TAG = ConvertUtils.class.getSimpleName(); + // 用于建立十六进制字符的输出的小写字符数组 + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + // 用于建立十六进制字符的输出的大写字符数组 + private static final char[] HEX_DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Object 转换所需类型对象 + * @param object Object + * @param 泛型 + * @return Object convert T object + */ + public static T convert(final Object object) { + if (object == null) return null; + try { + return (T) object; + } catch (Exception e) { + ALog.eTag(TAG, e, "convert"); + } + return null; + } + + /** + * Object 转 String + * @param data byte[] + * @return {@link String} + * @deprecated {@link #newString} + */ + @Deprecated + public static String toString(final byte[] data) throws Exception { + return newString(data, null); + } + + /** + * Object 转 String + * @param data byte[] + * @param defaultStr 默认字符串 + * @return {@link String} 如果转换失败则返回 defaultStr + * @deprecated {@link #newString} + */ + @Deprecated + public static String toString(final byte[] data, final String defaultStr) throws Exception { + return newString(data, defaultStr); + } + + /** + * Object 转 String + * @param data char[] + * @return {@link String} + * @deprecated {@link #newString} + */ + @Deprecated + public static String toString(final char[] data) throws Exception { + return newString(data, null); + } + + /** + * Object 转 String + * @param data char[] + * @param defaultStr 默认字符串 + * @return {@link String} 如果转换失败则返回 defaultStr + * @deprecated {@link #newString} + */ + @Deprecated + public static String toString(final char[] data, final String defaultStr) throws Exception { + return newString(data, defaultStr); + } + + /** + * Object 转 String + * @param value Value + * @return {@link String} + */ + public static String newString(final Object value) { + return newString(value, null); + } + + /** + * Object 转 String + * @param value Value + * @param defaultStr 默认字符串 + * @return {@link String} 如果转换失败则返回 defaultStr + */ + public static String newString(final Object value, final String defaultStr) { + if (value != null) { + try { + if (value instanceof byte[]) { + return new String((byte[]) value); + } + if (value instanceof char[]) { + return new String((char[]) value); + } + if (value instanceof String) { + return (String) value; + } + if (value instanceof StringBuffer) { + return new String((StringBuffer) value); + } + if (value instanceof StringBuilder) { + return new String((StringBuilder) value); + } + throw new Exception("can not new string, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "newString"); + } + } + return defaultStr; + } + + /** + * Object 转 String + * @param object Object + * @return {@link String} + */ + public static String toString(final Object object) { + return toString(object, null); + } + + /** + * Object 转 String + * @param object Object + * @param defaultStr 默认字符串 + * @return {@link String} 如果转换失败则返回 defaultStr + */ + public static String toString(final Object object, final String defaultStr) { + if (object != null) { + try { + if (object instanceof String) { + return (String) object; + } + if (object instanceof Integer) { + return Integer.toString((Integer) object); + } + if (object instanceof Boolean) { + return Boolean.toString((Boolean) object); + } + if (object instanceof Long) { + return Long.toString((Long) object); + } + if (object instanceof Double) { + return Double.toString((Double) object); + } + if (object instanceof Float) { + return Float.toString((Float) object); + } + if (object instanceof Byte) { + return Byte.toString((Byte) object); + } + if (object instanceof Character) { + return Character.toString((Character) object); + } + if (object instanceof Short) { + return Short.toString((Short) object); + } + Class clazz = object.getClass(); + // 判断是否数组类型 + if (clazz.isArray()) { + // = 基本数据类型 = + if (clazz.isAssignableFrom(int[].class)) { + return Arrays.toString((int[]) object); + } else if (clazz.isAssignableFrom(boolean[].class)) { + return Arrays.toString((boolean[]) object); + } else if (clazz.isAssignableFrom(long[].class)) { + return Arrays.toString((long[]) object); + } else if (clazz.isAssignableFrom(double[].class)) { + return Arrays.toString((double[]) object); + } else if (clazz.isAssignableFrom(float[].class)) { + return Arrays.toString((float[]) object); + } else if (clazz.isAssignableFrom(byte[].class)) { + return Arrays.toString((byte[]) object); + } else if (clazz.isAssignableFrom(char[].class)) { + return Arrays.toString((char[]) object); + } else if (clazz.isAssignableFrom(short[].class)) { + return Arrays.toString((short[]) object); + } + return Arrays.toString((Object[]) object); + } + return object.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "toString"); + } + } + return defaultStr; + } + + /** + * Object 转 Integer + * @param value Value + * @return Integer + */ + public static Integer toInt(final Object value) { + return toInt(value, 0); + } + + /** + * Object 转 Integer + * @param value Value + * @param defaultValue 默认值 + * @return Integer, 如果转换失败则返回 defaultValue + */ + public static Integer toInt(final Object value, final Integer defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + String strVal = (String) value; + if (strVal.indexOf(',') != 0) { + strVal = strVal.replaceAll(",", ""); + } + return Integer.parseInt(strVal); + } + if (value instanceof Boolean) { + return ((Boolean) value).booleanValue() ? 1 : 0; + } + throw new Exception("can not cast to int, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toInt"); + } + return defaultValue; + } + + /** + * Object 转 Boolean + * @param value Value + * @return Boolean + */ + public static Boolean toBoolean(final Object value) { + return toBoolean(value, false); + } + + /** + * Object 转 Boolean + * @param value Value + * @param defaultValue 默认值 + * @return Boolean, 如果转换失败则返回 defaultValue + */ + public static Boolean toBoolean(final Object value, final Boolean defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof Number) { + return ((Number) value).intValue() == 1; + } + if (value instanceof String) { + String strVal = (String) value; + if ("true".equalsIgnoreCase(strVal) || "1".equals(strVal)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(strVal) || "0".equals(strVal)) { + return Boolean.FALSE; + } + // YES、TRUE + if ("Y".equalsIgnoreCase(strVal) || "T".equalsIgnoreCase(strVal)) { + return Boolean.TRUE; + } + // NO、FALSE + if ("N".equalsIgnoreCase(strVal) || "F".equalsIgnoreCase(strVal)) { + return Boolean.FALSE; + } + } + throw new Exception("can not cast to boolean, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toBoolean"); + } + return defaultValue; + } + + /** + * Object 转 Float + * @param value Value + * @return Float + */ + public static Float toFloat(final Object value) { + return toFloat(value, 0f); + } + + /** + * Object 转 Float + * @param value Value + * @param defaultValue 默认值 + * @return Float, 如果转换失败则返回 defaultValue + */ + public static Float toFloat(final Object value, final Float defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Float) { + return (Float) value; + } + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + if (value instanceof String) { + String strVal = (String) value; + if (strVal.indexOf(',') != 0) { + strVal = strVal.replaceAll(",", ""); + } + return Float.parseFloat(strVal); + } + throw new Exception("can not cast to float, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toFloat"); + } + return defaultValue; + } + + /** + * Object 转 Double + * @param value Value + * @return Double + */ + public static Double toDouble(final Object value) { + return toDouble(value, 0d); + } + + /** + * Object 转 Double + * @param value Value + * @param defaultValue 默认值 + * @return Double, 如果转换失败则返回 defaultValue + */ + public static Double toDouble(final Object value, final Double defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + if (value instanceof String) { + String strVal = (String) value; + if (strVal.indexOf(',') != 0) { + strVal = strVal.replaceAll(",", ""); + } + return Double.parseDouble(strVal); + } + throw new Exception("can not cast to double, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toDouble"); + } + return defaultValue; + } + + /** + * Object 转 Long + * @param value Value + * @return Long + */ + public static Long toLong(final Object value) { + return toLong(value, 0L); + } + + /** + * Object 转 Long + * @param value Value + * @param defaultValue 默认值 + * @return Long, 如果转换失败则返回 defaultValue + */ + public static Long toLong(final Object value, final Long defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + String strVal = (String) value; + if (strVal.indexOf(',') != 0) { + strVal = strVal.replaceAll(",", ""); + } + return Long.parseLong(strVal); + } + throw new Exception("can not cast to long, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toLong"); + } + return defaultValue; + } + + /** + * Object 转 Short + * @param value Value + * @return Short + */ + public static Short toShort(final Object value) { + return toShort(value, (short) 0); + } + + /** + * Object 转 Short + * @param value Value + * @param defaultValue 默认值 + * @return Short, 如果转换失败则返回 defaultValue + */ + public static Short toShort(final Object value, final Short defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Short) { + return (Short) value; + } + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + if (value instanceof String) { + String strVal = (String) value; + return Short.parseShort(strVal); + } + throw new Exception("can not cast to short, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toShort"); + } + return defaultValue; + } + + /** + * Object 转 Character + * @param value Value + * @return Character + */ + public static Character toChar(final Object value) { + return toChar(value, (char) 0); + } + + /** + * Object 转 Character + * @param value Value + * @param defaultValue 默认值 + * @return Character, 如果转换失败则返回 defaultValue + */ + public static Character toChar(final Object value, final Character defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Character) { + return (Character) value; + } + if (value instanceof String) { + String strVal = (String) value; + if (strVal.length() == 1) { + return strVal.charAt(0); + } + } + throw new Exception("can not cast to char, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toChar"); + } + return defaultValue; + } + + /** + * Object 转 Byte + * @param value Value + * @return Byte + */ + public static byte toByte(final Object value) { + return toByte(value, (byte) 0); + } + + /** + * Object 转 Byte + * @param value Value + * @param defaultValue 默认值 + * @return Byte, 如果转换失败则返回 defaultValue + */ + public static byte toByte(final Object value, final Byte defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof Byte) { + return (Byte) value; + } + if (value instanceof Number) { + return ((Number) value).byteValue(); + } + if (value instanceof String) { + String strVal = (String) value; + return Byte.parseByte(strVal); + } + throw new Exception("can not cast to byte, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toByte"); + } + return defaultValue; + } + + /** + * Object 转 BigDecimal + * @param value Value + * @return BigDecimal + */ + public static BigDecimal toBigDecimal(final Object value) { + return toBigDecimal(value, new BigDecimal(0)); + } + + /** + * Object 转 BigDecimal + * @param value Value + * @param defaultValue 默认值 + * @return BigDecimal, 如果转换失败则返回 defaultValue + */ + public static BigDecimal toBigDecimal(final Object value, final BigDecimal defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } + if (value instanceof String) { + String strVal = (String) value; + return new BigDecimal(strVal); + } + throw new Exception("can not cast to BigDecimal, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toBigDecimal"); + } + return defaultValue; + } + + /** + * Object 转 BigInteger + * @param value Value + * @return BigInteger + */ + public static BigInteger toBigInteger(final Object value) { + return toBigInteger(value, BigInteger.valueOf(0L)); + } + + /** + * Object 转 BigInteger + * @param value Value + * @param defaultValue 默认值 + * @return BigInteger, 如果转换失败则返回 defaultValue + */ + public static BigInteger toBigInteger(final Object value, final BigInteger defaultValue) { + if (value == null) return defaultValue; + try { + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (value instanceof Float || value instanceof Double) { + return BigInteger.valueOf(((Number) value).longValue()); + } + if (value instanceof String) { + String strVal = (String) value; + return new BigInteger(strVal); + } + throw new Exception("can not cast to BigInteger, value : " + value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toBigInteger"); + } + return defaultValue; + } + + /** + * Object 获取 char[] + * @param value Value + * @return char[] + */ + public static char[] toChars(final Object value) { + try { + if (value instanceof char[]) { + return (char[]) value; + } + if (value instanceof String) { + return ((String) value).toCharArray(); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "toChars"); + } + return null; + } + + /** + * Object 获取 byte[] + * @param value Value + * @return byte[] + */ + public static byte[] toBytes(final Object value) { + try { + if (value instanceof byte[]) { + return (byte[]) value; + } + if (value instanceof String) { + return ((String) value).getBytes(); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "toBytes"); + } + return null; + } + + /** + * char 转换 unicode 编码 + * @param value char + * @return int + */ + public static int toCharInt(final char value) { + return (int) value; + } + + /** + * Object 获取 char ( 默认第一位 ) + * @param value Value + * @param defaultValue 默认值 + * @return 第一位值, 如果获取失败则返回 defaultValue + */ + public static char charAt(final Object value, final char defaultValue) { + return charAt(value, 0, defaultValue); + } + + /** + * Object 获取 char + * @param value Value + * @param pos 索引 + * @param defaultValue 默认值 + * @return 指定索引的值, 如果获取失败则返回 defaultValue + */ + public static char charAt(final Object value, final int pos, final char defaultValue) { + if (value == null || pos < 0) return defaultValue; + try { + return toChars(value)[pos]; + } catch (Exception e) { + ALog.eTag(TAG, e, "charAt"); + } + return defaultValue; + } + + // = + + /** + * 字符串转换对应的进制 + *
+     *     如: parseInt("1f603", 16) = 128515
+     * 
+ * @param str 待处理字符串 + * @param radix 进制 + * @return 对应进制的值 + */ + public static int parseInt(final String str, final int radix) { + if (str == null) return -1; + try { + return Integer.parseInt(str, radix); + } catch (Exception e) { + ALog.eTag(TAG, e, "parseInt"); + } + return -1; + } + + /** + * 字符串转换对应的进制 + * @param str 待处理字符串 + * @param radix 进制 + * @return 对应进制的值 + */ + public static long parseLong(final String str, final int radix) { + if (str == null) return -1; + try { + return Long.parseLong(str, radix); + } catch (Exception e) { + ALog.eTag(TAG, e, "parseLong"); + } + return -1; + } + + // = + + /** + * 将 short 转换成字节数组 + * @param data short + * @return byte[] + */ + public static byte[] valueOf(final short data) { + try { + byte[] bytes = new byte[2]; + for (int i = 0; i < 2; i++) { + int offset = (bytes.length - 1 - i) * 8; + bytes[i] = (byte) ((data >>> offset) & 0xff); + } + return bytes; + } catch (Exception e) { + ALog.eTag(TAG, e, "valueOf"); + } + return null; + } + + /** + * 将 int 转换成字节数组 + * @param data int + * @return byte[] + */ + public static byte[] valueOf(final int data) { + try { + byte[] bytes = new byte[4]; + for (int i = 0; i < 4; i++) { + int offset = (bytes.length - 1 - i) * 8; + bytes[i] = (byte) ((data >>> offset) & 0xFF); + } + return bytes; + } catch (Exception e) { + ALog.eTag(TAG, e, "valueOf"); + } + return null; + } + + // = + + /** + * byte[] 转为 Object + * @param bytes byte[] + * @return {@link Object} + */ + public static Object bytesToObject(final byte[] bytes) { + if (bytes == null) return null; + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + return ois.readObject(); + } catch (Exception e) { + ALog.eTag(TAG, e, "bytesToObject"); + } finally { + if (ois != null) { + try { + ois.close(); + } catch (Exception e) { + } + } + } + return null; + } + + /** + * Object 转为 byte[] + * @param object Object + * @return byte[] + */ + public static byte[] objectToBytes(final Object object) { + if (object == null) return null; + ObjectOutputStream oos = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(object); + return baos.toByteArray(); + } catch (Exception e) { + ALog.eTag(TAG, e, "objectToBytes"); + } finally { + if (oos != null) { + try { + oos.close(); + } catch (Exception e) { + } + } + } + return null; + } + + // = + + /** + * byte[] 转换 char[], 并且进行补码 + * @param data byte[] + * @return char[] + */ + public static char[] bytesToChars(final byte[] data) { + if (data == null) return null; + int len = data.length; + if (len == 0) return null; + try { + char[] chars = new char[len]; + for (int i = 0; i < len; i++) { + chars[i] = (char) (data[i] & 0xff); + } + return chars; + } catch (Exception e) { + ALog.eTag(TAG, e, "bytesToChars"); + } + return null; + } + + /** + * char[] 转换 byte[] + * @param data char[] + * @return byte[] + */ + public static byte[] charsToBytes(final char[] data) { + if (data == null) return null; + int len = data.length; + if (len == 0) return null; + try { + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + bytes[i] = (byte) (data[i]); + } + return bytes; + } catch (Exception e) { + ALog.eTag(TAG, e, "charsToBytes"); + } + return null; + } + + // ============================================== + // = (int、double、long、float)[] 转换 String[] = + // ============================================== + + /** + * int[] 转换 string[] + * @param datas int[] + * @return String[] + */ + public static String[] intsToStrings(final int[] datas) { + return intsToStrings(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 string[] + * @param off 起始值 + * @param datas int[] + * @return String[] + */ + public static String[] intsToStrings(final int off, final int[] datas) { + return intsToStrings(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 string[] + * @param off 起始值 + * @param length 所需长度 + * @param datas int[] + * @return String[] + */ + public static String[] intsToStrings(final int off, final int length, final int[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + String[] strings = new String[length - off]; + for (int i = 0, len = strings.length; i < len; i++) { + strings[i] = datas[off + i] + ""; + } + return strings; + } + + // = + + /** + * double[] 转换 string[] + * @param datas double[] + * @return String[] + */ + public static String[] doublesToStrings(final double[] datas) { + return doublesToStrings(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * double[] 转换 string[] + * @param off 起始值 + * @param datas double[] + * @return String[] + */ + public static String[] doublesToStrings(final int off, final double[] datas) { + return doublesToStrings(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * double[] 转换 string[] + * @param off 起始值 + * @param length 所需长度 + * @param datas double[] + * @return String[] + */ + public static String[] doublesToStrings(final int off, final int length, final double[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + String[] strings = new String[length - off]; + for (int i = 0, len = strings.length; i < len; i++) { + strings[i] = datas[off + i] + ""; + } + return strings; + } + + // = + + /** + * long[] 转换 string[] + * @param datas long[] + * @return String[] + */ + public static String[] longsToStrings(final long[] datas) { + return longsToStrings(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * long[] 转换 string[] + * @param off 起始值 + * @param datas long[] + * @return String[] + */ + public static String[] longsToStrings(final int off, final long[] datas) { + return longsToStrings(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * long[] 转换 string[] + * @param off 起始值 + * @param length 所需长度 + * @param datas long[] + * @return String[] + */ + public static String[] longsToStrings(final int off, final int length, final long[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + String[] strings = new String[length - off]; + for (int i = 0, len = strings.length; i < len; i++) { + strings[i] = datas[off + i] + ""; + } + return strings; + } + + // = + + /** + * float[] 转换 string[] + * @param datas float[] + * @return String[] + */ + public static String[] floatsToStrings(final float[] datas) { + return floatsToStrings(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * float[] 转换 string[] + * @param off 起始值 + * @param datas float[] + * @return String[] + */ + public static String[] floatsToStrings(final int off, final float[] datas) { + return floatsToStrings(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * float[] 转换 string[] + * @param off 起始值 + * @param length 所需长度 + * @param datas float[] + * @return String[] + */ + public static String[] floatsToStrings(final int off, final int length, final float[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + String[] strings = new String[length - off]; + for (int i = 0, len = strings.length; i < len; i++) { + strings[i] = datas[off + i] + ""; + } + return strings; + } + + // ====================================== + // = int[] 转换 (double、long、float)[] = + // ====================================== + + /** + * int[] 转换 double[] + * @param datas int[] + * @return double[] + */ + public static double[] intsToDoubles(final int[] datas) { + return intsToDoubles(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 double[] + * @param off 起始值 + * @param datas int[] + * @return double[] + */ + public static double[] intsToDoubles(final int off, final int[] datas) { + return intsToDoubles(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 double[] + * @param off 起始值 + * @param length 所需长度 + * @param datas int[] + * @return double[] + */ + public static double[] intsToDoubles(final int off, final int length, final int[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + double[] doubles = new double[length - off]; + for (int i = 0, len = doubles.length; i < len; i++) { + doubles[i] = datas[off + i]; + } + return doubles; + } + + // = + + /** + * int[] 转换 long[] + * @param datas int[] + * @return long[] + */ + public static long[] intsToLongs(final int[] datas) { + return intsToLongs(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 long[] + * @param off 起始值 + * @param datas int[] + * @return long[] + */ + public static long[] intsToLongs(final int off, final int[] datas) { + return intsToLongs(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 long[] + * @param off 起始值 + * @param length 所需长度 + * @param datas int[] + * @return long[] + */ + public static long[] intsToLongs(final int off, final int length, final int[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + long[] longs = new long[length - off]; + for (int i = 0, len = longs.length; i < len; i++) { + longs[i] = datas[off + i]; + } + return longs; + } + + // = + + /** + * int[] 转换 float[] + * @param datas int[] + * @return float[] + */ + public static float[] intsToFloats(final int[] datas) { + return intsToFloats(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 float[] + * @param off 起始值 + * @param datas int[] + * @return float[] + */ + public static float[] intsToFloats(final int off, final int[] datas) { + return intsToFloats(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * int[] 转换 float[] + * @param off 起始值 + * @param length 所需长度 + * @param datas int[] + * @return float[] + */ + public static float[] intsToFloats(final int off, final int length, final int[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + float[] floats = new float[length - off]; + for (int i = 0, len = floats.length; i < len; i++) { + floats[i] = datas[off + i]; + } + return floats; + } + + // ============================================== + // = String[] 转换 (int、double、long、float)[] = + // ============================================== + + /** + * string[] 转换 int[] + * @param datas String[] + * @return int[] + */ + public static int[] stringsToInts(final String... datas) { + return stringsToInts(0, (datas != null) ? datas.length : 0, -1, datas); + } + + /** + * string[] 转换 int[] + * @param off 起始值 + * @param datas String[] + * @return int[] + */ + public static int[] stringsToInts(final int off, final String... datas) { + return stringsToInts(off, (datas != null) ? datas.length : 0, -1, datas); + } + + /** + * string[] 转换 int[] + * @param off 起始值 + * @param length 所需长度 + * @param datas String[] + * @return int[] + */ + public static int[] stringsToInts(final int off, final int length, final String... datas) { + return stringsToInts(off, length, -1, datas); + } + + /** + * string[] 转换 int[] + * @param off 起始值 + * @param length 所需长度 + * @param errorValue 转换错误使用值 + * @param datas String[] + * @return int[] + */ + public static int[] stringsToInts(final int off, final int length, final int errorValue, final String... datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + int[] ints = new int[length - off]; + for (int i = 0, len = ints.length; i < len; i++) { + try { + ints[i] = Integer.parseInt(datas[off + i]); + } catch (Exception e) { + ints[i] = errorValue; + } + } + return ints; + } + + // = + + /** + * string[] 转换 double[] + * @param datas String[] + * @return double[] + */ + public static double[] stringsToDoubles(final String... datas) { + return stringsToDoubles(0, (datas != null) ? datas.length : 0, -1d, datas); + } + + /** + * string[] 转换 double[] + * @param off 起始值 + * @param datas String[] + * @return double[] + */ + public static double[] stringsToDoubles(final int off, final String... datas) { + return stringsToDoubles(off, (datas != null) ? datas.length : 0, -1d, datas); + } + + /** + * string[] 转换 double[] + * @param off 起始值 + * @param length 所需长度 + * @param datas String[] + * @return double[] + */ + public static double[] stringsToDoubles(final int off, final int length, final String... datas) { + return stringsToDoubles(off, length, -1d, datas); + } + + /** + * string[] 转换 double[] + * @param off 起始值 + * @param length 所需长度 + * @param errorValue 转换错误使用值 + * @param datas String[] + * @return double[] + */ + public static double[] stringsToDoubles(final int off, final int length, final double errorValue, final String... datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + double[] doubles = new double[length - off]; + for (int i = 0, len = doubles.length; i < len; i++) { + try { + doubles[i] = Double.parseDouble(datas[off + i]); + } catch (Exception e) { + doubles[i] = errorValue; + } + } + return doubles; + } + + // = + + /** + * string[] 转换 long[] + * @param datas String[] + * @return long[] + */ + public static long[] stringsToLongs(final String... datas) { + return stringsToLongs(0, (datas != null) ? datas.length : 0, -1L, datas); + } + + /** + * string[] 转换 long[] + * @param off 起始值 + * @param datas String[] + * @return long[] + */ + public static long[] stringsToLongs(final int off, final String... datas) { + return stringsToLongs(off, (datas != null) ? datas.length : 0, -1L, datas); + } + + /** + * string[] 转换 long[] + * @param off 起始值 + * @param length 所需长度 + * @param datas String[] + * @return long[] + */ + public static long[] stringsToLongs(final int off, final int length, final String... datas) { + return stringsToLongs(off, length, -1L, datas); + } + + /** + * string[] 转换 long[] + * @param off 起始值 + * @param length 所需长度 + * @param errorValue 转换错误使用值 + * @param datas String[] + * @return long[] + */ + public static long[] stringsToLongs(final int off, final int length, final long errorValue, final String... datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + long[] longs = new long[length - off]; + for (int i = 0, len = longs.length; i < len; i++) { + try { + longs[i] = Long.parseLong(datas[off + i]); + } catch (Exception e) { + longs[i] = errorValue; + } + } + return longs; + } + + // = + + /** + * string[] 转换 float[] + * @param datas String[] + * @return float[] + */ + public static float[] stringsToFloats(final String... datas) { + return stringsToFloats(0, (datas != null) ? datas.length : 0, -1f, datas); + } + + /** + * string[] 转换 float[] + * @param off 起始值 + * @param datas String[] + * @return float[] + */ + public static float[] stringsToFloats(final int off, final String... datas) { + return stringsToFloats(off, (datas != null) ? datas.length : 0, -1f, datas); + } + + /** + * string[] 转换 float[] + * @param off 起始值 + * @param length 所需长度 + * @param datas String[] + * @return float[] + */ + public static float[] stringsToFloats(final int off, final int length, final String... datas) { + return stringsToFloats(off, length, -1f, datas); + } + + /** + * string[] 转换 float[] + * @param off 起始值 + * @param length 所需长度 + * @param errorValue 转换错误使用值 + * @param datas String[] + * @return float[] + */ + public static float[] stringsToFloats(final int off, final int length, final float errorValue, final String... datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + float[] floats = new float[length - off]; + for (int i = 0, len = floats.length; i < len; i++) { + try { + floats[i] = Float.parseFloat(datas[off + i]); + } catch (Exception e) { + floats[i] = errorValue; + } + } + return floats; + } + + // ====================================== + // = (double、long、float)[] 转换 int[] = + // ====================================== + + /** + * double[] 转换 int[] + * @param datas double[] + * @return int[] + */ + public static int[] doublesToInts(final double[] datas) { + return doublesToInts(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * double[] 转换 int[] + * @param off 起始值 + * @param datas double[] + * @return int[] + */ + public static int[] doublesToInts(final int off, final double[] datas) { + return doublesToInts(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * double[] 转换 int[] + * @param off 起始值 + * @param length 所需长度 + * @param datas double[] + * @return int[] + */ + public static int[] doublesToInts(final int off, final int length, final double[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + int[] ints = new int[length - off]; + for (int i = 0, len = ints.length; i < len; i++) { + try { + ints[i] = (int) datas[off + i]; + } catch (Exception e) { + } + } + return ints; + } + + // = + + /** + * long[] 转换 int[] + * @param datas long[] + * @return int[] + */ + public static int[] longsToInts(final long[] datas) { + return longsToInts(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * long[] 转换 int[] + * @param off 起始值 + * @param datas long[] + * @return int[] + */ + public static int[] longsToInts(final int off, final long[] datas) { + return longsToInts(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * long[] 转换 int[] + * @param off 起始值 + * @param length 所需长度 + * @param datas long[] + * @return int[] + */ + public static int[] longsToInts(final int off, final int length, final long[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + int[] ints = new int[length - off]; + for (int i = 0, len = ints.length; i < len; i++) { + try { + ints[i] = (int) datas[off + i]; + } catch (Exception e) { + } + } + return ints; + } + + // = + + /** + * float[] 转换 int[] + * @param datas float[] + * @return int[] + */ + public static int[] floatsToInts(final float[] datas) { + return floatsToInts(0, (datas != null) ? datas.length : 0, datas); + } + + /** + * float[] 转换 int[] + * @param off 起始值 + * @param datas float[] + * @return int[] + */ + public static int[] floatsToInts(final int off, final float[] datas) { + return floatsToInts(off, (datas != null) ? datas.length : 0, datas); + } + + /** + * float[] 转换 int[] + * @param off 起始值 + * @param length 所需长度 + * @param datas float[] + * @return int[] + */ + public static int[] floatsToInts(final int off, final int length, final float[] datas) { + if (off < 0 || length < 1 || off >= length || datas == null || length > datas.length) + return null; + int[] ints = new int[length - off]; + for (int i = 0, len = ints.length; i < len; i++) { + try { + ints[i] = (int) datas[off + i]; + } catch (Exception e) { + } + } + return ints; + } + + // =================== + // = Binary - 二进制 = + // =================== + + /** + * 将 字节转换 为 二进制字符串 + * @param datas byte[] + * @return 二进制字符串 + */ + public static String toBinaryString(final byte... datas) { + if (datas == null || datas.length == 0) return null; + try { + StringBuilder builder = new StringBuilder(); + for (byte value : datas) { + for (int j = 7; j >= 0; --j) { + builder.append(((value >> j) & 0x01) == 0 ? '0' : '1'); + } + } + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "toBinaryString"); + } + return null; + } + + /** + * 二进制字符串 转换 byte[] 解码 + *
+     *     例: "011000010111001101100100" 传入 decodeBinary, 返回 byte[], 通过 new String(byte()) 获取配合 toBinaryString 使用
+     * 
+ * @param str 待处理字符串 + * @return 解码后的 byte[] + */ + public static byte[] decodeBinary(final String str) { + if (str == null) return null; + try { + String data = str; + int lenMod = data.length() % 8; + int byteLen = data.length() / 8; + // add "0" until length to 8 times + if (lenMod != 0) { + for (int i = lenMod; i < 8; i++) { + data = "0" + data; + } + byteLen++; + } + byte[] bytes = new byte[byteLen]; + for (int i = 0; i < byteLen; ++i) { + for (int j = 0; j < 8; ++j) { + bytes[i] <<= 1; + bytes[i] |= data.charAt(i * 8 + j) - '0'; + } + } + return bytes; + } catch (Exception e) { + ALog.eTag(TAG, e, "decodeBinary"); + } + return null; + } + + // ====================== + // = Hex - 十六进制处理 = + // ====================== + + /** + * 判断是否十六进制数据 + * @param data 待检验数据 + * @return {@code true} yes, {@code false} no + */ + public static boolean isHex(final String data) { + if (data == null) return false; + // 获取数据长度 + int len = data.length(); + if (len > 0) { + for (int i = len - 1; i >= 0; i--) { + char c = data.charAt(i); + if (!(c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f')) { + return false; + } + } + return true; + } + return false; + } + + /** + * 将十六进制字节数组解码 + * @param data 十六进制 byte[] + * @return 十六进制转 ( 解 ) 码后的数据 + */ + public static byte[] decodeHex(final byte[] data) { + return decodeHex((data.length == 0) ? null : bytesToChars(data)); + } + + /** + * 将十六进制字符串解码 + * @param str 十六进制字符串 + * @return 十六进制转 ( 解 ) 码后的数据 + */ + public static byte[] decodeHex(final String str) { + return decodeHex(TextUtils.isEmpty(str) ? null : str.toCharArray()); + } + + /** + * 将十六进制字符数组解码 + * @param data 十六进制 char[] + * @return 十六进制转 ( 解 ) 码后的数据 + */ + public static byte[] decodeHex(final char[] data) { + if (data == null) return null; + try { + int len = data.length; + byte[] out = new byte[len >> 1]; + // 十六进制由两个字符组成 + for (int i = 0, j = 0; j < len; i++) { + int d = toDigit(data[j], j) << 4; + j++; + d = d | toDigit(data[j], j); + j++; + out[i] = (byte) (d & 0xFF); + } + return out; + } catch (Exception e) { + ALog.eTag(TAG, e, "decodeHex"); + } + return null; + } + + /** + * 十六进制 char 转换 int + * @param hexChar 十六进制 char + * @return 十六进制转 ( 解 ) 码后的整数 + */ + public static int hexToInt(final char hexChar) { + if (hexChar >= '0' && hexChar <= '9') { + return hexChar - '0'; + } else if (hexChar >= 'A' && hexChar <= 'F') { + return hexChar - 'A' + 10; + } else { + throw new IllegalArgumentException(); + } + } + + /** + * 将十六进制字符转换成一个整数 + * @param ch 十六进制 char + * @param index 十六进制字符在字符数组中的位置 + * @return 一个整数 + * @throws Exception 当 ch 不是一个合法的十六进制字符时, 抛出运行时异常 + */ + private static int toDigit(final char ch, final int index) throws Exception { + int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new Exception(String.format("Illegal hexadecimal character %s at index %s", ch, index)); + } + return digit; + } + + // = + + // toHexString(0x1f603) = 1f603 + // parseInt("1f603", 16) = 128515 + // toHexString(128515) = 1f603 + + /** + * int 转换十六进制 + *
+     *     如: toHexString(0x1f603) 返回: 1f603
+     * 
+ * @param value int + * @return 十六进制字符串 + */ + public static String toHexString(final int value) { + try { + return Integer.toHexString(value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexString"); + } + return null; + } + + /** + * long 转换十六进制 + * @param value long + * @return 十六进制字符串 + */ + public static String toHexString(final long value) { + try { + return Long.toHexString(value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexString"); + } + return null; + } + + /** + * double 转换十六进制 + * @param value double + * @return 十六进制字符串 + */ + public static String toHexString(final double value) { + try { + return Double.toHexString(value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexString"); + } + return null; + } + + /** + * float 转换十六进制 + * @param value float + * @return 十六进制字符串 + */ + public static String toHexString(final float value) { + try { + return Float.toHexString(value); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexString"); + } + return null; + } + + // = + + /** + * 将 string 转换为 十六进制 char[] + * @param str 待处理字符串 + * @return 十六进制 char[] + */ + public static char[] toHexChars(final String str) { + return toHexChars(str, true); + } + + /** + * 将 string 转换为 十六进制 char[] + * @param str 待处理字符串 + * @param toLowerCase {@code true} 小写格式, {@code false} 大写格式 + * @return 十六进制 char[] + */ + public static char[] toHexChars(final String str, final boolean toLowerCase) { + return toHexChars(TextUtils.isEmpty(str) ? null : str.getBytes(), toLowerCase ? HEX_DIGITS : HEX_DIGITS_UPPER); + } + + // = + + /** + * 将 byte[] 转换为 十六进制 char[] + * @param data byte[] + * @return 十六进制 char[] + */ + public static char[] toHexChars(final byte[] data) { + return toHexChars(data, true); + } + + /** + * 将 byte[] 转换为 十六进制 char[] + * @param data byte[] + * @param toLowerCase {@code true} 小写格式, {@code false} 大写格式 + * @return 十六进制 char[] + */ + public static char[] toHexChars(final byte[] data, final boolean toLowerCase) { + return toHexChars(data, toLowerCase ? HEX_DIGITS : HEX_DIGITS_UPPER); + } + + /** + * 将 byte[] 转换为 十六进制 char[] + * @param data byte[] + * @param hexDigits {@link #HEX_DIGITS}、{@link #HEX_DIGITS_UPPER} + * @return 十六进制 char[] + */ + private static char[] toHexChars(final byte[] data, final char[] hexDigits) { + if (data == null || hexDigits == null) return null; + try { + return toHexString(data, hexDigits).toCharArray(); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexChars"); + } + return null; + } + + // = + + /** + * 将 string 转换 十六进制字符串 + * @param str 待转换数据 + * @return 十六进制字符串 + */ + public static String toHexString(final String str) { + return toHexString(str, true); + } + + /** + * 将 string 转换 十六进制字符串 + * @param str 待转换数据 + * @param toLowerCase {@code true} 小写格式, {@code false} 大写格式 + * @return 十六进制字符串 + */ + public static String toHexString(final String str, final boolean toLowerCase) { + return toHexString(TextUtils.isEmpty(str) ? null : str.getBytes(), toLowerCase ? HEX_DIGITS : HEX_DIGITS_UPPER); + } + + // = + + /** + * 将 byte[] 转换 十六进制字符串 + * @param data 待转换数据 + * @return 十六进制字符串 + */ + public static String toHexString(final byte[] data) { + return toHexString(data, true); + } + + /** + * 将 byte[] 转换 十六进制字符串 + * @param data 待转换数据 + * @param toLowerCase {@code true} 小写格式, {@code false} 大写格式 + * @return 十六进制字符串 + */ + public static String toHexString(final byte[] data, final boolean toLowerCase) { + return toHexString(data, toLowerCase ? HEX_DIGITS : HEX_DIGITS_UPPER); + } + + /** + * 将 byte[] 转换 十六进制字符串 + * @param data 待转换数据 + * @param hexDigits {@link #HEX_DIGITS}、{@link #HEX_DIGITS_UPPER} + * @return 十六进制字符串 + */ + private static String toHexString(final byte[] data, final char[] hexDigits) { + if (data == null || hexDigits == null) return null; + try { + int len = data.length; + StringBuilder builder = new StringBuilder(len); + for (int i = 0; i < len; i++) { + builder.append(hexDigits[(data[i] & 0xf0) >>> 4]); + builder.append(hexDigits[data[i] & 0x0f]); + } + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "toHexString"); + } + return null; + } + + // = + +// String data = "test"; +// // 转换二进制字符串 +// String result = toBinaryString(data.getBytes()); +// // 获取二进制数据 +// byte[] bytes = result.getBytes(); +// // 位移编码 +// bytesBitwiseAND(bytes); +// // = +// // 位移解码 +// bytesBitwiseAND(bytes); +// // 二进制数据解码 +// byte[] byteResult = decodeBinary(new String(bytes)); +// // 转换为原始数据 +// String data1 = new String(byteResult); +// // 判断是否一致 +// boolean equals = data.equals(data1); + + /** + * 按位求补 byte[] 位移编解码 ( 共用同一个方法 ) + * @param data byte[] + */ + public static void bytesBitwiseAND(final byte[] data) { + if (data == null) return; + for (int i = 0, len = data.length; i < len; i++) { + int d = data[i]; + d = ~d; // 按位补运算符, 翻转操作数的每一位, 即 0 变成 1, 1 变成 0, 再通过反转后的二进制初始化回十六进制 + data[i] = (byte) d; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/DensityUtil.java b/app/src/main/java/com/example/baseframe/utils/DensityUtil.java new file mode 100644 index 0000000..e681075 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/DensityUtil.java @@ -0,0 +1,106 @@ +package com.example.baseframe.utils; + +import android.app.Activity; +import android.app.Application; +import android.content.ComponentCallbacks; +import android.content.res.Configuration; +import android.util.DisplayMetrics; + +import com.example.baseframe.api.App; + +/** + * @ClassName: (今日头条适配方案) 根据设计图 宽高的dp去设置(现在主流的UI设计图都使用蓝湖-设计图px会标注换算成dp) + * @Description: 用来做屏幕适配,在application或者是baseactivity里面设置,需要在setcontentview前设置 + * 动态的设置屏幕的density。 + * @CreateDate: 2019/8/30 14:48 + * @Version: 1.0 + */ +public class DensityUtil { + + private static float WIDTH = 600;//参考设备的宽,单位是dp + private static float appDensity;//表示屏幕密度 + private static float appScaleDensity;//字体缩放比列,默认AppDensity + + public static void setDensity(final Application application, Activity activity,float width) { + WIDTH=width; + DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics(); + if (appDensity == 0) { + //初始化赋值操作 + appDensity = displayMetrics.density; + appScaleDensity = displayMetrics.scaledDensity; + + //添加字体变化监听回调 + application.registerComponentCallbacks(new ComponentCallbacks() { + @Override + public void onConfigurationChanged(Configuration newConfig) { + //字体发生更改,重新对ScaleDensity进行赋值 + if (newConfig != null && newConfig.fontScale > 0) { + appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity; + } + } + + @Override + public void onLowMemory() { + + } + }); + } + + //计算目标值density,scaleDensity,densityDpi + float targetDensity = displayMetrics.widthPixels / WIDTH;//1080/360=3.0 + float targetScaleDensity = targetDensity * (appScaleDensity / appDensity); + int targetDensityDpi = (int) (targetDensity * 160); + + //替换Activity的density,scaleDensity,densityDpi + DisplayMetrics dm = activity.getResources().getDisplayMetrics(); + dm.density = targetDensity; + dm.scaledDensity = targetScaleDensity; + dm.densityDpi = targetDensityDpi; + } + + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dip2px(float dpValue) { + final float scale = App.getInstance().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dip(float pxValue) { + final float scale = App.getInstance().getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * 将px值转换为sp值,保证文字大小不变 + * + * @param pxValue + * @return + */ + public static int px2sp(float pxValue) { + final float fontScale = App.getInstance().getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 将sp值转换为px值,保证文字大小不变 + * @param spValue + * @return + */ + public static int sp2px(float spValue) { + final float fontScale = App.getInstance().getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + public static int getScreenWidth() { + return App.getInstance().getResources().getDisplayMetrics().widthPixels; + } + + public static int getScreenHeight() { + return App.getInstance().getResources().getDisplayMetrics().heightPixels; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/DeviceUtils.java b/app/src/main/java/com/example/baseframe/utils/DeviceUtils.java new file mode 100644 index 0000000..f779cb6 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/DeviceUtils.java @@ -0,0 +1,701 @@ +package com.example.baseframe.utils; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; + +import com.blankj.ALog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * detail: 设备相关工具类 + * @author Ttt + *
+ *     @see 
+ *     android.os.Build.BOARD: 获取设备基板名称
+ *     android.os.Build.BOOTLOADER: 获取设备引导程序版本号
+ *     android.os.Build.BRAND: 获取设备品牌
+ *     android.os.Build.CPU_ABI: 获取设备指令集名称 (CPU 的类型 )
+ *     android.os.Build.CPU_ABI2: 获取第二个指令集名称
+ *     android.os.Build.DEVICE: 获取设备驱动名称
+ *     android.os.Build.DISPLAY: 获取设备显示的版本包 ( 在系统设置中显示为版本号 ) 和 ID 一样
+ *     android.os.Build.FINGERPRINT: 设备的唯一标识, 由设备的多个信息拼接合成
+ *     android.os.Build.HARDWARE: 设备硬件名称, 一般和基板名称一样 (BOARD)
+ *     android.os.Build.HOST: 设备主机地址
+ *     android.os.Build.ID: 设备版本号
+ *     android.os.Build.MODEL : 获取手机的型号 设备名称
+ *     android.os.Build.MANUFACTURER: 获取设备制造商
+ *     android:os.Build.PRODUCT: 整个产品的名称
+ *     android:os.Build.RADIO: 无线电固件版本号, 通常是不可用的 显示 unknown
+ *     android.os.Build.TAGS: 设备标签, 如 release-keys 或测试的 test-keys
+ *     android.os.Build.TIME: 时间
+ *     android.os.Build.TYPE: 设备版本类型 主要为 "user" 或 "eng".
+ *     android.os.Build.USER: 设备用户名 基本上都为 android-build
+ *     android.os.Build.VERSION.RELEASE: 获取系统版本字符串, 如 4.1.2 或 2.2 或 2.3 等
+ *     android.os.Build.VERSION.CODENAME: 设备当前的系统开发代号, 一般使用 REL 代替
+ *     android.os.Build.VERSION.INCREMENTAL: 系统源代码控制值, 一个数字或者 git hash 值
+ *     android.os.Build.VERSION.SDK: 系统的 API 级别 一般使用下面大的 SDK_INT 来查看
+ *     android.os.Build.VERSION.SDK_INT: 系统的 API 级别 数字表示
+ *     

+ * 所需权限 + * + * + *
+ */ +public final class DeviceUtils { + + private DeviceUtils() { + } + + // 日志 TAG + private static final String TAG = DeviceUtils.class.getSimpleName(); + // 换行字符串 + private static final String NEW_LINE_STR = System.getProperty("line.separator"); + + /** + * 获取设备信息 + * @return {@link Map} + */ + public static Map getDeviceInfo() { + return getDeviceInfo(new HashMap<>()); + } + + /** + * 获取设备信息 + * @param deviceInfoMap 设备信息 Map + * @return {@link Map} + */ + public static Map getDeviceInfo(final Map deviceInfoMap) { + // 获取设备信息类的所有申明的字段, 即包括 public、private 和 proteced, 但是不包括父类的申明字段 + Field[] fields = Build.class.getDeclaredFields(); + // 遍历字段 + for (Field field : fields) { + try { + // 取消 Java 的权限控制检查 + field.setAccessible(true); + // 转换当前设备支持的 ABI - CPU 指令集 + if (field.getName().toLowerCase().startsWith("SUPPORTED".toLowerCase())) { + try { + Object object = field.get(null); + // 判断是否数组 + if (object instanceof String[]) { + if (object != null) { + // 获取类型对应字段的数据, 并保存支持的指令集 [arm64-v8a, armeabi-v7a, armeabi] + deviceInfoMap.put(field.getName(), Arrays.toString((String[]) object)); + } + continue; + } + } catch (Exception e) { + } + } + // 获取类型对应字段的数据, 并保存 + deviceInfoMap.put(field.getName(), field.get(null).toString()); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDeviceInfo"); + } + } + return deviceInfoMap; + } + + /** + * 处理设备信息 + * @param deviceInfoMap 设备信息 Map + * @param errorInfo 错误提示信息, 如获取设备信息失败 + * @return 拼接后的设备信息字符串 + */ + public static String handlerDeviceInfo(final Map deviceInfoMap, final String errorInfo) { + try { + // 初始化 Builder, 拼接字符串 + StringBuilder builder = new StringBuilder(); + // 获取设备信息 + Iterator> mapIter = deviceInfoMap.entrySet().iterator(); + // 遍历设备信息 + while (mapIter.hasNext()) { + // 获取对应的 key - value + Map.Entry rnEntry = mapIter.next(); + String rnKey = rnEntry.getKey(); // key + String rnValue = rnEntry.getValue(); // value + // 保存设备信息 + builder.append(rnKey); + builder.append(" = "); + builder.append(rnValue); + builder.append(NEW_LINE_STR); + } + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "handlerDeviceInfo"); + } + return errorInfo; + } + + // = + + /** + * 获取设备基板名称 + * @return 设备基板名称 + */ + public static String getBoard() { + return Build.BOARD; + } + + /** + * 获取设备引导程序版本号 + * @return 设备引导程序版本号 + */ + public static String getBootloader() { + return Build.BOOTLOADER; + } + + /** + * 获取设备品牌 + * @return 设备品牌 + */ + public static String getBrand() { + return Build.BRAND; + } + + /** + * 获取支持的第一个指令集 + * @return 支持的第一个指令集 + */ + public static String getCPU_ABI() { + return Build.CPU_ABI; + } + + /** + * 获取支持的第二个指令集 + * @return 支持的第二个指令集 + */ + public static String getCPU_ABI2() { + return Build.CPU_ABI2; + } + + /** + * 获取支持的指令集 如: [arm64-v8a, armeabi-v7a, armeabi] + * @return 支持的指令集 + */ + public static String[] getABIs() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Build.SUPPORTED_ABIS; + } else { + if (!TextUtils.isEmpty(Build.CPU_ABI2)) { + return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; + } + return new String[]{Build.CPU_ABI}; + } + } + + /** + * 获取支持的 32 位指令集 + * @return 支持的 32 位指令集 + */ + public static String[] getSUPPORTED_32_BIT_ABIS() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Build.SUPPORTED_32_BIT_ABIS; + } + return null; + } + + /** + * 获取支持的 64 位指令集 + * @return 支持的 64 位指令集 + */ + public static String[] getSUPPORTED_64_BIT_ABIS() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Build.SUPPORTED_64_BIT_ABIS; + } + return null; + } + + /** + * 获取设备驱动名称 + * @return 设备驱动名称 + */ + public static String getDevice() { + return Build.DEVICE; + } + + /** + * 获取设备显示的版本包 ( 在系统设置中显示为版本号 ) 和 ID 一样 + * @return 设备显示的版本包 + */ + public static String getDisplay() { + return Build.DISPLAY; + } + + /** + * 获取设备的唯一标识, 由设备的多个信息拼接合成 + * @return 设备的唯一标识, 由设备的多个信息拼接合成 + */ + public static String getFingerprint() { + return Build.FINGERPRINT; + } + + /** + * 获取设备硬件名称, 一般和基板名称一样 (BOARD) + * @return 设备硬件名称, 一般和基板名称一样 (BOARD) + */ + public static String getHardware() { + return Build.HARDWARE; + } + + /** + * 获取设备主机地址 + * @return 设备主机地址 + */ + public static String getHost() { + return Build.HOST; + } + + /** + * 获取设备版本号 + * @return 设备版本号 + */ + public static String getID() { + return Build.ID; + } + + /** + * 获取设备型号 如 RedmiNote4X + * @return 设备型号 + */ + public static String getModel() { + String model = Build.MODEL; + if (model != null) { + model = model.trim().replaceAll("\\s*", ""); + } else { + model = ""; + } + return model; + } + + /** + * 获取设备厂商 如 Xiaomi + * @return 设备厂商 + */ + public static String getManufacturer() { + return Build.MANUFACTURER; + } + + /** + * 获取整个产品的名称 + * @return 整个产品的名称 + */ + public static String getProduct() { + return Build.PRODUCT; + } + + /** + * 获取无线电固件版本号, 通常是不可用的 显示 unknown + * @return 无线电固件版本号 + */ + public static String getRadio() { + return Build.RADIO; + } + + /** + * 获取设备标签, 如 release-keys 或测试的 test-keys + * @return 设备标签 + */ + public static String getTags() { + return Build.TAGS; + } + + /** + * 获取设备时间 + * @return 设备时间 + */ + public static long getTime() { + return Build.TIME; + } + + /** + * 获取设备版本类型 主要为 "user" 或 "eng". + * @return 设备版本类型 + */ + public static String getType() { + return Build.TYPE; + } + + /** + * 获取设备用户名 基本上都为 android-build + * @return 设备用户名 + */ + public static String getUser() { + return Build.USER; + } + + // = + + /** + * 获取 SDK 版本号 + * @return SDK 版本号 + */ + public static int getSDKVersion() { + return Build.VERSION.SDK_INT; + } + + /** + * 获取系统版本号, 如 4.1.2 或 2.2 或 2.3 等 + * @return 系统版本号 + */ + public static String getRelease() { + return Build.VERSION.RELEASE; + } + + /** + * 获取设备当前的系统开发代号, 一般使用 REL 代替 + * @return 设备当前的系统开发代号 + */ + public static String getCodename() { + return Build.VERSION.CODENAME; + } + + /** + * 获取系统源代码控制值, 一个数字或者 git hash 值 + * @return 系统源代码控制值 + */ + public static String getIncremental() { + return Build.VERSION.INCREMENTAL; + } + + /** + * 获取 Android id + *
+     *     在设备首次启动时, 系统会随机生成一个 64 位的数字, 并把这个数字以十六进制字符串的形式保存下来,
+     *     这个十六进制的字符串就是 ANDROID_ID, 当设备被 wipe 后该值会被重置
+     * 
+ * @return Android id + */ + public static String getAndroidId() { + String androidId = null; + try { + androidId = Settings.Secure.getString(ResourceUtils.getContentResolver(), Settings.Secure.ANDROID_ID); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAndroidId"); + } + return androidId; + } + + /** + * 获取基带版本 BASEBAND-VER + * @return 基带版本 BASEBAND-VER + */ + public static String getBaseband_Ver() { + String basebandVersion = ""; + try { + Class clazz = Class.forName("android.os.SystemProperties"); + Object invoker = clazz.newInstance(); + Method method = clazz.getMethod("get", String.class, String.class); + Object result = method.invoke(invoker, "gsm.version.baseband", "no message"); + basebandVersion = (String) result; + } catch (Exception e) { + ALog.eTag(TAG, e, "getBaseband_Ver"); + } + return basebandVersion; + } + + /** + * 获取内核版本 CORE-VER + * @return 内核版本 CORE-VER + */ + public static String getLinuxCore_Ver() { + String kernelVersion = ""; + try { + Process process = Runtime.getRuntime().exec("cat /proc/version"); + InputStream is = process.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr, 8 * 1024); + + String line; + StringBuilder builder = new StringBuilder(); + while ((line = br.readLine()) != null) { + builder.append(line); + } + String result = builder.toString(); + if (result != "") { + String keyword = "version "; + int index = result.indexOf(keyword); + line = result.substring(index + keyword.length()); + index = line.indexOf(" "); + kernelVersion = line.substring(0, index); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getLinuxCore_Ver"); + } + return kernelVersion; + } + + // = + + /** + * 判断设备是否 root + * @return {@code true} yes, {@code false} no + */ + public static boolean isDeviceRooted() { + String su = "su"; + String[] locations = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", + "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"}; + for (String location : locations) { + if (new File(location + su).exists()) { + return true; + } + } + return false; + } + + /** + * 获取是否启用 ADB + * @return {@code true} yes, {@code false} no + */ + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isAdbEnabled() { + try { + return Settings.Secure.getInt(ResourceUtils.getContentResolver(), Settings.Global.ADB_ENABLED, 0) > 0; + } catch (Exception e) { + ALog.eTag(TAG, e, "isAdbEnabled"); + } + return false; + } + + // = + + // Default MAC address reported to a client that does not have the android.permission.LOCAL_MAC_ADDRESS permission. + private static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00"; + + /** + * 获取设备 MAC 地址 + *
+     *     没有打开 Wifi, 则获取 WLAN MAC 地址失败
+     * 
+ * @return 设备 MAC 地址 + */ + @RequiresPermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.ACCESS_WIFI_STATE}) + public static String getMacAddress() { + String macAddress = getMacAddressByWifiInfo(); + if (!DEFAULT_MAC_ADDRESS.equals(macAddress)) { + return macAddress; + } + macAddress = getMacAddressByNetworkInterface(); + if (!DEFAULT_MAC_ADDRESS.equals(macAddress)) { + return macAddress; + } + macAddress = getMacAddressByInetAddress(); + if (!DEFAULT_MAC_ADDRESS.equals(macAddress)) { + return macAddress; + } + macAddress = getMacAddressByFile(); + if (!DEFAULT_MAC_ADDRESS.equals(macAddress)) { + return macAddress; + } + return null; + } + + /** + * 获取 MAC 地址 + * @return MAC 地址 + */ + @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE) + private static String getMacAddressByWifiInfo() { + try { + @SuppressLint("WifiManagerLeak") + WifiManager wifiManager = AppUtils.getWifiManager(); + if (wifiManager != null) { + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) return wifiInfo.getMacAddress(); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getMacAddressByWifiInfo"); + } + return DEFAULT_MAC_ADDRESS; + } + + /** + * 获取 MAC 地址 + * @return MAC 地址 + */ + @RequiresPermission(android.Manifest.permission.INTERNET) + private static String getMacAddressByNetworkInterface() { + try { + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + while (nis.hasMoreElements()) { + NetworkInterface ni = nis.nextElement(); + if (ni == null || !ni.getName().equalsIgnoreCase("wlan0")) continue; + byte[] macBytes = ni.getHardwareAddress(); + if (macBytes != null && macBytes.length > 0) { + StringBuilder builder = new StringBuilder(); + for (byte b : macBytes) { + builder.append(String.format("%02x:", b)); + } + return builder.substring(0, builder.length() - 1); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getMacAddressByNetworkInterface"); + } + return DEFAULT_MAC_ADDRESS; + } + + /** + * 通过 InetAddress 获取 Mac 地址 + * @return Mac 地址 + */ + private static String getMacAddressByInetAddress() { + try { + InetAddress inetAddress = getInetAddress(); + if (inetAddress != null) { + NetworkInterface ni = NetworkInterface.getByInetAddress(inetAddress); + if (ni != null) { + byte[] macBytes = ni.getHardwareAddress(); + if (macBytes != null && macBytes.length > 0) { + StringBuilder builder = new StringBuilder(); + for (byte b : macBytes) { + builder.append(String.format("%02x:", b)); + } + return builder.substring(0, builder.length() - 1); + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getMacAddressByInetAddress"); + } + return DEFAULT_MAC_ADDRESS; + } + + /** + * 获取 InetAddress + * @return {@link InetAddress} + */ + private static InetAddress getInetAddress() { + try { + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + while (nis.hasMoreElements()) { + NetworkInterface ni = nis.nextElement(); + // To prevent phone of xiaomi return "10.0.2.15" + if (!ni.isUp()) continue; + Enumeration addresses = ni.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress inetAddress = addresses.nextElement(); + if (!inetAddress.isLoopbackAddress()) { + String hostAddress = inetAddress.getHostAddress(); + if (hostAddress.indexOf(':') < 0) return inetAddress; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getInetAddress"); + } + return null; + } + + /** + * 获取 MAC 地址 + * @return MAC 地址 + */ + private static String getMacAddressByFile() { + ShellUtils.CommandResult result = ShellUtils.execCmd("getprop wifi.interface", false); + if (result.isSuccess()) { + String name = result.successMsg; + if (name != null) { + result = ShellUtils.execCmd("cat /sys/class/net/" + name + "/address", false); + if (result.result == 0) { + String address = result.successMsg; + if (address != null && address.length() > 0) { + return address; + } + } + } + } + return DEFAULT_MAC_ADDRESS; + } + + // = + + /** + * 关机 ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean shutdown() { + try { + ShellUtils.execCmd("reboot -p", true); + Intent intent = new Intent("android.intent.action.ACTION_REQUEST_SHUTDOWN"); + intent.putExtra("android.intent.extra.KEY_CONFIRM", false); + return AppUtils.startActivity(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "shutdown"); + } + return false; + } + + /** + * 重启设备 ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean reboot() { + try { + ShellUtils.execCmd("reboot", true); + Intent intent = new Intent(Intent.ACTION_REBOOT); + intent.putExtra("nowait", 1); + intent.putExtra("interval", 1); + intent.putExtra("window", 0); + return AppUtils.sendBroadcast(intent); + } catch (Exception e) { + ALog.eTag(TAG, e, "reboot"); + } + return false; + } + + /** + * 重启设备 ( 需要 root 权限 ) - 并进行特殊的引导模式 (recovery、Fastboot) + * @param reason 传递给内核来请求特殊的引导模式, 如 "recovery" + * 重启到 Fastboot 模式 bootloader + * @return {@code true} success, {@code false} fail + */ + public static boolean reboot(final String reason) { + try { + AppUtils.getPowerManager().reboot(reason); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "reboot"); + } + return false; + } + + /** + * 重启引导到 recovery ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean rebootToRecovery() { + ShellUtils.CommandResult result = ShellUtils.execCmd("reboot recovery", true); + return result.isSuccess2(); + } + + /** + * 重启引导到 bootloader ( 需要 root 权限 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean rebootToBootloader() { + ShellUtils.CommandResult result = ShellUtils.execCmd("reboot bootloader", true); + return result.isSuccess2(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/EncryptUtils.java b/app/src/main/java/com/example/baseframe/utils/EncryptUtils.java new file mode 100644 index 0000000..f4cf99b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/EncryptUtils.java @@ -0,0 +1,1254 @@ +package com.example.baseframe.utils;//package com.example.baseframe.utils; +// +//import android.util.Base64; +// +//import java.io.File; +//import java.io.FileInputStream; +//import java.io.IOException; +//import java.security.DigestInputStream; +//import java.security.InvalidKeyException; +//import java.security.Key; +//import java.security.KeyFactory; +//import java.security.MessageDigest; +//import java.security.NoSuchAlgorithmException; +//import java.security.spec.AlgorithmParameterSpec; +//import java.security.spec.InvalidKeySpecException; +//import java.security.spec.PKCS8EncodedKeySpec; +//import java.security.spec.X509EncodedKeySpec; +// +//import javax.crypto.BadPaddingException; +//import javax.crypto.Cipher; +//import javax.crypto.IllegalBlockSizeException; +//import javax.crypto.Mac; +//import javax.crypto.NoSuchPaddingException; +//import javax.crypto.SecretKey; +//import javax.crypto.SecretKeyFactory; +//import javax.crypto.spec.DESKeySpec; +//import javax.crypto.spec.IvParameterSpec; +//import javax.crypto.spec.SecretKeySpec; +// +///** +// *
+// *     author: Blankj
+// *     blog  : http://blankj.com
+// *     time  : 2016/08/02
+// *     desc  : utils about encrypt
+// * 
+// */ +//public final class EncryptUtils { +// +// private EncryptUtils() { +// throw new UnsupportedOperationException("u can't instantiate me..."); +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // hash encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the hex string of MD2 encryption. +// * +// * @param data The data. +// * @return the hex string of MD2 encryption +// */ +// public static String encryptMD2ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptMD2ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of MD2 encryption. +// * +// * @param data The data. +// * @return the hex string of MD2 encryption +// */ +// public static String encryptMD2ToString(final byte[] data) { +// return bytes2HexString(encryptMD2(data)); +// } +// +// /** +// * Return the bytes of MD2 encryption. +// * +// * @param data The data. +// * @return the bytes of MD2 encryption +// */ +// public static byte[] encryptMD2(final byte[] data) { +// return hashTemplate(data, "MD2"); +// } +// +// /** +// * Return the hex string of MD5 encryption. +// * +// * @param data The data. +// * @return the hex string of MD5 encryption +// */ +// public static String encryptMD5ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptMD5ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of MD5 encryption. +// * +// * @param data The data. +// * @param salt The salt. +// * @return the hex string of MD5 encryption +// */ +// public static String encryptMD5ToString(final String data, final String salt) { +// if (data == null && salt == null) return ""; +// if (salt == null) return bytes2HexString(encryptMD5(data.getBytes())); +// if (data == null) return bytes2HexString(encryptMD5(salt.getBytes())); +// return bytes2HexString(encryptMD5((data + salt).getBytes())); +// } +// +// /** +// * Return the hex string of MD5 encryption. +// * +// * @param data The data. +// * @return the hex string of MD5 encryption +// */ +// public static String encryptMD5ToString(final byte[] data) { +// return bytes2HexString(encryptMD5(data)); +// } +// +// /** +// * Return the hex string of MD5 encryption. +// * +// * @param data The data. +// * @param salt The salt. +// * @return the hex string of MD5 encryption +// */ +// public static String encryptMD5ToString(final byte[] data, final byte[] salt) { +// if (data == null && salt == null) return ""; +// if (salt == null) return bytes2HexString(encryptMD5(data)); +// if (data == null) return bytes2HexString(encryptMD5(salt)); +// byte[] dataSalt = new byte[data.length + salt.length]; +// System.arraycopy(data, 0, dataSalt, 0, data.length); +// System.arraycopy(salt, 0, dataSalt, data.length, salt.length); +// return bytes2HexString(encryptMD5(dataSalt)); +// } +// +// /** +// * Return the bytes of MD5 encryption. +// * +// * @param data The data. +// * @return the bytes of MD5 encryption +// */ +// public static byte[] encryptMD5(final byte[] data) { +// return hashTemplate(data, "MD5"); +// } +// +// /** +// * Return the hex string of file's MD5 encryption. +// * +// * @param filePath The path of file. +// * @return the hex string of file's MD5 encryption +// */ +// public static String encryptMD5File2String(final String filePath) { +// File file = isSpace(filePath) ? null : new File(filePath); +// return encryptMD5File2String(file); +// } +// +// /** +// * Return the bytes of file's MD5 encryption. +// * +// * @param filePath The path of file. +// * @return the bytes of file's MD5 encryption +// */ +// public static byte[] encryptMD5File(final String filePath) { +// File file = isSpace(filePath) ? null : new File(filePath); +// return encryptMD5File(file); +// } +// +// /** +// * Return the hex string of file's MD5 encryption. +// * +// * @param file The file. +// * @return the hex string of file's MD5 encryption +// */ +// public static String encryptMD5File2String(final File file) { +// return bytes2HexString(encryptMD5File(file)); +// } +// +// /** +// * Return the bytes of file's MD5 encryption. +// * +// * @param file The file. +// * @return the bytes of file's MD5 encryption +// */ +// public static byte[] encryptMD5File(final File file) { +// if (file == null) return null; +// FileInputStream fis = null; +// DigestInputStream digestInputStream; +// try { +// fis = new FileInputStream(file); +// MessageDigest md = MessageDigest.getInstance("MD5"); +// digestInputStream = new DigestInputStream(fis, md); +// byte[] buffer = new byte[256 * 1024]; +// while (true) { +// if (!(digestInputStream.read(buffer) > 0)) break; +// } +// md = digestInputStream.getMessageDigest(); +// return md.digest(); +// } catch (NoSuchAlgorithmException | IOException e) { +// e.printStackTrace(); +// return null; +// } finally { +// try { +// if (fis != null) { +// fis.close(); +// } +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// +// /** +// * Return the hex string of SHA1 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA1 encryption +// */ +// public static String encryptSHA1ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptSHA1ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of SHA1 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA1 encryption +// */ +// public static String encryptSHA1ToString(final byte[] data) { +// return bytes2HexString(encryptSHA1(data)); +// } +// +// /** +// * Return the bytes of SHA1 encryption. +// * +// * @param data The data. +// * @return the bytes of SHA1 encryption +// */ +// public static byte[] encryptSHA1(final byte[] data) { +// return hashTemplate(data, "SHA-1"); +// } +// +// /** +// * Return the hex string of SHA224 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA224 encryption +// */ +// public static String encryptSHA224ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptSHA224ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of SHA224 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA224 encryption +// */ +// public static String encryptSHA224ToString(final byte[] data) { +// return bytes2HexString(encryptSHA224(data)); +// } +// +// /** +// * Return the bytes of SHA224 encryption. +// * +// * @param data The data. +// * @return the bytes of SHA224 encryption +// */ +// public static byte[] encryptSHA224(final byte[] data) { +// return hashTemplate(data, "SHA224"); +// } +// +// /** +// * Return the hex string of SHA256 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA256 encryption +// */ +// public static String encryptSHA256ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptSHA256ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of SHA256 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA256 encryption +// */ +// public static String encryptSHA256ToString(final byte[] data) { +// return bytes2HexString(encryptSHA256(data)); +// } +// +// /** +// * Return the bytes of SHA256 encryption. +// * +// * @param data The data. +// * @return the bytes of SHA256 encryption +// */ +// public static byte[] encryptSHA256(final byte[] data) { +// return hashTemplate(data, "SHA-256"); +// } +// +// /** +// * Return the hex string of SHA384 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA384 encryption +// */ +// public static String encryptSHA384ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptSHA384ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of SHA384 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA384 encryption +// */ +// public static String encryptSHA384ToString(final byte[] data) { +// return bytes2HexString(encryptSHA384(data)); +// } +// +// /** +// * Return the bytes of SHA384 encryption. +// * +// * @param data The data. +// * @return the bytes of SHA384 encryption +// */ +// public static byte[] encryptSHA384(final byte[] data) { +// return hashTemplate(data, "SHA-384"); +// } +// +// /** +// * Return the hex string of SHA512 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA512 encryption +// */ +// public static String encryptSHA512ToString(final String data) { +// if (data == null || data.length() == 0) return ""; +// return encryptSHA512ToString(data.getBytes()); +// } +// +// /** +// * Return the hex string of SHA512 encryption. +// * +// * @param data The data. +// * @return the hex string of SHA512 encryption +// */ +// public static String encryptSHA512ToString(final byte[] data) { +// return bytes2HexString(encryptSHA512(data)); +// } +// +// /** +// * Return the bytes of SHA512 encryption. +// * +// * @param data The data. +// * @return the bytes of SHA512 encryption +// */ +// public static byte[] encryptSHA512(final byte[] data) { +// return hashTemplate(data, "SHA-512"); +// } +// +// /** +// * Return the bytes of hash encryption. +// * +// * @param data The data. +// * @param algorithm The name of hash encryption. +// * @return the bytes of hash encryption +// */ +// public static byte[] hashTemplate(final byte[] data, final String algorithm) { +// if (data == null || data.length <= 0) return null; +// try { +// MessageDigest md = MessageDigest.getInstance(algorithm); +// md.update(data); +// return md.digest(); +// } catch (NoSuchAlgorithmException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // hmac encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the hex string of HmacMD5 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacMD5 encryption +// */ +// public static String encryptHmacMD5ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacMD5ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacMD5 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacMD5 encryption +// */ +// public static String encryptHmacMD5ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacMD5(data, key)); +// } +// +// /** +// * Return the bytes of HmacMD5 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacMD5 encryption +// */ +// public static byte[] encryptHmacMD5(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacMD5"); +// } +// +// /** +// * Return the hex string of HmacSHA1 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA1 encryption +// */ +// public static String encryptHmacSHA1ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacSHA1ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacSHA1 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA1 encryption +// */ +// public static String encryptHmacSHA1ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacSHA1(data, key)); +// } +// +// /** +// * Return the bytes of HmacSHA1 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacSHA1 encryption +// */ +// public static byte[] encryptHmacSHA1(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacSHA1"); +// } +// +// /** +// * Return the hex string of HmacSHA224 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA224 encryption +// */ +// public static String encryptHmacSHA224ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacSHA224ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacSHA224 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA224 encryption +// */ +// public static String encryptHmacSHA224ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacSHA224(data, key)); +// } +// +// /** +// * Return the bytes of HmacSHA224 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacSHA224 encryption +// */ +// public static byte[] encryptHmacSHA224(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacSHA224"); +// } +// +// /** +// * Return the hex string of HmacSHA256 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA256 encryption +// */ +// public static String encryptHmacSHA256ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacSHA256ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacSHA256 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA256 encryption +// */ +// public static String encryptHmacSHA256ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacSHA256(data, key)); +// } +// +// /** +// * Return the bytes of HmacSHA256 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacSHA256 encryption +// */ +// public static byte[] encryptHmacSHA256(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacSHA256"); +// } +// +// /** +// * Return the hex string of HmacSHA384 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA384 encryption +// */ +// public static String encryptHmacSHA384ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacSHA384ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacSHA384 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA384 encryption +// */ +// public static String encryptHmacSHA384ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacSHA384(data, key)); +// } +// +// /** +// * Return the bytes of HmacSHA384 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacSHA384 encryption +// */ +// public static byte[] encryptHmacSHA384(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacSHA384"); +// } +// +// /** +// * Return the hex string of HmacSHA512 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA512 encryption +// */ +// public static String encryptHmacSHA512ToString(final String data, final String key) { +// if (data == null || data.length() == 0 || key == null || key.length() == 0) return ""; +// return encryptHmacSHA512ToString(data.getBytes(), key.getBytes()); +// } +// +// /** +// * Return the hex string of HmacSHA512 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the hex string of HmacSHA512 encryption +// */ +// public static String encryptHmacSHA512ToString(final byte[] data, final byte[] key) { +// return bytes2HexString(encryptHmacSHA512(data, key)); +// } +// +// /** +// * Return the bytes of HmacSHA512 encryption. +// * +// * @param data The data. +// * @param key The key. +// * @return the bytes of HmacSHA512 encryption +// */ +// public static byte[] encryptHmacSHA512(final byte[] data, final byte[] key) { +// return hmacTemplate(data, key, "HmacSHA512"); +// } +// +// /** +// * Return the bytes of hmac encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param algorithm The name of hmac encryption. +// * @return the bytes of hmac encryption +// */ +// private static byte[] hmacTemplate(final byte[] data, +// final byte[] key, +// final String algorithm) { +// if (data == null || data.length == 0 || key == null || key.length == 0) return null; +// try { +// SecretKeySpec secretKey = new SecretKeySpec(key, algorithm); +// Mac mac = Mac.getInstance(algorithm); +// mac.init(secretKey); +// return mac.doFinal(data); +// } catch (InvalidKeyException | NoSuchAlgorithmException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // DES encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the Base64-encode bytes of DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the Base64-encode bytes of DES encryption +// */ +// public static byte[] encryptDES2Base64(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return base64Encode(encryptDES(data, key, transformation, iv)); +// } +// +// /** +// * Return the hex string of DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the hex string of DES encryption +// */ +// public static String encryptDES2HexString(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return bytes2HexString(encryptDES(data, key, transformation, iv)); +// } +// +// /** +// * Return the bytes of DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of DES encryption +// */ +// public static byte[] encryptDES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "DES", transformation, iv, true); +// } +// +// /** +// * Return the bytes of DES decryption for Base64-encode bytes. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of DES decryption for Base64-encode bytes +// */ +// public static byte[] decryptBase64DES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decryptDES(base64Decode(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of DES decryption for hex string. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of DES decryption for hex string +// */ +// public static byte[] decryptHexStringDES(final String data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decryptDES(hexString2Bytes(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of DES decryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of DES decryption +// */ +// public static byte[] decryptDES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "DES", transformation, iv, false); +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // 3DES encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the Base64-encode bytes of 3DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the Base64-encode bytes of 3DES encryption +// */ +// public static byte[] encrypt3DES2Base64(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return base64Encode(encrypt3DES(data, key, transformation, iv)); +// } +// +// /** +// * Return the hex string of 3DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the hex string of 3DES encryption +// */ +// public static String encrypt3DES2HexString(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return bytes2HexString(encrypt3DES(data, key, transformation, iv)); +// } +// +// /** +// * Return the bytes of 3DES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of 3DES encryption +// */ +// public static byte[] encrypt3DES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "DESede", transformation, iv, true); +// } +// +// /** +// * Return the bytes of 3DES decryption for Base64-encode bytes. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of 3DES decryption for Base64-encode bytes +// */ +// public static byte[] decryptBase64_3DES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decrypt3DES(base64Decode(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of 3DES decryption for hex string. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of 3DES decryption for hex string +// */ +// public static byte[] decryptHexString3DES(final String data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decrypt3DES(hexString2Bytes(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of 3DES decryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of 3DES decryption +// */ +// public static byte[] decrypt3DES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "DESede", transformation, iv, false); +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // AES encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the Base64-encode bytes of AES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the Base64-encode bytes of AES encryption +// */ +// public static byte[] encryptAES2Base64(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return base64Encode(encryptAES(data, key, transformation, iv)); +// } +// +// /** +// * Return the hex string of AES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the hex string of AES encryption +// */ +// public static String encryptAES2HexString(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return bytes2HexString(encryptAES(data, key, transformation, iv)); +// } +// +// /** +// * Return the bytes of AES encryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of AES encryption +// */ +// public static byte[] encryptAES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "AES", transformation, iv, true); +// } +// +// /** +// * Return the bytes of AES decryption for Base64-encode bytes. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of AES decryption for Base64-encode bytes +// */ +// public static byte[] decryptBase64AES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decryptAES(base64Decode(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of AES decryption for hex string. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of AES decryption for hex string +// */ +// public static byte[] decryptHexStringAES(final String data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return decryptAES(hexString2Bytes(data), key, transformation, iv); +// } +// +// /** +// * Return the bytes of AES decryption. +// * +// * @param data The data. +// * @param key The key. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param iv The buffer with the IV. The contents of the +// * buffer are copied to protect against subsequent modification. +// * @return the bytes of AES decryption +// */ +// public static byte[] decryptAES(final byte[] data, +// final byte[] key, +// final String transformation, +// final byte[] iv) { +// return symmetricTemplate(data, key, "AES", transformation, iv, false); +// } +// +// /** +// * Return the bytes of symmetric encryption or decryption. +// * +// * @param data The data. +// * @param key The key. +// * @param algorithm The name of algorithm. +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. +// * @param isEncrypt True to encrypt, false otherwise. +// * @return the bytes of symmetric encryption or decryption +// */ +// private static byte[] symmetricTemplate(final byte[] data, +// final byte[] key, +// final String algorithm, +// final String transformation, +// final byte[] iv, +// final boolean isEncrypt) { +// if (data == null || data.length == 0 || key == null || key.length == 0) return null; +// try { +// SecretKey secretKey; +// if ("DES".equals(algorithm)) { +// DESKeySpec desKey = new DESKeySpec(key); +// SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); +// secretKey = keyFactory.generateSecret(desKey); +// } else { +// secretKey = new SecretKeySpec(key, algorithm); +// } +// Cipher cipher = Cipher.getInstance(transformation); +// if (iv == null || iv.length == 0) { +// cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey); +// } else { +// AlgorithmParameterSpec params = new IvParameterSpec(iv); +// cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, params); +// } +// return cipher.doFinal(data); +// } catch (Throwable e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // RSA encryption +// /////////////////////////////////////////////////////////////////////////// +// +// /** +// * Return the Base64-encode bytes of RSA encryption. +// * +// * @param data The data. +// * @param publicKey The public key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the Base64-encode bytes of RSA encryption +// */ +// public static byte[] encryptRSA2Base64(final byte[] data, +// final byte[] publicKey, +// final int keySize, +// final String transformation) { +// return base64Encode(encryptRSA(data, publicKey, keySize, transformation)); +// } +// +// /** +// * Return the hex string of RSA encryption. +// * +// * @param data The data. +// * @param publicKey The public key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the hex string of RSA encryption +// */ +// public static String encryptRSA2HexString(final byte[] data, +// final byte[] publicKey, +// final int keySize, +// final String transformation) { +// return bytes2HexString(encryptRSA(data, publicKey, keySize, transformation)); +// } +// +// /** +// * Return the bytes of RSA encryption. +// * +// * @param data The data. +// * @param publicKey The public key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the bytes of RSA encryption +// */ +// public static byte[] encryptRSA(final byte[] data, +// final byte[] publicKey, +// final int keySize, +// final String transformation) { +// return rsaTemplate(data, publicKey, keySize, transformation, true); +// } +// +// /** +// * Return the bytes of RSA decryption for Base64-encode bytes. +// * +// * @param data The data. +// * @param privateKey The private key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the bytes of RSA decryption for Base64-encode bytes +// */ +// public static byte[] decryptBase64RSA(final byte[] data, +// final byte[] privateKey, +// final int keySize, +// final String transformation) { +// return decryptRSA(base64Decode(data), privateKey, keySize, transformation); +// } +// +// /** +// * Return the bytes of RSA decryption for hex string. +// * +// * @param data The data. +// * @param privateKey The private key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the bytes of RSA decryption for hex string +// */ +// public static byte[] decryptHexStringRSA(final String data, +// final byte[] privateKey, +// final int keySize, +// final String transformation) { +// return decryptRSA(hexString2Bytes(data), privateKey, keySize, transformation); +// } +// +// /** +// * Return the bytes of RSA decryption. +// * +// * @param data The data. +// * @param privateKey The private key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., RSA/CBC/PKCS1Padding. +// * @return the bytes of RSA decryption +// */ +// public static byte[] decryptRSA(final byte[] data, +// final byte[] privateKey, +// final int keySize, +// final String transformation) { +// return rsaTemplate(data, privateKey, keySize, transformation, false); +// } +// +// /** +// * Return the bytes of RSA encryption or decryption. +// * +// * @param data The data. +// * @param key The key. +// * @param keySize The size of key, e.g. 1024, 2048... +// * @param transformation The name of the transformation, e.g., DES/CBC/PKCS1Padding. +// * @param isEncrypt True to encrypt, false otherwise. +// * @return the bytes of RSA encryption or decryption +// */ +// private static byte[] rsaTemplate(final byte[] data, +// final byte[] key, +// final int keySize, +// final String transformation, +// final boolean isEncrypt) { +// if (data == null || data.length == 0 || key == null || key.length == 0) { +// return null; +// } +// try { +// Key rsaKey; +// if (isEncrypt) { +// X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key); +// rsaKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); +// } else { +// PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); +// rsaKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); +// } +// if (rsaKey == null) return null; +// Cipher cipher = Cipher.getInstance(transformation); +// cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, rsaKey); +// int len = data.length; +// int maxLen = keySize / 8; +// if (isEncrypt) { +// String lowerTrans = transformation.toLowerCase(); +// if (lowerTrans.endsWith("pkcs1padding")) { +// maxLen -= 11; +// } +// } +// int count = len / maxLen; +// if (count > 0) { +// byte[] ret = new byte[0]; +// byte[] buff = new byte[maxLen]; +// int index = 0; +// for (int i = 0; i < count; i++) { +// System.arraycopy(data, index, buff, 0, maxLen); +// ret = joins(ret, cipher.doFinal(buff)); +// index += maxLen; +// } +// if (index != len) { +// int restLen = len - index; +// buff = new byte[restLen]; +// System.arraycopy(data, index, buff, 0, restLen); +// ret = joins(ret, cipher.doFinal(buff)); +// } +// return ret; +// } else { +// return cipher.doFinal(data); +// } +// } catch (NoSuchAlgorithmException e) { +// e.printStackTrace(); +// } catch (NoSuchPaddingException e) { +// e.printStackTrace(); +// } catch (InvalidKeyException e) { +// e.printStackTrace(); +// } catch (BadPaddingException e) { +// e.printStackTrace(); +// } catch (IllegalBlockSizeException e) { +// e.printStackTrace(); +// } catch (InvalidKeySpecException e) { +// e.printStackTrace(); +// } +// return null; +// } +// +// /** +// * Return the bytes of RC4 encryption/decryption. +// * +// * @param data The data. +// * @param key The key. +// */ +// public static byte[] rc4(byte[] data, byte[] key) { +// if (data == null || data.length == 0 || key == null) return null; +// if (key.length < 1 || key.length > 256) { +// throw new IllegalArgumentException("key must be between 1 and 256 bytes"); +// } +// final byte[] iS = new byte[256]; +// final byte[] iK = new byte[256]; +// int keyLen = key.length; +// for (int i = 0; i < 256; i++) { +// iS[i] = (byte) i; +// iK[i] = key[i % keyLen]; +// } +// int j = 0; +// byte tmp; +// for (int i = 0; i < 256; i++) { +// j = (j + iS[i] + iK[i]) & 0xFF; +// tmp = iS[j]; +// iS[j] = iS[i]; +// iS[i] = tmp; +// } +// +// final byte[] ret = new byte[data.length]; +// int i = 0, k, t; +// for (int counter = 0; counter < data.length; counter++) { +// i = (i + 1) & 0xFF; +// j = (j + iS[i]) & 0xFF; +// tmp = iS[j]; +// iS[j] = iS[i]; +// iS[i] = tmp; +// t = (iS[i] + iS[j]) & 0xFF; +// k = iS[t]; +// ret[counter] = (byte) (data[counter] ^ k); +// } +// return ret; +// } +// +// /////////////////////////////////////////////////////////////////////////// +// // other utils methods +// /////////////////////////////////////////////////////////////////////////// +// +// private static byte[] joins(final byte[] prefix, final byte[] suffix) { +// byte[] ret = new byte[prefix.length + suffix.length]; +// System.arraycopy(prefix, 0, ret, 0, prefix.length); +// System.arraycopy(suffix, 0, ret, prefix.length, suffix.length); +// return ret; +// } +// +// private static final char[] HEX_DIGITS = +// {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; +// +// private static String bytes2HexString(final byte[] bytes) { +// if (bytes == null) return ""; +// int len = bytes.length; +// if (len <= 0) return ""; +// char[] ret = new char[len << 1]; +// for (int i = 0, j = 0; i < len; i++) { +// ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f]; +// ret[j++] = HEX_DIGITS[bytes[i] & 0x0f]; +// } +// return new String(ret); +// } +// +// private static byte[] hexString2Bytes(String hexString) { +// if (isSpace(hexString)) return null; +// int len = hexString.length(); +// if (len % 2 != 0) { +// hexString = "0" + hexString; +// len = len + 1; +// } +// char[] hexBytes = hexString.toUpperCase().toCharArray(); +// byte[] ret = new byte[len >> 1]; +// for (int i = 0; i < len; i += 2) { +// ret[i >> 1] = (byte) (hex2Dec(hexBytes[i]) << 4 | hex2Dec(hexBytes[i + 1])); +// } +// return ret; +// } +// +// private static int hex2Dec(final char hexChar) { +// if (hexChar >= '0' && hexChar <= '9') { +// return hexChar - '0'; +// } else if (hexChar >= 'A' && hexChar <= 'F') { +// return hexChar - 'A' + 10; +// } else { +// throw new IllegalArgumentException(); +// } +// } +// +// private static byte[] base64Encode(final byte[] input) { +// return Base64.encode(input, Base64.NO_WRAP); +// } +// +// private static byte[] base64Decode(final byte[] input) { +// return Base64.decode(input, Base64.NO_WRAP); +// } +// +// private static boolean isSpace(final String s) { +// if (s == null) return true; +// for (int i = 0, len = s.length(); i < len; ++i) { +// if (!Character.isWhitespace(s.charAt(i))) { +// return false; +// } +// } +// return true; +// } +//} diff --git a/app/src/main/java/com/example/baseframe/utils/FileIOUtils.java b/app/src/main/java/com/example/baseframe/utils/FileIOUtils.java new file mode 100644 index 0000000..ce2c2d3 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/FileIOUtils.java @@ -0,0 +1,698 @@ +package com.example.baseframe.utils; + +import com.blankj.ALog; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; + +/** + * detail: 文件 (IO 流 ) 工具类 + * @author Ttt + */ +public final class FileIOUtils { + + private FileIOUtils() { + } + + // 日志 TAG + private static final String TAG = FileIOUtils.class.getSimpleName(); + // 换行符 + private static final String NEW_LINE_STR = System.getProperty("line.separator"); + // 缓存大小 + private static int sBufferSize = 8192; + // 无数据读取 + public static final int EOF = -1; + + /** + * 设置缓冲区的大小, 默认大小等于 8192 字节 + * @param bufferSize 缓冲 Buffer 大小 + */ + public static void setBufferSize(final int bufferSize) { + sBufferSize = bufferSize; + } + + /** + * 获取输入流 + * @param filePath 文件路径 + * @return {@link FileInputStream} + */ + public static FileInputStream getFileInputStream(final String filePath) { + return getFileInputStream(FileUtils.getFile(filePath)); + } + + /** + * 获取输入流 + * @param file 文件 + * @return {@link FileInputStream} + */ + public static FileInputStream getFileInputStream(final File file) { + if (file == null) return null; + try { + return new FileInputStream(file); + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileInputStream"); + } + return null; + } + + /** + * 获取输出流 + * @param filePath 文件路径 + * @return {@link FileOutputStream} + */ + public static FileOutputStream getFileOutputStream(final String filePath) { + return getFileOutputStream(FileUtils.getFile(filePath)); + } + + /** + * 获取输出流 + * @param filePath 文件路径 + * @param append 是否追加到结尾 + * @return {@link FileOutputStream} + */ + public static FileOutputStream getFileOutputStream(final String filePath, final boolean append) { + return getFileOutputStream(FileUtils.getFile(filePath), append); + } + + /** + * 获取输出流 + * @param file 文件 + * @return {@link FileOutputStream} + */ + public static FileOutputStream getFileOutputStream(final File file) { + return getFileOutputStream(file, false); + } + + /** + * 获取输出流 + * @param file 文件 + * @param append 是否追加到结尾 + * @return {@link FileOutputStream} + */ + public static FileOutputStream getFileOutputStream(final File file, final boolean append) { + if (file == null) return null; + try { + return new FileOutputStream(file, append); + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileOutputStream"); + } + return null; + } + + // = + + /** + * 通过输入流写入文件 + * @param filePath 文件路径 + * @param inputStream {@link InputStream} + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromIS(final String filePath, final InputStream inputStream) { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, false); + } + + /** + * 通过输入流写入文件 + * @param filePath 文件路径 + * @param inputStream {@link InputStream} + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromIS(final String filePath, final InputStream inputStream, final boolean append) { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, append); + } + + /** + * 通过输入流写入文件 + * @param file 文件 + * @param inputStream {@link InputStream} + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromIS(final File file, final InputStream inputStream) { + return writeFileFromIS(file, inputStream, false); + } + + /** + * 通过输入流写入文件 + * @param file 文件 + * @param inputStream {@link InputStream} + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromIS(final File file, final InputStream inputStream, final boolean append) { + if (inputStream == null || !FileUtils.createOrExistsFile(file)) return false; + OutputStream os = null; + try { + os = new BufferedOutputStream(new FileOutputStream(file, append)); + byte[] data = new byte[sBufferSize]; + int len; + while ((len = inputStream.read(data, 0, sBufferSize)) != EOF) { + os.write(data, 0, len); + } + return true; + } catch (IOException e) { + ALog.eTag(TAG, e, "writeFileFromIS"); + return false; + } finally { + CloseUtils.closeIOQuietly(inputStream, os); + } + } + + /** + * 通过字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByStream(final String filePath, final byte[] bytes) { + return writeFileFromBytesByStream(FileUtils.getFileByPath(filePath), bytes, false); + } + + /** + * 通过字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByStream(final String filePath, final byte[] bytes, final boolean append) { + return writeFileFromBytesByStream(FileUtils.getFileByPath(filePath), bytes, append); + } + + /** + * 通过字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByStream(final File file, final byte[] bytes) { + return writeFileFromBytesByStream(file, bytes, false); + } + + /** + * 通过字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByStream(final File file, final byte[] bytes, final boolean append) { + if (bytes == null || !FileUtils.createOrExistsFile(file)) return false; + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(file, append)); + bos.write(bytes); + return true; + } catch (IOException e) { + ALog.eTag(TAG, e, "writeFileFromBytesByStream"); + return false; + } finally { + CloseUtils.closeIOQuietly(bos); + } + } + + /** + * 通过 FileChannel 把字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByChannel(final String filePath, final byte[] bytes, final boolean isForce) { + return writeFileFromBytesByChannel(FileUtils.getFileByPath(filePath), bytes, false, isForce); + } + + /** + * 通过 FileChannel 把字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByChannel(final String filePath, final byte[] bytes, final boolean append, final boolean isForce) { + return writeFileFromBytesByChannel(FileUtils.getFileByPath(filePath), bytes, append, isForce); + } + + /** + * 通过 FileChannel 把字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByChannel(final File file, final byte[] bytes, final boolean isForce) { + return writeFileFromBytesByChannel(file, bytes, false, isForce); + } + + /** + * 通过 FileChannel 把字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByChannel(final File file, final byte[] bytes, final boolean append, final boolean isForce) { + if (bytes == null || !FileUtils.createOrExistsFile(file)) return false; + FileChannel fc = null; + try { + fc = new FileOutputStream(file, append).getChannel(); + fc.position(fc.size()); + fc.write(ByteBuffer.wrap(bytes)); + if (isForce) fc.force(true); + return true; + } catch (IOException e) { + ALog.eTag(TAG, e, "writeFileFromBytesByChannel"); + return false; + } finally { + CloseUtils.closeIOQuietly(fc); + } + } + + /** + * 通过 MappedByteBuffer 把字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByMap(final String filePath, final byte[] bytes, final boolean isForce) { + return writeFileFromBytesByMap(filePath, bytes, false, isForce); + } + + /** + * 通过 MappedByteBuffer 把字节流写入文件 + * @param filePath 文件路径 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByMap(final String filePath, final byte[] bytes, final boolean append, final boolean isForce) { + return writeFileFromBytesByMap(FileUtils.getFileByPath(filePath), bytes, append, isForce); + } + + /** + * 通过 MappedByteBuffer 把字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByMap(final File file, final byte[] bytes, final boolean isForce) { + return writeFileFromBytesByMap(file, bytes, false, isForce); + } + + /** + * 通过 MappedByteBuffer 把字节流写入文件 + * @param file 文件 + * @param bytes byte[] + * @param append 是否追加到结尾 + * @param isForce 是否强制写入 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromBytesByMap(final File file, final byte[] bytes, final boolean append, final boolean isForce) { + if (bytes == null || !FileUtils.createOrExistsFile(file)) return false; + FileChannel fc = null; + try { + fc = new FileOutputStream(file, append).getChannel(); + MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, fc.size(), bytes.length); + mbb.put(bytes); + if (isForce) mbb.force(); + return true; + } catch (IOException e) { + ALog.eTag(TAG, e, "writeFileFromBytesByMap"); + return false; + } finally { + CloseUtils.closeIOQuietly(fc); + } + } + + /** + * 通过字符串写入文件 + * @param filePath 文件路径 + * @param content 写入内容 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromString(final String filePath, final String content) { + return writeFileFromString(FileUtils.getFileByPath(filePath), content, false); + } + + /** + * 通过字符串写入文件 + * @param filePath 文件路径 + * @param content 写入内容 + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromString(final String filePath, final String content, final boolean append) { + return writeFileFromString(FileUtils.getFileByPath(filePath), content, append); + } + + /** + * 通过字符串写入文件 + * @param file 文件 + * @param content 写入内容 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromString(final File file, final String content) { + return writeFileFromString(file, content, false); + } + + /** + * 通过字符串写入文件 + * @param file 文件 + * @param content 写入内容 + * @param append 是否追加到结尾 + * @return {@code true} success, {@code false} fail + */ + public static boolean writeFileFromString(final File file, final String content, final boolean append) { + if (content == null || !FileUtils.createOrExistsFile(file)) return false; + BufferedWriter bw = null; + try { + bw = new BufferedWriter(new FileWriter(file, append)); + bw.write(content); + return true; + } catch (IOException e) { + ALog.eTag(TAG, e, "writeFileFromString"); + return false; + } finally { + CloseUtils.closeIOQuietly(bw); + } + } + + // ============== + // = 读写分界线 = + // ============== + + /** + * 读取文件内容, 返回换行 List + * @param filePath 文件路径 + * @return 换行 {@link List} + */ + public static List readFileToList(final String filePath) { + return readFileToList(FileUtils.getFileByPath(filePath), null); + } + + /** + * 读取文件内容, 返回换行 List + * @param filePath 文件路径 + * @param charsetName 字符编码 + * @return 换行 {@link List} + */ + public static List readFileToList(final String filePath, final String charsetName) { + return readFileToList(FileUtils.getFileByPath(filePath), charsetName); + } + + /** + * 读取文件内容, 返回换行 List + * @param file 文件 + * @return 换行 {@link List} + */ + public static List readFileToList(final File file) { + return readFileToList(file, 0, Integer.MAX_VALUE, null); + } + + /** + * 读取文件内容, 返回换行 List + * @param file 文件 + * @param charsetName 字符编码 + * @return 换行 {@link List} + */ + public static List readFileToList(final File file, final String charsetName) { + return readFileToList(file, 0, Integer.MAX_VALUE, charsetName); + } + + /** + * 读取文件内容, 返回换行 List + * @param filePath 文件路径 + * @param start 开始位置 + * @param end 结束位置 + * @return 换行 {@link List} + */ + public static List readFileToList(final String filePath, final int start, final int end) { + return readFileToList(FileUtils.getFileByPath(filePath), start, end, null); + } + + /** + * 读取文件内容, 返回换行 List + * @param filePath 文件路径 + * @param start 开始位置 + * @param end 结束位置 + * @param charsetName 字符编码 + * @return 换行 {@link List} + */ + public static List readFileToList(final String filePath, final int start, final int end, final String charsetName) { + return readFileToList(FileUtils.getFileByPath(filePath), start, end, charsetName); + } + + /** + * 读取文件内容, 返回换行 List + * @param file 文件 + * @param start 开始位置 + * @param end 结束位置 + * @return 换行 {@link List} + */ + public static List readFileToList(final File file, final int start, final int end) { + return readFileToList(file, start, end, null); + } + + /** + * 读取文件内容, 返回换行 List + * @param file 文件 + * @param start 开始位置 + * @param end 结束位置 + * @param charsetName 字符编码 + * @return 换行 {@link List} + */ + public static List readFileToList(final File file, final int start, final int end, final String charsetName) { + if (!FileUtils.isFileExists(file)) return null; + if (start > end) return null; + BufferedReader br = null; + try { + String line; + int curLine = 1; + List list = new ArrayList<>(); + if (CommUtils.isEmpty(charsetName)) { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); + } else { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charsetName)); + } + while ((line = br.readLine()) != null) { + if (curLine > end) break; + if (start <= curLine && curLine <= end) list.add(line); + ++curLine; + } + return list; + } catch (IOException e) { + ALog.eTag(TAG, e, "readFileToList"); + return null; + } finally { + CloseUtils.closeIOQuietly(br); + } + } + + // = + + /** + * 读取文件内容, 返回字符串 + * @param filePath 文件路径 + * @return 文件内容字符串 + */ + public static String readFileToString(final String filePath) { + return readFileToString(FileUtils.getFileByPath(filePath), null); + } + + /** + * 读取文件内容, 返回字符串 + * @param filePath 文件路径 + * @param charsetName 字符编码 + * @return 文件内容字符串 + */ + public static String readFileToString(final String filePath, final String charsetName) { + return readFileToString(FileUtils.getFileByPath(filePath), charsetName); + } + + /** + * 读取文件内容, 返回字符串 + * @param file 文件 + * @return 文件内容字符串 + */ + public static String readFileToString(final File file) { + return readFileToString(file, null); + } + + /** + * 读取文件内容, 返回字符串 + * @param file 文件 + * @param charsetName 字符编码 + * @return 文件内容字符串 + */ + public static String readFileToString(final File file, final String charsetName) { + if (!FileUtils.isFileExists(file)) return null; + BufferedReader br = null; + try { + StringBuilder builder = new StringBuilder(); + if (CommUtils.isEmpty(charsetName)) { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); + } else { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charsetName)); + } + String line; + if ((line = br.readLine()) != null) { + builder.append(line); + while ((line = br.readLine()) != null) { + builder.append(NEW_LINE_STR).append(line); + } + } + return builder.toString(); + } catch (IOException e) { + ALog.eTag(TAG, e, "readFileToString"); + return null; + } finally { + CloseUtils.closeIOQuietly(br); + } + } + + /** + * 读取文件内容, 返回 byte[] + * @param filePath 文件路径 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByStream(final String filePath) { + return readFileToBytesByStream(FileUtils.getFileByPath(filePath)); + } + + /** + * 读取文件内容, 返回 byte[] + * @param file 文件 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByStream(final File file) { + if (!FileUtils.isFileExists(file)) return null; + FileInputStream fis = null; + ByteArrayOutputStream baos = null; + try { + fis = new FileInputStream(file); + baos = new ByteArrayOutputStream(); + byte[] b = new byte[sBufferSize]; + int len; + while ((len = fis.read(b, 0, sBufferSize)) != EOF) { + baos.write(b, 0, len); + } + return baos.toByteArray(); + } catch (IOException e) { + ALog.eTag(TAG, e, "readFileToBytesByStream"); + return null; + } finally { + CloseUtils.closeIOQuietly(fis, baos); + } + } + + /** + * 通过 FileChannel, 读取文件内容, 返回 byte[] + * @param filePath 文件路径 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByChannel(final String filePath) { + return readFileToBytesByChannel(FileUtils.getFileByPath(filePath)); + } + + /** + * 通过 FileChannel, 读取文件内容, 返回 byte[] + * @param file 文件 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByChannel(final File file) { + if (!FileUtils.isFileExists(file)) return null; + FileChannel fc = null; + try { + fc = new RandomAccessFile(file, "r").getChannel(); + ByteBuffer byteBuffer = ByteBuffer.allocate((int) fc.size()); + while (true) { + if (!((fc.read(byteBuffer)) > 0)) break; + } + return byteBuffer.array(); + } catch (IOException e) { + ALog.eTag(TAG, e, "readFileToBytesByChannel"); + return null; + } finally { + CloseUtils.closeIOQuietly(fc); + } + } + + /** + * 通过 MappedByteBuffer, 读取文件内容, 返回 byte[] + * @param filePath 文件路径 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByMap(final String filePath) { + return readFileToBytesByMap(FileUtils.getFileByPath(filePath)); + } + + /** + * 通过 MappedByteBuffer, 读取文件内容, 返回 byte[] + * @param file 文件 + * @return 文件内容 byte[] + */ + public static byte[] readFileToBytesByMap(final File file) { + if (!FileUtils.isFileExists(file)) return null; + FileChannel fc = null; + try { + fc = new RandomAccessFile(file, "r").getChannel(); + int size = (int) fc.size(); + MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, size).load(); + byte[] result = new byte[size]; + mbb.get(result, 0, size); + return result; + } catch (IOException e) { + ALog.eTag(TAG, e, "readFileToBytesByMap"); + return null; + } finally { + CloseUtils.closeIOQuietly(fc); + } + } + + // = + + /** + * 复制 InputStream 到 OutputStream + * @param inputStream {@link InputStream} 读取流 + * @param outputStream {@link OutputStream} 写入流 + * @return bytes number + */ + public static long copyLarge(final InputStream inputStream, final OutputStream outputStream) { + try { + byte[] data = new byte[sBufferSize]; + long count = 0; + int n; + while (EOF != (n = inputStream.read(data))) { + outputStream.write(data, 0, n); + count += n; + } + return count; + } catch (Exception e) { + ALog.eTag(TAG, e, "copyLarge"); + } finally { + CloseUtils.closeIOQuietly(inputStream, outputStream); + } + return -1; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/FileUtils.java b/app/src/main/java/com/example/baseframe/utils/FileUtils.java new file mode 100644 index 0000000..428c2a0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/FileUtils.java @@ -0,0 +1,2127 @@ +package com.example.baseframe.utils; + +import com.blankj.ALog; +import com.example.baseframe.utils.encrypt.MD5Utils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * detail: 文件操作工具类 + * @author Ttt + */ +public final class FileUtils { + + private FileUtils() { + } + + // 日志 TAG + private static final String TAG = FileUtils.class.getSimpleName(); + // 换行字符串 + private static final String NEW_LINE_STR = System.getProperty("line.separator"); + + /** + * 获取文件 + * @param filePath 文件路径 + * @return 文件 {@link File} + */ + public static File getFile(final String filePath) { + return getFileByPath(filePath); + } + + /** + * 获取文件 + * @param filePath 文件路径 + * @param fileName 文件名 + * @return 文件 {@link File} + */ + public static File getFile(final String filePath, final String fileName) { + return (filePath != null && fileName != null) ? new File(filePath, fileName) : null; + } + + /** + * 获取文件 + * @param filePath 文件路径 + * @return 文件 {@link File} + */ + public static File getFileByPath(final String filePath) { + return filePath != null ? new File(filePath) : null; + } + + /** + * 获取路径, 并且进行创建目录 + * @param filePath 保存目录 + * @param fileName 文件名 + * @return 文件 {@link File} + */ + public static File getFileCreateFolder(final String filePath, final String fileName) { + // 防止不存在目录文件, 自动创建 + createFolder(filePath); + // 返回处理过后的 File + return getFile(filePath, fileName); + } + + /** + * 获取路径, 并且进行创建目录 + * @param filePath 保存目录 + * @param fileName 文件名 + * @return 文件 {@link File} + */ + public static String getFilePathCreateFolder(final String filePath, final String fileName) { + // 防止不存在目录文件, 自动创建 + createFolder(filePath); + // 返回处理过后的 File + File file = getFile(filePath, fileName); + // 返回文件路径 + return getAbsolutePath(file); + } + + /** + * 判断某个文件夹是否创建, 未创建则创建 ( 纯路径 - 无文件名 ) + * @param dirPath 文件夹路径 ( 无文件名字. 后缀 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolder(final String dirPath) { + return createFolder(getFileByPath(dirPath)); + } + + /** + * 判断某个文件夹是否创建, 未创建则创建 ( 纯路径 - 无文件名 ) + * @param file 文件夹路径 ( 无文件名字. 后缀 ) + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolder(final File file) { + if (file != null) { + try { + // 当这个文件夹不存在的时候则创建文件夹 + if (!file.exists()) { + // 允许创建多级目录 + return file.mkdirs(); + } + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "createFolder"); + } + } + return false; + } + + /** + * 创建文件夹目录 - 可以传入文件名 + * @param filePath 文件路径 + 文件名 + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolderByPath(final String filePath) { + return createFolderByPath(getFileByPath(filePath)); + } + + /** + * 创建文件夹目录 - 可以传入文件名 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolderByPath(final File file) { + // 创建文件夹 - 如果失败才创建 + if (file != null) { + if (file.exists()) { + return true; + } else if (!file.getParentFile().mkdirs()) { + return createFolder(file.getParent()); + } + } + return false; + } + + /** + * 创建多个文件夹, 如果不存在则创建 + * @param filePaths 文件路径数组 + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolderByPaths(final String... filePaths) { + if (filePaths != null && filePaths.length != 0) { + for (int i = 0, len = filePaths.length; i < len; i++) { + createFolder(filePaths[i]); + } + return true; + } + return false; + } + + /** + * 创建多个文件夹, 如果不存在则创建 + * @param files 文件数组 + * @return {@code true} success, {@code false} fail + */ + public static boolean createFolderByPaths(final File... files) { + if (files != null && files.length != 0) { + for (int i = 0, len = files.length; i < len; i++) { + createFolder(files[i]); + } + return true; + } + return false; + } + + // = + + /** + * 判断目录是否存在, 不存在则判断是否创建成功 + * @param dirPath 目录路径 + * @return {@code true} 存在或创建成功, {@code false} 不存在或创建失败 + */ + public static boolean createOrExistsDir(final String dirPath) { + return createOrExistsDir(getFileByPath(dirPath)); + } + + /** + * 判断目录是否存在, 不存在则判断是否创建成功 + * @param file 文件 + * @return {@code true} 存在或创建成功, {@code false} 不存在或创建失败 + */ + public static boolean createOrExistsDir(final File file) { + // 如果存在, 是目录则返回 true, 是文件则返回 false, 不存在则返回是否创建成功 + return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); + } + + /** + * 判断文件是否存在, 不存在则判断是否创建成功 + * @param filePath 文件路径 + * @return {@code true} 存在或创建成功, {@code false} 不存在或创建失败 + */ + public static boolean createOrExistsFile(final String filePath) { + return createOrExistsFile(getFileByPath(filePath)); + } + + /** + * 判断文件是否存在, 不存在则判断是否创建成功 + * @param file 文件 + * @return {@code true} 存在或创建成功, {@code false} 不存在或创建失败 + */ + public static boolean createOrExistsFile(final File file) { + if (file == null) return false; + // 如果存在, 是文件则返回 true, 是目录则返回 false + if (file.exists()) return file.isFile(); + // 判断文件是否存在, 不存在则直接返回 + if (!createOrExistsDir(file.getParentFile())) return false; + try { + // 存在, 则返回新的路径 + return file.createNewFile(); + } catch (Exception e) { + ALog.eTag(TAG, e, "createOrExistsFile"); + return false; + } + } + + /** + * 判断文件是否存在, 存在则在创建之前删除 + * @param filePath 文件路径 + * @return {@code true} 创建成功, {@code false} 创建失败 + */ + public static boolean createFileByDeleteOldFile(final String filePath) { + return createFileByDeleteOldFile(getFileByPath(filePath)); + } + + /** + * 判断文件是否存在, 存在则在创建之前删除 + * @param file 文件 + * @return {@code true} 创建成功, {@code false} 创建失败 + */ + public static boolean createFileByDeleteOldFile(final File file) { + if (file == null) return false; + // 文件存在并且删除失败返回 false + if (file.exists() && !file.delete()) return false; + // 创建目录失败返回 false + if (!createOrExistsDir(file.getParentFile())) return false; + try { + return file.createNewFile(); + } catch (IOException e) { + ALog.eTag(TAG, e, "createFileByDeleteOldFile"); + return false; + } + } + + /** + * 获取文件路径 + * @param file 文件 + * @return 文件路径 + */ + public static String getPath(final File file) { + return file != null ? file.getPath() : null; + } + + /** + * 获取文件绝对路径 + * @param file 文件 + * @return 文件绝对路径 + */ + public static String getAbsolutePath(final File file) { + return file != null ? file.getAbsolutePath() : null; + } + + // = + + /** + * 获取文件名 + * @param file 文件 + * @return 文件名 + */ + public static String getName(final File file) { + return file != null ? file.getName() : null; + } + + /** + * 获取文件名 + * @param filePath 文件路径 + * @return 文件名 + */ + public static String getName(final String filePath) { + return getName(filePath, ""); + } + + /** + * 获取文件名 + * @param filePath 文件路径 + * @param defaultStr 默认字符串 + * @return 文件名, 如果文件路径为 null 时, 返回默认字符串 + */ + public static String getName(final String filePath, final String defaultStr) { + return CommUtils.isEmpty(filePath) ? defaultStr : new File(filePath).getName(); + } + + /** + * 获取文件后缀名 ( 无 "." 单独后缀 ) + * @param file 文件 + * @return 文件后缀名 ( 无 "." 单独后缀 ) + */ + public static String getFileSuffix(final File file) { + return getFileSuffix(getAbsolutePath(file)); + } + + /** + * 获取文件后缀名 ( 无 "." 单独后缀 ) + * @param filePath 文件路径或文件名 + * @return 文件后缀名 ( 无 "." 单独后缀 ) + */ + public static String getFileSuffix(final String filePath) { + // 获取最后的索引 + int lastIndexOf; + // 判断是否存在 + if (filePath != null && (lastIndexOf = filePath.lastIndexOf('.')) != -1) { + String result = filePath.substring(lastIndexOf); + if (result.startsWith(".")) { + return result.substring(1); + } + return result; + } + return null; + } + + /** + * 获取文件名 ( 无后缀 ) + * @param file 文件 + * @return 文件名 ( 无后缀 ) + */ + public static String getFileNotSuffix(final File file) { + return getFileNotSuffix(getName(file)); + } + + /** + * 获取文件名 ( 无后缀 ) + * @param filePath 文件路径 + * @return 文件名 ( 无后缀 ) + */ + public static String getFileNotSuffixToPath(final String filePath) { + return getFileNotSuffix(getName(filePath)); + } + + /** + * 获取文件名 ( 无后缀 ) + * @param fileName 文件名 + * @return 文件名 ( 无后缀 ) + */ + public static String getFileNotSuffix(final String fileName) { + if (fileName != null) { + if (fileName.lastIndexOf('.') != -1) { + return fileName.substring(0, fileName.lastIndexOf('.')); + } else { + return fileName; + } + } + return null; + } + + /** + * 获取路径中的不带拓展名的文件名 + * @param file 文件 + * @return 不带拓展名的文件名 + */ + public static String getFileNameNoExtension(final File file) { + if (file == null) return null; + return getFileNameNoExtension(file.getPath()); + } + + /** + * 获取路径中的不带拓展名的文件名 + * @param filePath 文件路径 + * @return 不带拓展名的文件名 + */ + public static String getFileNameNoExtension(final String filePath) { + if (CommUtils.isEmpty(filePath)) return filePath; + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastSep == -1) { + return (lastPoi == -1 ? filePath : filePath.substring(0, lastPoi)); + } + if (lastPoi == -1 || lastSep > lastPoi) { + return filePath.substring(lastSep + 1); + } + return filePath.substring(lastSep + 1, lastPoi); + } + + /** + * 获取路径中的文件拓展名 + * @param file 文件 + * @return 文件拓展名 + */ + public static String getFileExtension(final File file) { + if (file == null) return null; + return getFileExtension(file.getPath()); + } + + /** + * 获取路径中的文件拓展名 + * @param filePath 文件路径 + * @return 文件拓展名 + */ + public static String getFileExtension(final String filePath) { + if (CommUtils.isEmpty(filePath)) return filePath; + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastPoi == -1 || lastSep >= lastPoi) return ""; + return filePath.substring(lastPoi + 1); + } + + // = + + /** + * 检查是否存在某个文件 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFileExists(final File file) { + return file != null && file.exists(); + } + + /** + * 检查是否存在某个文件 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFileExists(final String filePath) { + return isFileExists(getFileByPath(filePath)); + } + + /** + * 检查是否存在某个文件 + * @param filePath 文件路径 + * @param fileName 文件名 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFileExists(final String filePath, final String fileName) { + return filePath != null && fileName != null && new File(filePath, fileName).exists(); + } + + /** + * 判断是否文件 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFile(final String filePath) { + return isFile(getFileByPath(filePath)); + } + + /** + * 判断是否文件 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFile(final File file) { + return file != null && file.exists() && file.isFile(); + } + + /** + * 判断是否文件夹 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isDirectory(final String filePath) { + return isDirectory(getFileByPath(filePath)); + } + + /** + * 判断是否文件夹 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isDirectory(final File file) { + return file != null && file.exists() && file.isDirectory(); + } + + /** + * 判断是否隐藏文件 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isHidden(final String filePath) { + return isHidden(getFileByPath(filePath)); + } + + /** + * 判断是否隐藏文件 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isHidden(final File file) { + return file != null && file.exists() && file.isHidden(); + } + + // = + + /** + * 获取文件最后修改的毫秒时间戳 + * @param filePath 文件路径 + * @return 文件最后修改的毫秒时间戳 + */ + public static long getFileLastModified(final String filePath) { + return getFileLastModified(getFileByPath(filePath)); + } + + /** + * 获取文件最后修改的毫秒时间戳 + * @param file 文件 + * @return 文件最后修改的毫秒时间戳 + */ + public static long getFileLastModified(final File file) { + if (file == null) return 0L; + return file.lastModified(); + } + + /** + * 获取文件编码格式 + * @param filePath 文件路径 + * @return 文件编码格式 + */ + public static String getFileCharsetSimple(final String filePath) { + return getFileCharsetSimple(getFileByPath(filePath)); + } + + /** + * 获取文件编码格式 + * @param file 文件 + * @return 文件编码格式 + */ + public static String getFileCharsetSimple(final File file) { + if (!isFileExists(file)) return null; + int pos = 0; + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + pos = (is.read() << 8) + is.read(); + } catch (IOException e) { + ALog.eTag(TAG, e, "getFileCharsetSimple"); + } finally { + CloseUtils.closeIOQuietly(is); + } + switch (pos) { + case 0xefbb: + return "UTF-8"; + case 0xfffe: + return "Unicode"; + case 0xfeff: + return "UTF-16BE"; + default: + return "GBK"; + } + } + + /** + * 获取文件行数 + * @param filePath 文件路径 + * @return 文件行数 + */ + public static int getFileLines(final String filePath) { + return getFileLines(getFileByPath(filePath)); + } + + /** + * 获取文件行数 ( 比 readLine 要快很多 ) + * @param file 文件 + * @return 文件行数 + */ + public static int getFileLines(final File file) { + if (!isFileExists(file)) return 0; + int lineCount = 1; + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + byte[] buffer = new byte[1024]; + int readChars; + if (NEW_LINE_STR.endsWith("\n")) { + while ((readChars = is.read(buffer, 0, 1024)) != -1) { + for (int i = 0; i < readChars; ++i) { + if (buffer[i] == '\n') ++lineCount; + } + } + } else { + while ((readChars = is.read(buffer, 0, 1024)) != -1) { + for (int i = 0; i < readChars; ++i) { + if (buffer[i] == '\r') ++lineCount; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileLines"); + } finally { + CloseUtils.closeIOQuietly(is); + } + return lineCount; + } + + // = + + /** + * 获取文件大小 + * @param filePath 文件路径 + * @return 文件大小 + */ + public static String getFileSize(final String filePath) { + return getFileSize(getFileByPath(filePath)); + } + + /** + * 获取文件大小 + * @param file 文件 + * @return 文件大小 + */ + public static String getFileSize(final File file) { + return formatByteMemorySize(getFileLength(file)); + } + + /** + * 获取目录大小 + * @param dirPath 目录路径 + * @return 文件大小 + */ + public static String getDirSize(final String dirPath) { + return getDirSize(getFileByPath(dirPath)); + } + + /** + * 获取目录大小 + * @param dir 目录 + * @return 文件大小 + */ + public static String getDirSize(final File dir) { + return formatByteMemorySize(getDirLength(dir)); + } + + /** + * 获取文件大小 + * @param filePath 文件路径 + * @return 文件大小 + */ + public static long getFileLength(final String filePath) { + return getFileLength(getFileByPath(filePath)); + } + + /** + * 获取文件大小 + * @param file 文件 + * @return 文件大小 + */ + public static long getFileLength(final File file) { + return file != null ? file.length() : 0L; + } + + /** + * 获取目录全部文件大小 + * @param dirPath 目录路径 + * @return 目录全部文件大小 + */ + public static long getDirLength(final String dirPath) { + return getDirLength(getFileByPath(dirPath)); + } + + /** + * 获取目录全部文件大小 + * @param dir 目录 + * @return 目录全部文件大小 + */ + public static long getDirLength(final File dir) { + if (!isDirectory(dir)) return 0; + long len = 0; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.isDirectory()) { + len += getDirLength(file); + } else { + len += file.length(); + } + } + } + return len; + } + + /** + * 获取文件大小 - 网络资源 + * @param httpUri 文件网络链接 + * @return 文件大小 + */ + public static long getFileLengthNetwork(final String httpUri) { + if (CommUtils.isEmpty(httpUri)) return 0L; + // 判断是否网络资源 + boolean isHttpRes = httpUri.toLowerCase().startsWith("http:") || httpUri.toLowerCase().startsWith("https:"); + if (isHttpRes) { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(httpUri).openConnection(); + conn.setRequestProperty("Accept-Encoding", "identity"); + conn.connect(); + if (conn.getResponseCode() == 200) { + return conn.getContentLength(); + } + return 0L; + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileLengthNetwork"); + } + } + return 0L; + } + + /** + * 获取路径中的文件名 + * @param file 文件 + * @return 文件名 + */ + public static String getFileName(final File file) { + if (file == null) return null; + return getFileName(file.getPath()); + } + + /** + * 获取路径中的文件名 + * @param filePath 文件路径 + * @return 文件名 + */ + public static String getFileName(final String filePath) { + if (CommUtils.isEmpty(filePath)) return filePath; + int lastSep = filePath.lastIndexOf(File.separator); + return lastSep == -1 ? filePath : filePath.substring(lastSep + 1); + } + + /** + * 获取路径中的最长目录地址 + * @param file 文件 + * @return 最长目录地址 + */ + public static String getDirName(final File file) { + if (file == null) return null; + return getDirName(file.getPath()); + } + + /** + * 获取全路径中的最长目录地址 + * @param filePath 文件路径 + * @return 最长目录地址 + */ + public static String getDirName(final String filePath) { + if (CommUtils.isEmpty(filePath)) return filePath; + int lastSep = filePath.lastIndexOf(File.separator); + return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1); + } + + // = + + /** + * 重命名文件 - 同个目录下, 修改文件名 + * @param filePath 文件路径 + * @param newFileName 文件新名称 + * @return {@code true} yes, {@code false} no + */ + public static boolean rename(final String filePath, final String newFileName) { + return rename(getFileByPath(filePath), newFileName); + } + + /** + * 重命名文件 - 同个目录下, 修改文件名 + * @param file 文件 + * @param newFileName 文件新名称 + * @return {@code true} yes, {@code false} no + */ + public static boolean rename(final File file, final String newFileName) { + // 文件为空返回 false + if (file == null) return false; + // 文件不存在返回 false + if (!file.exists()) return false; + // 如果文件名没有改变返回 true + if (newFileName.equals(file.getName())) return true; + // 拼接新的文件路径 + File newFile = new File(file.getParent() + File.separator + newFileName); + // 如果重命名的文件已存在返回 false + return !newFile.exists() && file.renameTo(newFile); + } + + // ================ + // = 文件大小处理 = + // ================ + + /** + * 传入文件路径, 返回对应的文件大小 + * @param filePath 文件路径 + * @return 文件大小转换字符串 + */ + public static String formatFileSize(final String filePath) { + File file = getFileByPath(filePath); + return formatFileSize(file != null ? file.length() : 0); + } + + /** + * 传入文件路径, 返回对应的文件大小 + * @param file 文件 + * @return 文件大小转换字符串 + */ + public static String formatFileSize(final File file) { + return formatFileSize(file != null ? file.length() : 0); + } + + /** + * 传入对应的文件大小, 返回转换后文件大小 + * @param fileSize 文件大小 + * @return 文件大小转换字符串 + */ + public static String formatFileSize(final double fileSize) { + // 转换文件大小 + DecimalFormat df = new DecimalFormat("#.00"); + String fileSizeStr; + if (fileSize <= 0) { + fileSizeStr = "0B"; + } else if (fileSize < 1024) { + fileSizeStr = df.format(fileSize) + "B"; + } else if (fileSize < 1048576) { + fileSizeStr = df.format(fileSize / 1024) + "KB"; + } else if (fileSize < 1073741824) { + fileSizeStr = df.format(fileSize / 1048576) + "MB"; + } else if (fileSize < 1099511627776d) { + fileSizeStr = df.format(fileSize / 1073741824) + "GB"; + } else { + fileSizeStr = df.format(fileSize / 1099511627776d) + "TB"; + } + return fileSizeStr; + } + + /** + * 字节数转合适内存大小 保留 3 位小数 (%.位数f) + * @param byteSize 字节数 + * @return 合适内存大小字符串 + */ + public static String formatByteMemorySize(final double byteSize) { + return formatByteMemorySize(3, byteSize); + } + + /** + * 字节数转合适内存大小 保留 number 位小数 (%.位数f) + * @param number 保留小数位数 + * @param byteSize 字节数 + * @return 合适内存大小字符串 + */ + public static String formatByteMemorySize(final int number, final double byteSize) { + if (byteSize < 0d) { + return "0B"; + } else if (byteSize < 1024d) { + return String.format("%." + number + "fB", byteSize); + } else if (byteSize < 1048576d) { + return String.format("%." + number + "fKB", byteSize / 1024d); + } else if (byteSize < 1073741824d) { + return String.format("%." + number + "fMB", byteSize / 1048576d); + } else if (byteSize < 1099511627776d) { + return String.format("%." + number + "fGB", byteSize / 1073741824d); + } else { + return String.format("%." + number + "fTB", byteSize / 1099511627776d); + } + } + + // ============ + // = 文件操作 = + // ============ + + /** + * 删除文件 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFile(final String filePath) { + return deleteFile(getFileByPath(filePath)); + } + + /** + * 删除文件 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFile(final File file) { + // 文件存在, 并且不是目录文件, 则直接删除 + if (file != null && file.exists() && !file.isDirectory()) { + return file.delete(); + } + return false; + } + + /** + * 删除多个文件 + * @param filePaths 文件路径数组 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFiles(final String... filePaths) { + if (filePaths != null && filePaths.length != 0) { + for (int i = 0, len = filePaths.length; i < len; i++) { + deleteFile(filePaths[i]); + } + return true; + } + return false; + } + + /** + * 删除多个文件 + * @param files 文件数组 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFiles(final File... files) { + if (files != null && files.length != 0) { + for (int i = 0, len = files.length; i < len; i++) { + deleteFile(files[i]); + } + return true; + } + return false; + } + + // = + + /** + * 删除文件夹 + * @param filePath 文件路径 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFolder(final String filePath) { + return deleteFolder(getFileByPath(filePath)); + } + + /** + * 删除文件夹 + * @param file 文件 + * @return {@code true} success, {@code false} fail + */ + public static boolean deleteFolder(final File file) { + if (file != null) { + try { + // 文件存在, 并且不是目录文件, 则直接删除 + if (file.exists()) { + if (file.isDirectory()) { // 属于文件目录 + File[] files = file.listFiles(); + if (null == files || files.length == 0) { + file.delete(); + } + for (File f : files) { + deleteFolder(f.getPath()); + } + return file.delete(); + } else { // 属于文件 + return deleteFile(file); + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "deleteFolder"); + } + } + return false; + } + + /** + * 保存文件 + * @param filePath 文件路径 + * @param data 待存储数据 + * @return {@code true} success, {@code false} fail + */ + public static boolean saveFile(final String filePath, final byte[] data) { + return saveFile(FileUtils.getFile(filePath), data); + } + + /** + * 保存文件 + * @param file 文件 + * @param data 待存储数据 + * @return {@code true} success, {@code false} fail + */ + public static boolean saveFile(final File file, final byte[] data) { + if (file != null && data != null) { + try { + // 防止文件夹没创建 + createFolder(getDirName(file)); + // 写入文件 + FileOutputStream fos = new FileOutputStream(file); + BufferedOutputStream bos = new BufferedOutputStream(fos); + bos.write(data); + bos.close(); + fos.close(); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "saveFile"); + } + } + return false; + } + + /** + * 追加文件 + * @param filePath 文件路径 + * @param data 待追加数据 + * @return {@code true} success, {@code false} fail + */ + public static boolean appendFile(final String filePath, final byte[] data) { + return appendFile(FileUtils.getFile(filePath), data); + } + + /** + * 追加文件 + *
+     *     如果未创建文件, 则会创建并写入数据 ( 等同 {@link #saveFile} )
+     *     如果已创建文件, 则在结尾追加数据
+     * 
+ * @param file 文件 + * @param data 待追加数据 + * @return {@code true} success, {@code false} fail + */ + public static boolean appendFile(final File file, final byte[] data) { + try { + // 防止文件夹没创建 + createFolder(getDirName(file)); + // 写入文件 + FileOutputStream fos = new FileOutputStream(file, true); + BufferedOutputStream bos = new BufferedOutputStream(fos); + bos.write(data); + bos.close(); + fos.close(); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "appendFile"); + } + return false; + } + + // = + + /** + * 读取文件 + * @param filePath 文件路径 + * @return 文件内容 byte[] + */ + public static byte[] readFileBytes(final String filePath) { + return readFileBytes(getFileByPath(filePath)); + } + + /** + * 读取文件 + * @param file 文件 + * @return 文件内容 byte[] + */ + public static byte[] readFileBytes(final File file) { + if (file != null && file.exists()) { + try { + FileInputStream fis = new FileInputStream(file); + int length = fis.available(); + byte[] buffer = new byte[length]; + fis.read(buffer); + fis.close(); + return buffer; + } catch (Exception e) { + ALog.eTag(TAG, e, "readFileBytes"); + } + } + return null; + } + + /** + * 读取文件 + * @param inputStream {@link InputStream} + * @return 文件内容 byte[] + */ + public static byte[] readFileBytes(final InputStream inputStream) { + if (inputStream != null) { + try { + int length = inputStream.available(); + byte[] buffer = new byte[length]; + inputStream.read(buffer); + inputStream.close(); + return buffer; + } catch (Exception e) { + ALog.eTag(TAG, e, "readFileBytes"); + } + } + return null; + } + + /** + * 读取文件 + * @param filePath 文件路径 + * @return 文件内容字符串 + */ + public static String readFile(final String filePath) { + return readFile(getFileByPath(filePath)); + } + + /** + * 读取文件 + * @param file 文件 + * @return 文件内容字符串 + */ + public static String readFile(final File file) { + if (file != null && file.exists()) { + try { + return readFile(new FileInputStream(file)); + } catch (Exception e) { + ALog.eTag(TAG, e, "readFile"); + } + } + return null; + } + + /** + * 读取文件 + * @param inputStream {@link InputStream} new FileInputStream(path) + * @return 文件内容字符串 + */ + public static String readFile(final InputStream inputStream) { + return readFile(inputStream, null); + } + + /** + * 读取文件 + * @param inputStream {@link InputStream} new FileInputStream(path) + * @param encode 编码格式 + * @return 文件内容字符串 + */ + public static String readFile(final InputStream inputStream, final String encode) { + if (inputStream != null) { + try { + InputStreamReader isr = null; + if (encode != null) { + new InputStreamReader(inputStream, encode); + } else { + new InputStreamReader(inputStream); + } + BufferedReader br = new BufferedReader(isr); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + builder.append(line); + } + isr.close(); + br.close(); + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "readFile"); + } + } + return null; + } + + // = + + /** + * 复制单个文件 + * @param inputStream 文件流 ( 被复制 ) + * @param destFilePath 目标文件地址 + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + public static boolean copyFile(final InputStream inputStream, final String destFilePath, final boolean overlay) { + if (inputStream == null || destFilePath == null) { + return false; + } + File destFile = new File(destFilePath); + // 如果属于文件夹则跳过 + if (destFile.isDirectory()) { + return false; + } + if (destFile.exists()) { + // 如果目标文件存在并允许覆盖 + if (overlay) { + // 删除已经存在的目标文件, 无论目标文件是目录还是单个文件 + destFile.delete(); + } else { // 如果文件存在, 但是不覆盖, 则返回 false 表示失败 + return false; + } + } else { + // 如果目标文件所在目录不存在, 则创建目录 + if (!destFile.getParentFile().exists()) { + // 目标文件所在目录不存在 + if (!destFile.getParentFile().mkdirs()) { + // 复制文件失败: 创建目标文件所在目录失败 + return false; + } + } + } + // 复制文件 + int byteread = 0; // 读取的字节数 + InputStream is = inputStream; + OutputStream os = null; + try { + os = new FileOutputStream(destFile); + byte[] buffer = new byte[1024]; + while ((byteread = is.read(buffer)) != -1) { + os.write(buffer, 0, byteread); + } + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "copyFile"); + return false; + } finally { + CloseUtils.closeIOQuietly(os, is); + } + } + + /** + * 复制单个文件 + * @param srcFilePath 待复制的文件地址 + * @param destFilePath 目标文件地址 + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + public static boolean copyFile(final String srcFilePath, final String destFilePath, final boolean overlay) { + if (srcFilePath == null || destFilePath == null) { + return false; + } + File srcFile = new File(srcFilePath); + // 判断源文件是否存在 + if (!srcFile.exists()) { + return false; + } else if (!srcFile.isFile()) { // srcFile.isDirectory(); + return false; + } + // 判断目标文件是否存在 + File destFile = new File(destFilePath); + // 如果属于文件夹则跳过 + if (destFile.isDirectory()) { + return false; + } + if (destFile.exists()) { + // 如果目标文件存在并允许覆盖 + if (overlay) { + // 删除已经存在的目标文件, 无论目标文件是目录还是单个文件 + new File(destFilePath).delete(); + } else { // 如果文件存在, 但是不覆盖, 则返回 false 表示失败 + return false; + } + } else { + // 如果目标文件所在目录不存在, 则创建目录 + if (!destFile.getParentFile().exists()) { + // 目标文件所在目录不存在 + if (!destFile.getParentFile().mkdirs()) { + // 复制文件失败: 创建目标文件所在目录失败 + return false; + } + } + } + // 复制文件 + int byteread = 0; // 读取的字节数 + InputStream is = null; + OutputStream os = null; + try { + is = new FileInputStream(srcFile); + os = new FileOutputStream(destFile); + byte[] buffer = new byte[1024]; + while ((byteread = is.read(buffer)) != -1) { + os.write(buffer, 0, byteread); + } + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "copyFile"); + return false; + } finally { + CloseUtils.closeIOQuietly(os, is); + } + } + + /** + * 复制文件夹 + * @param srcFolderPath 待复制的文件夹地址 + * @param destFolderPath 目标文件夹地址 + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + public static boolean copyFolder(final String srcFolderPath, final String destFolderPath, final boolean overlay) { + return copyFolder(srcFolderPath, destFolderPath, srcFolderPath, overlay); + } + + /** + * 复制文件夹 + * @param srcFolderPath 待复制的文件夹地址 + * @param destFolderPath 目标文件夹地址 + * @param sourcePath 源文件地址 ( 用于保递归留记录 ) + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + private static boolean copyFolder(final String srcFolderPath, final String destFolderPath, final String sourcePath, final boolean overlay) { + if (srcFolderPath == null || destFolderPath == null || sourcePath == null) { + return false; + } + File srcFile = new File(srcFolderPath); + // 判断源文件是否存在 + if (!srcFile.exists()) { + return false; + } else if (!srcFile.isDirectory()) { // 不属于文件夹则跳过 + return false; + } + // 判断目标文件是否存在 + File destFile = new File(destFolderPath); + // 如果文件夹没创建, 则创建 + if (!destFile.exists()) { + // 允许创建多级目录 + destFile.mkdirs(); + } + // 判断是否属于文件夹 + if (!destFile.isDirectory()) { // 不属于文件夹则跳过 + return false; + } + // 判断是否存在 + if (srcFile.exists()) { + // 获取文件路径 + File[] files = srcFile.listFiles(); + // 防止不存在文件 + if (files != null && files.length != 0) { + // 进行遍历 + for (File file : files) { + // 文件存在才进行处理 + if (file.exists()) { + // 属于文件夹 + if (file.isDirectory()) { + copyFolder(file.getAbsolutePath(), destFolderPath, sourcePath, overlay); + } else { // 属于文件 + // 复制的文件地址 + String filePath = file.getAbsolutePath(); + // 获取源文件地址 - 并且进行判断 + String dealSource = new File(sourcePath).getAbsolutePath(); + // 属于最前才进行处理 + if (filePath.indexOf(dealSource) == 0) { + // 获取处理后的地址 + dealSource = filePath.substring(dealSource.length()); + // 获取需要复制保存的地址 + String savePath = new File(destFolderPath, dealSource).getAbsolutePath(); + // 进行复制文件 + boolean isResult = copyFile(filePath, savePath, overlay); + } + } + } + } + } + } + return true; + } + + // = + + /** + * 移动 ( 剪切 ) 文件 + * @param srcFilePath 待移动的文件地址 + * @param destFilePath 目标文件地址 + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + public static boolean moveFile(final String srcFilePath, final String destFilePath, final boolean overlay) { + // 复制文件 + if (copyFile(srcFilePath, destFilePath, overlay)) { + // 删除文件 + return deleteFile(srcFilePath); + } + return false; + } + + /** + * 移动 ( 剪切 ) 文件夹 + * @param srcFilePath 待移动的文件夹地址 + * @param destFilePath 目标文件夹地址 + * @param overlay 如果目标文件存在, 是否覆盖 + * @return {@code true} success, {@code false} fail + */ + public static boolean moveFolder(final String srcFilePath, final String destFilePath, final boolean overlay) { + // 复制文件夹 + if (copyFolder(srcFilePath, destFilePath, overlay)) { + // 删除文件夹 + return deleteFolder(srcFilePath); + } + return false; + } + + // = + + /** + * detail: 覆盖 / 替换事件 + * @author Ttt + */ + public interface OnReplaceListener { + + /** + * 是否覆盖 / 替换文件 + * @return {@code true} yes, {@code false} no + */ + boolean onReplace(); + } + + /** + * 复制或移动目录 + * @param srcDirPath 源目录路径 + * @param destDirPath 目标目录路径 + * @param listener 是否覆盖监听器 + * @param isMove 是否移动 + * @return {@code true} 复制或移动成功, {@code false} 复制或移动失败 + */ + public static boolean copyOrMoveDir(final String srcDirPath, final String destDirPath, final OnReplaceListener listener, final boolean isMove) { + return copyOrMoveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener, isMove); + } + + /** + * 复制或移动目录 + * @param srcDir 源目录 + * @param destDir 目标目录 + * @param listener 是否覆盖监听器 + * @param isMove 是否移动 + * @return {@code true} 复制或移动成功, {@code false} 复制或移动失败 + */ + public static boolean copyOrMoveDir(final File srcDir, final File destDir, final OnReplaceListener listener, final boolean isMove) { + if (srcDir == null || destDir == null || listener == null) return false; + // 为防止以上这种情况出现出现误判, 须分别在后面加个路径分隔符 + String srcPath = srcDir.getPath() + File.separator; + String destPath = destDir.getPath() + File.separator; + if (destPath.contains(srcPath)) return false; + // 源文件不存在或者不是目录则返回 false + if (!srcDir.exists() || !srcDir.isDirectory()) return false; + if (destDir.exists()) { + if (listener.onReplace()) { // 需要覆盖则删除旧目录 + if (!deleteAllInDir(destDir)) { // 删除文件失败的话返回 false + return false; + } + } else { // 不需要覆盖直接返回即可 true + return true; + } + } + // 目标目录不存在返回 false + if (!createOrExistsDir(destDir)) return false; + File[] files = srcDir.listFiles(); + for (File file : files) { + File oneDestFile = new File(destPath + file.getName()); + if (file.isFile()) { + // 如果操作失败返回 false + if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false; + } else if (file.isDirectory()) { + // 如果操作失败返回 false + if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false; + } + } + return !isMove || deleteDir(srcDir); + } + + /** + * 复制或移动文件 + * @param srcFilePath 源文件路径 + * @param destFilePath 目标文件路径 + * @param listener 是否覆盖监听器 + * @param isMove 是否移动 + * @return {@code true} 复制或移动成功, {@code false} 复制或移动失败 + */ + public static boolean copyOrMoveFile(final String srcFilePath, final String destFilePath, final OnReplaceListener listener, final boolean isMove) { + return copyOrMoveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener, isMove); + } + + /** + * 复制或移动文件 + * @param srcFile 源文件 + * @param destFile 目标文件 + * @param listener 是否覆盖监听器 + * @param isMove 是否移动 + * @return {@code true} 复制或移动成功, {@code false} 复制或移动失败 + */ + public static boolean copyOrMoveFile(final File srcFile, final File destFile, final OnReplaceListener listener, final boolean isMove) { + if (srcFile == null || destFile == null || listener == null) return false; + // 如果源文件和目标文件相同则返回 false + if (srcFile.equals(destFile)) return false; + // 源文件不存在或者不是文件则返回 false + if (!srcFile.exists() || !srcFile.isFile()) return false; + if (destFile.exists()) { // 目标文件存在 + if (listener.onReplace()) { // 需要覆盖则删除旧文件 + if (!destFile.delete()) { // 删除文件失败的话返回 false + return false; + } + } else { // 不需要覆盖直接返回即可 true + return true; + } + } + // 目标目录不存在返回 false + if (!createOrExistsDir(destFile.getParentFile())) return false; + try { + return FileIOUtils.writeFileFromIS(destFile, new FileInputStream(srcFile), false) && !(isMove && !deleteFile(srcFile)); + } catch (FileNotFoundException e) { + ALog.eTag(TAG, e, "copyOrMoveFile"); + return false; + } + } + + /** + * 复制目录 + * @param srcDirPath 源目录路径 + * @param destDirPath 目标目录路径 + * @param listener 是否覆盖监听器 + * @return {@code true} 复制成功, {@code false} 复制失败 + */ + public static boolean copyDir(final String srcDirPath, final String destDirPath, final OnReplaceListener listener) { + return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener); + } + + /** + * 复制目录 + * @param srcDir 源目录 + * @param destDir 目标目录 + * @param listener 是否覆盖监听器 + * @return {@code true} 复制成功, {@code false} 复制失败 + */ + public static boolean copyDir(final File srcDir, final File destDir, final OnReplaceListener listener) { + return copyOrMoveDir(srcDir, destDir, listener, false); + } + + /** + * 复制文件 + * @param srcFilePath 源文件路径 + * @param destFilePath 目标文件路径 + * @param listener 是否覆盖监听器 + * @return {@code true} 复制成功, {@code false} 复制失败 + */ + public static boolean copyFile(final String srcFilePath, final String destFilePath, final OnReplaceListener listener) { + return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener); + } + + /** + * 复制文件 + * @param srcFile 源文件 + * @param destFile 目标文件 + * @param listener 是否覆盖监听器 + * @return {@code true} 复制成功, {@code false} 复制失败 + */ + public static boolean copyFile(final File srcFile, final File destFile, final OnReplaceListener listener) { + return copyOrMoveFile(srcFile, destFile, listener, false); + } + + /** + * 移动目录 + * @param srcDirPath 源目录路径 + * @param destDirPath 目标目录路径 + * @param listener 是否覆盖监听器 + * @return {@code true} 移动成功, {@code false} 移动失败 + */ + public static boolean moveDir(final String srcDirPath, final String destDirPath, final OnReplaceListener listener) { + return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener); + } + + /** + * 移动目录 + * @param srcDir 源目录 + * @param destDir 目标目录 + * @param listener 是否覆盖监听器 + * @return {@code true} 移动成功, {@code false} 移动失败 + */ + public static boolean moveDir(final File srcDir, final File destDir, final OnReplaceListener listener) { + return copyOrMoveDir(srcDir, destDir, listener, true); + } + + /** + * 移动文件 + * @param srcFilePath 源文件路径 + * @param destFilePath 目标文件路径 + * @param listener 是否覆盖监听器 + * @return {@code true} 移动成功, {@code false} 移动失败 + */ + public static boolean moveFile(final String srcFilePath, final String destFilePath, final OnReplaceListener listener) { + return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener); + } + + /** + * 移动文件 + * @param srcFile 源文件 + * @param destFile 目标文件 + * @param listener 是否覆盖监听器 + * @return {@code true} 移动成功, {@code false} 移动失败 + */ + public static boolean moveFile(final File srcFile, final File destFile, final OnReplaceListener listener) { + return copyOrMoveFile(srcFile, destFile, listener, true); + } + + /** + * 删除目录 + * @param dirPath 目录路径 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteDir(final String dirPath) { + return deleteDir(getFileByPath(dirPath)); + } + + /** + * 删除目录 + * @param dir 目录 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteDir(final File dir) { + if (dir == null) return false; + // dir doesn't exist then return true + if (!dir.exists()) return true; + // dir isn't a directory then return false + if (!dir.isDirectory()) return false; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.isFile()) { + if (!file.delete()) return false; + } else if (file.isDirectory()) { + if (!deleteDir(file)) return false; + } + } + } + return dir.delete(); + } + + /** + * 删除目录下所有东西 + * @param dirPath 目录路径 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteAllInDir(final String dirPath) { + return deleteAllInDir(getFileByPath(dirPath)); + } + + /** + * 删除目录下所有东西 + * @param dir 目录 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteAllInDir(final File dir) { + return deleteFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }); + } + + /** + * 删除目录下所有文件 + * @param dirPath 目录路径 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteFilesInDir(final String dirPath) { + return deleteFilesInDir(getFileByPath(dirPath)); + } + + /** + * 删除目录下所有文件 + * @param dir 目录 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteFilesInDir(final File dir) { + return deleteFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isFile(); + } + }); + } + + /** + * 删除目录下所有过滤的文件 + * @param dirPath 目录路径 + * @param filter 过滤器 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteFilesInDirWithFilter(final String dirPath, final FileFilter filter) { + return deleteFilesInDirWithFilter(getFileByPath(dirPath), filter); + } + + /** + * 删除目录下所有过滤的文件 + * @param dir 目录 + * @param filter 过滤器 + * @return {@code true} 删除成功, {@code false} 删除失败 + */ + public static boolean deleteFilesInDirWithFilter(final File dir, final FileFilter filter) { + if (filter == null) return false; + // dir is null then return false + if (dir == null) return false; + // dir doesn't exist then return true + if (!dir.exists()) return true; + // dir isn't a directory then return false + if (!dir.isDirectory()) return false; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (filter.accept(file)) { + if (file.isFile()) { + if (!file.delete()) return false; + } else if (file.isDirectory()) { + if (!deleteDir(file)) return false; + } + } + } + } + return true; + } + + /** + * 获取目录下所有文件 - 不递归进子目录 + * @param dirPath 目录路径 + * @return 文件链表 + */ + public static List listFilesInDir(final String dirPath) { + return listFilesInDir(dirPath, false); + } + + /** + * 获取目录下所有文件 - 不递归进子目录 + * @param dir 目录 + * @return 文件链表 + */ + public static List listFilesInDir(final File dir) { + return listFilesInDir(dir, false); + } + + /** + * 获取目录下所有文件 + * @param dirPath 目录路径 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDir(final String dirPath, final boolean isRecursive) { + return listFilesInDir(getFileByPath(dirPath), isRecursive); + } + + /** + * 获取目录下所有文件 + * @param dir 目录 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDir(final File dir, final boolean isRecursive) { + return listFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }, isRecursive); + } + + /** + * 获取目录下所有过滤的文件 - 不递归进子目录 + * @param dirPath 目录路径 + * @param filter 过滤器 + * @return 文件链表 + */ + public static List listFilesInDirWithFilter(final String dirPath, final FileFilter filter) { + return listFilesInDirWithFilter(getFileByPath(dirPath), filter, false); + } + + /** + * 获取目录下所有过滤的文件 - 不递归进子目录 + * @param dir 目录 + * @param filter 过滤器 + * @return 文件链表 + */ + public static List listFilesInDirWithFilter(final File dir, final FileFilter filter) { + return listFilesInDirWithFilter(dir, filter, false); + } + + /** + * 获取目录下所有过滤的文件 + * @param dirPath 目录路径 + * @param filter 过滤器 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirWithFilter(final String dirPath, final FileFilter filter, final boolean isRecursive) { + return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive); + } + + /** + * 获取目录下所有过滤的文件 + * @param dir 目录 + * @param filter 过滤器 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirWithFilter(final File dir, final FileFilter filter, final boolean isRecursive) { + if (!isDirectory(dir) || filter == null) return null; + List list = new ArrayList<>(); + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (filter.accept(file)) { + list.add(file); + } + if (isRecursive && file.isDirectory()) { + list.addAll(listFilesInDirWithFilter(file, filter, true)); + } + } + } + return list; + } + + // = + + /** + * detail: 文件列表 + * @author Ttt + */ + public static class FileList { + + // 当前文件夹 + private File mFile; + // 文件夹内子文件列表 + private List mSubFiles; + + /** + * 构造函数 + * @param file 当前文件夹 + */ + public FileList(File file) { + this(file, new ArrayList<>(0)); + } + + /** + * 构造函数 + * @param file 当前文件夹 + * @param lists 文件夹内子文件列表 + */ + public FileList(File file, List lists) { + this.mFile = file; + this.mSubFiles = lists; + } + + // = + + /** + * 获取当前文件夹 + * @return {@link File} + */ + public File getFile() { + return mFile; + } + + /** + * 获取文件夹内子文件列表 + * @return {@link ArrayList} + */ + public List getSubFiles() { + return mSubFiles; + } + } + + // = + + /** + * 获取目录下所有文件 - 不递归进子目录 + * @param dirPath 目录路径 + * @return 文件链表 + */ + public static List listFilesInDirBean(final String dirPath) { + return listFilesInDirBean(dirPath, false); + } + + /** + * 获取目录下所有文件 - 不递归进子目录 + * @param dir 目录 + * @return 文件链表 + */ + public static List listFilesInDirBean(final File dir) { + return listFilesInDirBean(dir, false); + } + + /** + * 获取目录下所有文件 + * @param dirPath 目录路径 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirBean(final String dirPath, final boolean isRecursive) { + return listFilesInDirBean(getFileByPath(dirPath), isRecursive); + } + + /** + * 获取目录下所有文件 + * @param dir 目录 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirBean(final File dir, final boolean isRecursive) { + return listFilesInDirWithFilterBean(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }, isRecursive); + } + + /** + * 获取目录下所有过滤的文件 - 不递归进子目录 + * @param dirPath 目录路径 + * @param filter 过滤器 + * @return 文件链表 + */ + public static List listFilesInDirWithFilterBean(final String dirPath, final FileFilter filter) { + return listFilesInDirWithFilterBean(getFileByPath(dirPath), filter, false); + } + + /** + * 获取目录下所有过滤的文件 - 不递归进子目录 + * @param dir 目录 + * @param filter 过滤器 + * @return 文件链表 + */ + public static List listFilesInDirWithFilterBean(final File dir, final FileFilter filter) { + return listFilesInDirWithFilterBean(dir, filter, false); + } + + /** + * 获取目录下所有过滤的文件 + * @param dirPath 目录路径 + * @param filter 过滤器 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirWithFilterBean(final String dirPath, final FileFilter filter, final boolean isRecursive) { + return listFilesInDirWithFilterBean(getFileByPath(dirPath), filter, isRecursive); + } + + /** + * 获取目录下所有过滤的文件 + * @param dir 目录 + * @param filter 过滤器 + * @param isRecursive 是否递归进子目录 + * @return 文件链表 + */ + public static List listFilesInDirWithFilterBean(final File dir, final FileFilter filter, final boolean isRecursive) { + if (!isDirectory(dir) || filter == null) return null; + List list = new ArrayList<>(); + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (filter.accept(file)) { + FileList fileList; + if (isRecursive && file.isDirectory()) { + List subs = listFilesInDirWithFilterBean(file, filter, true); + fileList = new FileList(file, subs); + } else { + fileList = new FileList(file); + } + list.add(fileList); + } + } + } + return list; + } + + // ================ + // = 图片类型判断 = + // ================ + + // 图片格式 + private static final String[] IMAGE_FORMATS = new String[]{".PNG", ".JPG", ".JPEG", ".BMP", ".GIF", ".WEBP"}; + + /** + * 根据文件名判断文件是否为图片 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isImageFormats(final File file) { + return file != null && isImageFormats(file.getPath(), IMAGE_FORMATS); + } + + /** + * 根据文件名判断文件是否为图片 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isImageFormats(final String filePath) { + return isImageFormats(filePath, IMAGE_FORMATS); + } + + /** + * 根据文件名判断文件是否为图片 + * @param filePath 文件路径 + * @param fileFormats 文件格式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isImageFormats(final String filePath, final String[] fileFormats) { + return isFileFormats(filePath, fileFormats); + } + + // ================ + // = 音频类型判断 = + // ================ + + // 音频格式 + private static final String[] AUDIO_FORMATS = new String[]{".MP3", ".AAC", ".OGG", ".WMA", ".APE", ".FLAC", ".RA"}; + + /** + * 根据文件名判断文件是否为音频 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAudioFormats(final File file) { + return file != null && isAudioFormats(file.getPath(), AUDIO_FORMATS); + } + + /** + * 根据文件名判断文件是否为音频 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAudioFormats(final String filePath) { + return isAudioFormats(filePath, AUDIO_FORMATS); + } + + /** + * 根据文件名判断文件是否为音频 + * @param filePath 文件路径 + * @param fileFormats 文件格式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isAudioFormats(final String filePath, final String[] fileFormats) { + return isFileFormats(filePath, fileFormats); + } + + // ================ + // = 视频类型判断 = + // ================ + + // 视频格式 + private static final String[] VIDEO_FORMATS = new String[]{".MP4", ".AVI", ".MOV", ".ASF", ".MPG", ".MPEG", ".WMV", ".RM", ".RMVB", ".3GP", ".MKV"}; + + /** + * 根据文件名判断文件是否为视频 + * @param file 文件 + * @return {@code true} yes, {@code false} no + */ + public static boolean isVideoFormats(final File file) { + return file != null && isVideoFormats(file.getPath(), VIDEO_FORMATS); + } + + /** + * 根据文件名判断文件是否为视频 + * @param filePath 文件路径 + * @return {@code true} yes, {@code false} no + */ + public static boolean isVideoFormats(final String filePath) { + return isVideoFormats(filePath, VIDEO_FORMATS); + } + + /** + * 根据文件名判断文件是否为视频 + * @param filePath 文件路径 + * @param fileFormats 文件格式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isVideoFormats(final String filePath, final String[] fileFormats) { + return isFileFormats(filePath, fileFormats); + } + + // = + + /** + * 根据文件名判断文件是否为指定格式 + * @param file 文件 + * @param fileFormats 文件格式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFileFormats(final File file, final String[] fileFormats) { + return file != null && isFileFormats(file.getPath(), fileFormats); + } + + /** + * 根据文件名判断文件是否为指定格式 + * @param filePath 文件路径 + * @param fileFormats 文件格式 + * @return {@code true} yes, {@code false} no + */ + public static boolean isFileFormats(final String filePath, final String[] fileFormats) { + if (filePath == null || fileFormats == null || fileFormats.length == 0) return false; + String path = filePath.toUpperCase(); + for (String format : fileFormats) { + if (format != null) { + if (path.endsWith(format.toUpperCase())) { + return true; + } + } + } + return false; + } + + // ============ + // = MD5Utils = + // ============ + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值 + */ + public static byte[] getFileMD5(final String filePath) { + return MD5Utils.getFileMD5(filePath); + } + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String getFileMD5ToHexString(final String filePath) { + return MD5Utils.getFileMD5ToHexString(filePath); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String getFileMD5ToHexString(final File file) { + return MD5Utils.getFileMD5ToHexString(file); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值 byte[] + */ + public static byte[] getFileMD5(final File file) { + return MD5Utils.getFileMD5(file); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/GlideImageLoader.java b/app/src/main/java/com/example/baseframe/utils/GlideImageLoader.java new file mode 100644 index 0000000..543c712 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/GlideImageLoader.java @@ -0,0 +1,25 @@ +package com.example.baseframe.utils; + + +import android.content.Context; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.example.baseframe.weight.banner.loader.ImageLoader; + + +public class GlideImageLoader extends ImageLoader { + @Override + public void displayImage(Context context, Object path, ImageView imageView) { + //具体方法内容自己去选择,次方法是为了减少banner过多的依赖第三方包,所以将这个权限开放给使用者去选择 + Glide.with(context.getApplicationContext()) + .load(path) + .into(imageView); + } + + /* @Override + public ImageView createImageView(Context context) { + //圆角 + return new RoundImageView(context); + }*/ +} diff --git a/app/src/main/java/com/example/baseframe/utils/GsonUtil.java b/app/src/main/java/com/example/baseframe/utils/GsonUtil.java new file mode 100644 index 0000000..f950205 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/GsonUtil.java @@ -0,0 +1,154 @@ +package com.example.baseframe.utils; + +import android.text.TextUtils; + +import com.blankj.ALog; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Gson 工具类 + */ +public class GsonUtil { + private static String TAG = GsonUtil.class.getSimpleName(); + private static Gson gson = new Gson(); + + /** + * 把一个map变成json字符串 + * + * @param map + * @return + */ + public static String parseMapToJson(Map map) { + try { + return gson.toJson(map); + } catch (Exception e) { + } + return null; + } + + /** + * 把一个json字符串变成对象 + * + * @param json + * @param cls + * @return + */ + public static T parseJsonToBean(String json, Class cls) { + T t = null; + try { + t = gson.fromJson(json, cls); + } catch (Exception e) { + + } + return t; + } + + /** + * 把json字符串变成map + * + * @param json + * @return + */ + public static HashMap parseJsonToMap(String json) { + Type type = new TypeToken>() { + }.getType(); + HashMap map = null; + try { + map = gson.fromJson(json, type); + } catch (Exception e) { + ALog.v(TAG, "转换异常" + e.getMessage()); + + } + return map; + } + + /** + * 把json字符串变成集合 + * params: new TypeToken>(){}.getType(), + * + * @param json + * @param type new TypeToken>(){}.getType() + * @return + */ + public static List parseJsonToList(String json, Type type) { + List list = gson.fromJson(json, type); + return list; + } + + /** + * 把 list 转成 Strin json + * + * @param list + * @return + */ + public static String parseListToJson(List list) { + String json = gson.toJson(list); + return json; + } + + /** + * 获取json串中某个字段的值,注意,只能获取同一层级的value + * + * @param json + * @param key + * @return + */ + public static String getFieldValue(String json, String key) { + if (TextUtils.isEmpty(json)) + return null; + if (!json.contains(key)) + return ""; + JSONObject jsonObject = null; + String value = null; + try { + jsonObject = new JSONObject(json); + value = jsonObject.getString(key); + } catch (JSONException e) { + e.printStackTrace(); + } + return value; + } + + /** + * 格式化json + * + * @param uglyJSONString + * @return + */ + public static String jsonFormatter(String uglyJSONString) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonParser jp = new JsonParser(); + JsonElement je = jp.parse(uglyJSONString); + String prettyJsonString = gson.toJson(je); + return prettyJsonString; + } + + + public static Map getJsonToMap(String json) { + return gson.fromJson(json, new TypeToken>() { + }.getType()); + } + + /** + * 通过 bean 返回 json + * + * @param bean + * @return + */ + public static String getBeanToJson(Object bean) { + Gson gson = new Gson(); + return gson.toJson(bean); + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/HandlerUtil.java b/app/src/main/java/com/example/baseframe/utils/HandlerUtil.java new file mode 100644 index 0000000..d809c8b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/HandlerUtil.java @@ -0,0 +1,24 @@ +package com.example.baseframe.utils; + +import android.os.Handler; +import android.os.Looper; + +/** + * @author yeziheng + * @date 2016-02-14 + */ +public class HandlerUtil { + public static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + public static void runOnUiThread(Runnable runnable){ + HANDLER.post(runnable); + } + + public static void runOnUiThreadDelay(Runnable runnable, long delayMillis){ + HANDLER.postDelayed(runnable,delayMillis); + } + + public static void removeRunable(Runnable runnable){ + HANDLER.removeCallbacks(runnable); + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/ImageUtils.java b/app/src/main/java/com/example/baseframe/utils/ImageUtils.java new file mode 100644 index 0000000..6392c33 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ImageUtils.java @@ -0,0 +1,954 @@ +package com.example.baseframe.utils; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader.TileMode; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + + +/** + * Created by goldze on 2017/7/17. + * 图片相关工具类,包含图片压缩,图片缩放,图片裁剪等功能 + */ +public class ImageUtils { + + private static final float MAX_SIZE = 200;//接受的最大图片尺寸为200k,200k以上的图片压缩到200k一下 + + public final static String SDCARD_MNT = "/mnt/sdcard"; + public final static String SDCARD = "/sdcard"; + + /** 请求相册 */ + public static final int REQUEST_CODE_GETIMAGE_BYSDCARD = 0; + + public static final int REQUEST_CODE_GETIMAGE_BYSDCARD_info = 4; + + /** 请求相机 */ + public static final int REQUEST_CODE_GETIMAGE_BYCAMERA = 1; + /** 请求裁剪 */ + public static final int REQUEST_CODE_GETIMAGE_BYCROP = 2; + /** 从图片浏览界面发送动弹 */ + public static final int REQUEST_CODE_GETIMAGE_IMAGEPAVER = 3; + + /** + * 写图片文件 在Android系统中,文件保存在 /data/data/PACKAGE_NAME/files 目录下 + * + * @throws IOException + */ + public static void saveImage(Context context, String fileName, Bitmap bitmap) + throws IOException { + saveImage(context, fileName, bitmap, 100); + } + + public static void saveImage(Context context, String fileName, + Bitmap bitmap, int quality) throws IOException { + if (bitmap == null || fileName == null || context == null) + return; + + FileOutputStream fos = context.openFileOutput(fileName, + Context.MODE_PRIVATE); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(CompressFormat.JPEG, quality, stream); + byte[] bytes = stream.toByteArray(); + fos.write(bytes); + fos.close(); + } + + /** + * 写图片文件到SD卡 + * + * @throws IOException + */ + public static void saveImageToSD(Context ctx, String filePath, + Bitmap bitmap, int quality) throws IOException { + if (bitmap != null) { + File file = new File(filePath.substring(0, + filePath.lastIndexOf(File.separator))); + if (!file.exists()) { + file.mkdirs(); + } + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(filePath)); + bitmap.compress(CompressFormat.JPEG, quality, bos); + bos.flush(); + bos.close(); + if (ctx != null) { + scanPhoto(ctx, filePath); + } + } + } + + public static void saveBackgroundImage(Context ctx, String filePath, + Bitmap bitmap, int quality) throws IOException { + if (bitmap != null) { + File file = new File(filePath.substring(0, + filePath.lastIndexOf(File.separator))); + if (!file.exists()) { + file.mkdirs(); + } + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(filePath)); + bitmap.compress(CompressFormat.PNG, quality, bos); + bos.flush(); + bos.close(); + if (ctx != null) { + scanPhoto(ctx, filePath); + } + } + } + + /** + * 让Gallery上能马上看到该图片 + */ + private static void scanPhoto(Context ctx, String imgFileName) { + Intent mediaScanIntent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + File file = new File(imgFileName); + Uri contentUri = Uri.fromFile(file); + mediaScanIntent.setData(contentUri); + ctx.sendBroadcast(mediaScanIntent); + } + + /** + * 获取bitmap + * + * @param context + * @param fileName + * @return + */ + public static Bitmap getBitmap(Context context, String fileName) { + FileInputStream fis = null; + Bitmap bitmap = null; + try { + fis = context.openFileInput(fileName); + bitmap = BitmapFactory.decodeStream(fis); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } finally { + try { + fis.close(); + } catch (Exception e) { + } + } + return bitmap; + } + + /** + * 获取bitmap + * + * @param filePath + * @return + */ + public static Bitmap getBitmapByPath(String filePath) { + return getBitmapByPath(filePath, null); + } + + public static Bitmap getBitmapByPath(String filePath, + BitmapFactory.Options opts) { + FileInputStream fis = null; + Bitmap bitmap = null; + try { + File file = new File(filePath); + fis = new FileInputStream(file); + bitmap = BitmapFactory.decodeStream(fis, null, opts); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } finally { + try { + fis.close(); + } catch (Exception e) { + } + } + return bitmap; + } + + /** + * 获取bitmap + * + * @param file + * @return + */ + public static Bitmap getBitmapByFile(File file) { + FileInputStream fis = null; + Bitmap bitmap = null; + try { + fis = new FileInputStream(file); + bitmap = BitmapFactory.decodeStream(fis); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } finally { + try { + fis.close(); + } catch (Exception e) { + } + } + return bitmap; + } + + /** + * 使用当前时间戳拼接一个唯一的文件名 + * + * @param + * @return + */ + public static String getTempFileName() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SS"); + String fileName = format.format(new Timestamp(System + .currentTimeMillis())); + return fileName; + } + + /** + * 获取照相机使用的目录 + * + * @return + */ + public static String getCamerPath() { + return Environment.getExternalStorageDirectory() + File.separator + + "FounderNews" + File.separator; + } + + /** + * 判断当前Url是否标准的content://样式,如果不是,则返回绝对路径 + * + * @param + * @return + */ + public static String getAbsolutePathFromNoStandardUri(Uri mUri) { + String filePath = null; + + String mUriString = mUri.toString(); + mUriString = Uri.decode(mUriString); + + String pre1 = "file://" + SDCARD + File.separator; + String pre2 = "file://" + SDCARD_MNT + File.separator; + + if (mUriString.startsWith(pre1)) { + filePath = Environment.getExternalStorageDirectory().getPath() + + File.separator + mUriString.substring(pre1.length()); + } else if (mUriString.startsWith(pre2)) { + filePath = Environment.getExternalStorageDirectory().getPath() + + File.separator + mUriString.substring(pre2.length()); + } + return filePath; + } + + /** + * 通过uri获取文件的绝对路径 + * + * @param uri + * @return + */ + @SuppressWarnings("deprecation") + public static String getAbsoluteImagePath(Activity context, Uri uri) { + String imagePath = ""; + String[] proj = { MediaStore.Images.Media.DATA }; + Cursor cursor = context.managedQuery(uri, proj, // Which columns to + // return + null, // WHERE clause; which rows to return (all rows) + null, // WHERE clause selection arguments (none) + null); // Order-by clause (ascending by name) + + if (cursor != null) { + int column_index = cursor + .getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + if (cursor.getCount() > 0 && cursor.moveToFirst()) { + imagePath = cursor.getString(column_index); + } + } + + return imagePath; + } + + /** + * 获取图片缩略图 只有Android2.1以上版本支持 + * + * @param imgName + * @param kind + * MediaStore.Images.Thumbnails.MICRO_KIND + * @return + */ + @SuppressWarnings("deprecation") + public static Bitmap loadImgThumbnail(Activity context, String imgName, + int kind) { + Bitmap bitmap = null; + + String[] proj = { MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME }; + + Cursor cursor = context.managedQuery( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj, + MediaStore.Images.Media.DISPLAY_NAME + "='" + imgName + "'", + null, null); + + if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) { + ContentResolver crThumb = context.getContentResolver(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 1; + bitmap = MediaStore.Images.Thumbnails.getThumbnail(crThumb, cursor.getInt(0), + kind, options); + } + return bitmap; + } + + public static Bitmap loadImgThumbnail(String filePath, int w, int h) { + Bitmap bitmap = getBitmapByPath(filePath); + return zoomBitmap(bitmap, w, h); + } + + /** + * 获取SD卡中最新图片路径 + * + * @return + */ + public static String getLatestImage(Activity context) { + String latestImage = null; + String[] items = { MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATA }; + Cursor cursor = context.managedQuery( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, items, null, + null, MediaStore.Images.Media._ID + " desc"); + + if (cursor != null && cursor.getCount() > 0) { + cursor.moveToFirst(); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor + .moveToNext()) { + latestImage = cursor.getString(1); + break; + } + } + + return latestImage; + } + + /** + * 计算缩放图片的宽高 + * + * @param img_size + * @param square_size + * @return + */ + public static int[] scaleImageSize(int[] img_size, int square_size) { + if (img_size[0] <= square_size && img_size[1] <= square_size) + return img_size; + double ratio = square_size + / (double) Math.max(img_size[0], img_size[1]); + return new int[] { (int) (img_size[0] * ratio), + (int) (img_size[1] * ratio) }; + } + + /** + * 创建缩略图 + * + * @param context + * @param largeImagePath + * 原始大图路径 + * @param thumbfilePath + * 输出缩略图路径 + * @param square_size + * 输出图片宽度 + * @param quality + * 输出图片质量 + * @throws IOException + */ + public static void createImageThumbnail(Context context, + String largeImagePath, String thumbfilePath, int square_size, + int quality) throws IOException { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inSampleSize = 1; + // 原始图片bitmap + Bitmap cur_bitmap = getBitmapByPath(largeImagePath, opts); + + if (cur_bitmap == null) + return; + + // 原始图片的高宽 + int[] cur_img_size = new int[] { cur_bitmap.getWidth(), + cur_bitmap.getHeight() }; + // 计算原始图片缩放后的宽高 + int[] new_img_size = scaleImageSize(cur_img_size, square_size); + // 生成缩放后的bitmap + Bitmap thb_bitmap = zoomBitmap(cur_bitmap, new_img_size[0], + new_img_size[1]); + // 生成缩放后的图片文件 + saveImageToSD(null, thumbfilePath, thb_bitmap, quality); + } + + /** + * 放大缩小图片 + * + * @param bitmap + * @param w + * @param h + * @return + */ + public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) { + Bitmap newbmp = null; + if (bitmap != null) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + Matrix matrix = new Matrix(); + float scaleWidht = ((float) w / width); + float scaleHeight = ((float) h / height); + matrix.postScale(scaleWidht, scaleHeight); + newbmp = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, + true); + } + return newbmp; + } + + public static Bitmap scaleBitmap(Bitmap bitmap) { + // 获取这个图片的宽和高 + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + // 定义预转换成的图片的宽度和高度 + int newWidth = 200; + int newHeight = 200; + // 计算缩放率,新尺寸除原始尺寸 + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + // 创建操作图片用的matrix对象 + Matrix matrix = new Matrix(); + // 缩放图片动作 + matrix.postScale(scaleWidth, scaleHeight); + // 旋转图片 动作 + // matrix.postRotate(45); + // 创建新的图片 + Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, + matrix, true); + return resizedBitmap; + } + + /** + * (缩放)重绘图片 + * + * @param context + * Activity + * @param bitmap + * @return + */ + public static Bitmap reDrawBitMap(Activity context, Bitmap bitmap) { + DisplayMetrics dm = new DisplayMetrics(); + context.getWindowManager().getDefaultDisplay().getMetrics(dm); + int rHeight = dm.heightPixels; + int rWidth = dm.widthPixels; + // float rHeight=dm.heightPixels/dm.density+0.5f; + // float rWidth=dm.widthPixels/dm.density+0.5f; + // int height=bitmap.getScaledHeight(dm); + // int width = bitmap.getScaledWidth(dm); + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + float zoomScale; + + /** 方式3 **/ + if (width >= rWidth) + zoomScale = ((float) rWidth) / width; + else + zoomScale = 1.0f; + // 创建操作图片用的matrix对象 + Matrix matrix = new Matrix(); + // 缩放图片动作 + matrix.postScale(zoomScale, zoomScale); + Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, + bitmap.getWidth(), bitmap.getHeight(), matrix, true); + return resizedBitmap; + } + + /** + * 将Drawable转化为Bitmap + * + * @param drawable + * @return + */ + public static Bitmap drawableToBitmap(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, drawable + .getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8888 + : Config.RGB_565); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + return bitmap; + + } + + /** + * 获得圆角图片的方法 + * + * @param bitmap + * @param roundPx + * 一般设成14 + * @return + */ + public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, float roundPx) { + + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), + bitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + final RectF rectF = new RectF(rect); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawRoundRect(rectF, roundPx, roundPx, paint); + + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + + /** + * 获得带倒影的图片方法 + * + * @param bitmap + * @return + */ + public static Bitmap createReflectionImageWithOrigin(Bitmap bitmap) { + final int reflectionGap = 4; + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + Matrix matrix = new Matrix(); + matrix.preScale(1, -1); + + Bitmap reflectionImage = Bitmap.createBitmap(bitmap, 0, height / 2, + width, height / 2, matrix, false); + + Bitmap bitmapWithReflection = Bitmap.createBitmap(width, + (height + height / 2), Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmapWithReflection); + canvas.drawBitmap(bitmap, 0, 0, null); + Paint deafalutPaint = new Paint(); + canvas.drawRect(0, height, width, height + reflectionGap, deafalutPaint); + + canvas.drawBitmap(reflectionImage, 0, height + reflectionGap, null); + + Paint paint = new Paint(); + LinearGradient shader = new LinearGradient(0, bitmap.getHeight(), 0, + bitmapWithReflection.getHeight() + reflectionGap, 0x70ffffff, + 0x00ffffff, TileMode.CLAMP); + paint.setShader(shader); + // Set the Transfer mode to be porter duff and destination in + paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)); + // Draw a rectangle using the paint with our linear gradient + canvas.drawRect(0, height, width, bitmapWithReflection.getHeight() + + reflectionGap, paint); + + return bitmapWithReflection; + } + + /** + * 将bitmap转化为drawable + * + * @param bitmap + * @return + */ + public static Drawable bitmapToDrawable(Bitmap bitmap) { + Drawable drawable = new BitmapDrawable(bitmap); + return drawable; + } + + /** + * 获取图片类型 + * + * @param file + * @return + */ + public static String getImageType(File file) { + if (file == null || !file.exists()) { + return null; + } + InputStream in = null; + try { + in = new FileInputStream(file); + String type = getImageType(in); + return type; + } catch (IOException e) { + return null; + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + } + } + } + + /** + * 获取图片的类型信息 + * + * @param in + * @return + * @see #getImageType(byte[]) + */ + public static String getImageType(InputStream in) { + if (in == null) { + return null; + } + try { + byte[] bytes = new byte[8]; + in.read(bytes); + return getImageType(bytes); + } catch (IOException e) { + return null; + } + } + + /** + * 获取图片的类型信息 + * + * @param bytes + * 2~8 byte at beginning of the image file + * @return image mimetype or null if the file is not image + */ + public static String getImageType(byte[] bytes) { + if (isJPEG(bytes)) { + return "image/jpeg"; + } + if (isGIF(bytes)) { + return "image/gif"; + } + if (isPNG(bytes)) { + return "image/png"; + } + if (isBMP(bytes)) { + return "application/x-bmp"; + } + return null; + } + + private static boolean isJPEG(byte[] b) { + if (b.length < 2) { + return false; + } + return (b[0] == (byte) 0xFF) && (b[1] == (byte) 0xD8); + } + + private static boolean isGIF(byte[] b) { + if (b.length < 6) { + return false; + } + return b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8' + && (b[4] == '7' || b[4] == '9') && b[5] == 'a'; + } + + private static boolean isPNG(byte[] b) { + if (b.length < 8) { + return false; + } + return (b[0] == (byte) 137 && b[1] == (byte) 80 && b[2] == (byte) 78 + && b[3] == (byte) 71 && b[4] == (byte) 13 && b[5] == (byte) 10 + && b[6] == (byte) 26 && b[7] == (byte) 10); + } + + private static boolean isBMP(byte[] b) { + if (b.length < 2) { + return false; + } + return (b[0] == 0x42) && (b[1] == 0x4d); + } + + /** + * 获取图片路径 + * + * @param uri + * @param + */ + public static String getImagePath(Uri uri, Activity context) { + + String[] projection = { MediaColumns.DATA }; + Cursor cursor = context.getContentResolver().query(uri, projection, + null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + int columIndex = cursor.getColumnIndexOrThrow(MediaColumns.DATA); + String ImagePath = cursor.getString(columIndex); + cursor.close(); + return ImagePath; + } + + return uri.toString(); + } + + static Bitmap bitmap = null; + + public static Bitmap loadPicasaImageFromGalley(final Uri uri, + final Activity context) { + + String[] projection = { MediaColumns.DATA, MediaColumns.DISPLAY_NAME }; + Cursor cursor = context.getContentResolver().query(uri, projection, + null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + + int columIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); + if (columIndex != -1) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + bitmap = MediaStore.Images.Media + .getBitmap(context.getContentResolver(), + uri); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + }).start(); + } + cursor.close(); + return bitmap; + } else + return null; + } + + /******************************************************************/ + + /** + * 压缩Bitmap,同时使用两种策略压缩,先压缩宽高,再压缩质量 + * + * @return 存储Bitmap的文件 + * @throws IOException + */ + public static File compressBitmap(String url, String storageDir, String prefix) throws IOException { + if (!TextUtils.isEmpty(url)) { + File img = new File(url); + Bitmap bitmap = revitionImageSize(img); + bitmap = compressImage(bitmap); + return convertToFile(bitmap, storageDir, prefix); + } + return null; + } + + + /** + * 使用矩阵缩放图片至期待的宽高 + * + * @param source 被缩放的图片 + * @param expectWidth 期待的宽 + * @param expectHeight 期待的高 + * @return 返回压缩后的图片 + */ + public static Bitmap zoomBitmap(Bitmap source, float expectWidth, float expectHeight) { + // 获取这个图片的宽和高 + float width = source.getWidth(); + float height = source.getHeight(); + // 创建操作图片用的matrix对象 + Matrix matrix = new Matrix(); + //默认不缩放 + float scaleWidth = 1; + float scaleHeight = 1; + // 计算宽高缩放率 + if (expectWidth < width) { + scaleWidth = ((float) expectWidth) / width; + } + if (expectHeight < height) { + scaleHeight = ((float) expectHeight) / height; + } + // 缩放图片动作 + matrix.postScale(scaleWidth, scaleHeight); + Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, (int) width, + (int) height, matrix, true); + return bitmap; + } + + //压缩图片大小 + public static Bitmap revitionImageSize(File file) throws IOException { + BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(in, null, options); + in.close(); + int i = 1; + Bitmap bitmap = null; + while (true) { + if (((options.outWidth / i) <= 600) + && ((options.outHeight / i) <= 600)) { + in = new BufferedInputStream( + new FileInputStream(file)); + options.inSampleSize = i; + options.inJustDecodeBounds = false; + bitmap = BitmapFactory.decodeStream(in, null, options); + break; + } + i += 1; + } + return bitmap; + } + + + //压缩图片质量 + public static Bitmap compressImage(Bitmap image) { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + image.compress(CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 + int offset = 100; + while (baos.toByteArray().length / 1024 > MAX_SIZE) { //循环判断如果压缩后图片是否大于200kb,大于继续压缩 + + baos.reset();//重置baos即清空baos + image.compress(CompressFormat.JPEG, offset, baos);//这里压缩options%,把压缩后的数据存放到baos中 + offset -= 10;//每次都减少10 + } + ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray()); + //把压缩后的数据baos存放到ByteArrayInputStream中 + Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片 + return bitmap; + } + + /** + * 将bitmap写入一个file中 + * + * @return 保存bitmap的file对象 + */ + public static File convertToFile(Bitmap bitmap, String storageDir, String prefix) throws IOException { + File cacheDir = checkTargetCacheDir(storageDir); + //以时间戳生成一个临时文件名称 + cacheDir = createFile(cacheDir, prefix, ".jpg"); + boolean created = false;//是否创建成功,默认没有创建 + if (!cacheDir.exists()) created = cacheDir.createNewFile(); + if (created)//将图片写入目标file,100表示不压缩,Note:png是默认忽略这个参数的 + bitmap.compress(CompressFormat.PNG, 100, new FileOutputStream(cacheDir)); + return cacheDir; + } + + /** + * 检查目标缓存目录是否存在,如果存在则返回这个目录,如果不存在则新建这个目录 + * + * @return + */ + public static File checkTargetCacheDir(String storageDir) { + + File file = null; + file = new File(storageDir); + + if (!file.exists()) { + file.mkdirs();//创建目录 + } + + if (file != null && file.exists()) + return file;//文件已经被成功创建 + else { + return null;//即时经过以上检查,文件还是没有被准确的创建 + } + } + + /** + * 根据系统时间、前缀、后缀产生一个文件 + */ + public static File createFile(File folder, String prefix, String suffix) { + if (!folder.exists() || !folder.isDirectory()) folder.mkdirs(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.CHINA); + String filename = prefix + dateFormat.format(new Date(System.currentTimeMillis())) + suffix; + return new File(folder, filename); + } + +// /** +// * android图片压缩工具 +// * 压缩多张图片 RxJava 方式 +// */ +// public static void compressWithRx(List files, Observer observer) { +// +// Luban.get(getContext()) +// .load(files) +// .putGear(Luban.THIRD_GEAR) +// .asListObservable() +// .subscribeOn(Schedulers.computation()) +// .observeOn(AndroidSchedulers.mainThread()) +// .doOnError(new Consumer() { +// @Override +// public void accept(Throwable throwable) throws Exception { +// throwable.printStackTrace(); +// } +// }) +// .onErrorResumeNext(new Function>() { +// @Override +// public ObservableSource apply(Throwable throwable) throws Exception { +// return Observable.empty(); +// } +// }) +// .subscribe(observer); +// } +// +// /** +// * android图片压缩工具 +// * 压缩单张图片 RxJava 方式 +// */ +// public static void compressWithRx(String url, Consumer consumer) { +// +// Luban.get(getContext()) +// .load(url) +// .putGear(Luban.THIRD_GEAR) +// .asObservable() +// .subscribeOn(Schedulers.computation()) +// .observeOn(AndroidSchedulers.mainThread()) +// .doOnError(new Consumer() { +// @Override +// public void accept(Throwable throwable) throws Exception { +// throwable.printStackTrace(); +// } +// }) +// .onErrorResumeNext(new Function>() { +// @Override +// public ObservableSource apply(Throwable throwable) throws Exception { +// return Observable.empty(); +// } +// }) +// .subscribe(consumer); +// } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/ImageUtilss.java b/app/src/main/java/com/example/baseframe/utils/ImageUtilss.java new file mode 100644 index 0000000..f41eae5 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ImageUtilss.java @@ -0,0 +1,2044 @@ +package com.example.baseframe.utils; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.ExifInterface; +import android.os.Build; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; + +import com.example.baseframe.api.App; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/08/12
+ *     desc  : utils about image
+ * 
+ */ +public final class ImageUtilss { + + private ImageUtilss() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Bitmap to bytes. + * + * @param bitmap The bitmap. + * @param format The format of bitmap. + * @return bytes + */ + public static byte[] bitmap2Bytes(final Bitmap bitmap, final CompressFormat format) { + if (bitmap == null) return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(format, 100, baos); + return baos.toByteArray(); + } + + /** + * Bytes to bitmap. + * + * @param bytes The bytes. + * @return bitmap + */ + public static Bitmap bytes2Bitmap(final byte[] bytes) { + return (bytes == null || bytes.length == 0) + ? null + : BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + /** + * Drawable to bitmap. + * + * @param drawable The drawable. + * @return bitmap + */ + public static Bitmap drawable2Bitmap(final Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + Bitmap bitmap; + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, + drawable.getOpacity() != PixelFormat.OPAQUE + ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + drawable.getOpacity() != PixelFormat.OPAQUE + ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + } + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** + * Bitmap to drawable. + * + * @param bitmap The bitmap. + * @return drawable + */ + public static Drawable bitmap2Drawable(final Bitmap bitmap) { + return bitmap == null ? null : new BitmapDrawable(App.getInstance().getResources(), bitmap); + } + + /** + * Drawable to bytes. + * + * @param drawable The drawable. + * @param format The format of bitmap. + * @return bytes + */ + public static byte[] drawable2Bytes(final Drawable drawable, final CompressFormat format) { + return drawable == null ? null : bitmap2Bytes(drawable2Bitmap(drawable), format); + } + + /** + * Bytes to drawable. + * + * @param bytes The bytes. + * @return drawable + */ + public static Drawable bytes2Drawable(final byte[] bytes) { + return bitmap2Drawable(bytes2Bitmap(bytes)); + } + + /** + * View to bitmap. + * + * @param view The view. + * @return bitmap + */ + public static Bitmap view2Bitmap(final View view) { + if (view == null) return null; + boolean drawingCacheEnabled = view.isDrawingCacheEnabled(); + boolean willNotCacheDrawing = view.willNotCacheDrawing(); + view.setDrawingCacheEnabled(true); + view.setWillNotCacheDrawing(false); + final Bitmap drawingCache = view.getDrawingCache(); + Bitmap bitmap; + if (null == drawingCache) { + view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + view.buildDrawingCache(); + bitmap = Bitmap.createBitmap(view.getDrawingCache()); + } else { + bitmap = Bitmap.createBitmap(drawingCache); + } + view.destroyDrawingCache(); + view.setWillNotCacheDrawing(willNotCacheDrawing); + view.setDrawingCacheEnabled(drawingCacheEnabled); + return bitmap; + } + + /** + * Return bitmap. + * + * @param file The file. + * @return bitmap + */ + public static Bitmap getBitmap(final File file) { + if (file == null) return null; + return BitmapFactory.decodeFile(file.getAbsolutePath()); + } + + /** + * Return bitmap. + * + * @param file The file. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(final File file, final int maxWidth, final int maxHeight) { + if (file == null) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + } + + /** + * Return bitmap. + * + * @param filePath The path of file. + * @return bitmap + */ + public static Bitmap getBitmap(final String filePath) { + if (isSpace(filePath)) return null; + return BitmapFactory.decodeFile(filePath); + } + + /** + * Return bitmap. + * + * @param filePath The path of file. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(final String filePath, final int maxWidth, final int maxHeight) { + if (isSpace(filePath)) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(filePath, options); + } + + /** + * Return bitmap. + * + * @param is The input stream. + * @return bitmap + */ + public static Bitmap getBitmap(final InputStream is) { + if (is == null) return null; + return BitmapFactory.decodeStream(is); + } + + /** + * Return bitmap. + * + * @param is The input stream. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(final InputStream is, final int maxWidth, final int maxHeight) { + if (is == null) return null; + byte[] bytes = input2Byte(is); + return getBitmap(bytes, 0, maxWidth, maxHeight); + } + + /** + * Return bitmap. + * + * @param data The data. + * @param offset The offset. + * @return bitmap + */ + public static Bitmap getBitmap(final byte[] data, final int offset) { + if (data.length == 0) return null; + return BitmapFactory.decodeByteArray(data, offset, data.length); + } + + /** + * Return bitmap. + * + * @param data The data. + * @param offset The offset. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(final byte[] data, + final int offset, + final int maxWidth, + final int maxHeight) { + if (data.length == 0) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, offset, data.length, options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray(data, offset, data.length, options); + } + + /** + * Return bitmap. + * + * @param resId The resource id. + * @return bitmap + */ + public static Bitmap getBitmap(@DrawableRes final int resId) { + Drawable drawable = ContextCompat.getDrawable(App.getInstance(), resId); + Canvas canvas = new Canvas(); + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** + * Return bitmap. + * + * @param resId The resource id. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(@DrawableRes final int resId, + final int maxWidth, + final int maxHeight) { + BitmapFactory.Options options = new BitmapFactory.Options(); + final Resources resources = App.getInstance().getResources(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(resources, resId, options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(resources, resId, options); + } + + /** + * Return bitmap. + * + * @param fd The file descriptor. + * @return bitmap + */ + public static Bitmap getBitmap(final FileDescriptor fd) { + if (fd == null) return null; + return BitmapFactory.decodeFileDescriptor(fd); + } + + /** + * Return bitmap. + * + * @param fd The file descriptor + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return bitmap + */ + public static Bitmap getBitmap(final FileDescriptor fd, + final int maxWidth, + final int maxHeight) { + if (fd == null) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fd, null, options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFileDescriptor(fd, null, options); + } + + /** + * Return the bitmap with the specified color. + * + * @param src The source of bitmap. + * @param color The color. + * @return the bitmap with the specified color + */ + public static Bitmap drawColor(@NonNull final Bitmap src, @ColorInt final int color) { + return drawColor(src, color, false); + } + + /** + * Return the bitmap with the specified color. + * + * @param src The source of bitmap. + * @param color The color. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the bitmap with the specified color + */ + public static Bitmap drawColor(@NonNull final Bitmap src, + @ColorInt final int color, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = recycle ? src : src.copy(src.getConfig(), true); + Canvas canvas = new Canvas(ret); + canvas.drawColor(color, PorterDuff.Mode.DARKEN); + return ret; + } + + /** + * Return the scaled bitmap. + * + * @param src The source of bitmap. + * @param newWidth The new width. + * @param newHeight The new height. + * @return the scaled bitmap + */ + public static Bitmap scale(final Bitmap src, final int newWidth, final int newHeight) { + return scale(src, newWidth, newHeight, false); + } + + /** + * Return the scaled bitmap. + * + * @param src The source of bitmap. + * @param newWidth The new width. + * @param newHeight The new height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the scaled bitmap + */ + public static Bitmap scale(final Bitmap src, + final int newWidth, + final int newHeight, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = Bitmap.createScaledBitmap(src, newWidth, newHeight, true); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the scaled bitmap + * + * @param src The source of bitmap. + * @param scaleWidth The scale of width. + * @param scaleHeight The scale of height. + * @return the scaled bitmap + */ + public static Bitmap scale(final Bitmap src, final float scaleWidth, final float scaleHeight) { + return scale(src, scaleWidth, scaleHeight, false); + } + + /** + * Return the scaled bitmap + * + * @param src The source of bitmap. + * @param scaleWidth The scale of width. + * @param scaleHeight The scale of height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the scaled bitmap + */ + public static Bitmap scale(final Bitmap src, + final float scaleWidth, + final float scaleHeight, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Matrix matrix = new Matrix(); + matrix.setScale(scaleWidth, scaleHeight); + Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the clipped bitmap. + * + * @param src The source of bitmap. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @param width The width. + * @param height The height. + * @return the clipped bitmap + */ + public static Bitmap clip(final Bitmap src, + final int x, + final int y, + final int width, + final int height) { + return clip(src, x, y, width, height, false); + } + + /** + * Return the clipped bitmap. + * + * @param src The source of bitmap. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @param width The width. + * @param height The height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the clipped bitmap + */ + public static Bitmap clip(final Bitmap src, + final int x, + final int y, + final int width, + final int height, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = Bitmap.createBitmap(src, x, y, width, height); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the skewed bitmap. + * + * @param src The source of bitmap. + * @param kx The skew factor of x. + * @param ky The skew factor of y. + * @return the skewed bitmap + */ + public static Bitmap skew(final Bitmap src, final float kx, final float ky) { + return skew(src, kx, ky, 0, 0, false); + } + + /** + * Return the skewed bitmap. + * + * @param src The source of bitmap. + * @param kx The skew factor of x. + * @param ky The skew factor of y. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the skewed bitmap + */ + public static Bitmap skew(final Bitmap src, + final float kx, + final float ky, + final boolean recycle) { + return skew(src, kx, ky, 0, 0, recycle); + } + + /** + * Return the skewed bitmap. + * + * @param src The source of bitmap. + * @param kx The skew factor of x. + * @param ky The skew factor of y. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @return the skewed bitmap + */ + public static Bitmap skew(final Bitmap src, + final float kx, + final float ky, + final float px, + final float py) { + return skew(src, kx, ky, px, py, false); + } + + /** + * Return the skewed bitmap. + * + * @param src The source of bitmap. + * @param kx The skew factor of x. + * @param ky The skew factor of y. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the skewed bitmap + */ + public static Bitmap skew(final Bitmap src, + final float kx, + final float ky, + final float px, + final float py, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Matrix matrix = new Matrix(); + matrix.setSkew(kx, ky, px, py); + Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the rotated bitmap. + * + * @param src The source of bitmap. + * @param degrees The number of degrees. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @return the rotated bitmap + */ + public static Bitmap rotate(final Bitmap src, + final int degrees, + final float px, + final float py) { + return rotate(src, degrees, px, py, false); + } + + /** + * Return the rotated bitmap. + * + * @param src The source of bitmap. + * @param degrees The number of degrees. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the rotated bitmap + */ + public static Bitmap rotate(final Bitmap src, + final int degrees, + final float px, + final float py, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + if (degrees == 0) return src; + Matrix matrix = new Matrix(); + matrix.setRotate(degrees, px, py); + Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the rotated degree. + * + * @param filePath The path of file. + * @return the rotated degree + */ + public static int getRotateDegree(final String filePath) { + try { + ExifInterface exifInterface = new ExifInterface(filePath); + int orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; + } + } catch (IOException e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Return the round bitmap. + * + * @param src The source of bitmap. + * @return the round bitmap + */ + public static Bitmap toRound(final Bitmap src) { + return toRound(src, 0, 0, false); + } + + /** + * Return the round bitmap. + * + * @param src The source of bitmap. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the round bitmap + */ + public static Bitmap toRound(final Bitmap src, final boolean recycle) { + return toRound(src, 0, 0, recycle); + } + + /** + * Return the round bitmap. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param borderColor The color of border. + * @return the round bitmap + */ + public static Bitmap toRound(final Bitmap src, + @IntRange(from = 0) int borderSize, + @ColorInt int borderColor) { + return toRound(src, borderSize, borderColor, false); + } + + /** + * Return the round bitmap. + * + * @param src The source of bitmap. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @param borderSize The size of border. + * @param borderColor The color of border. + * @return the round bitmap + */ + public static Bitmap toRound(final Bitmap src, + @IntRange(from = 0) int borderSize, + @ColorInt int borderColor, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + int width = src.getWidth(); + int height = src.getHeight(); + int size = Math.min(width, height); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Bitmap ret = Bitmap.createBitmap(width, height, src.getConfig()); + float center = size / 2f; + RectF rectF = new RectF(0, 0, width, height); + rectF.inset((width - size) / 2f, (height - size) / 2f); + Matrix matrix = new Matrix(); + matrix.setTranslate(rectF.left, rectF.top); + if (width != height) { + matrix.preScale((float) size / width, (float) size / height); + } + BitmapShader shader = new BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + shader.setLocalMatrix(matrix); + paint.setShader(shader); + Canvas canvas = new Canvas(ret); + canvas.drawRoundRect(rectF, center, center, paint); + if (borderSize > 0) { + paint.setShader(null); + paint.setColor(borderColor); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(borderSize); + float radius = center - borderSize / 2f; + canvas.drawCircle(width / 2f, height / 2f, radius, paint); + } + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the round corner bitmap. + * + * @param src The source of bitmap. + * @param radius The radius of corner. + * @return the round corner bitmap + */ + public static Bitmap toRoundCorner(final Bitmap src, final float radius) { + return toRoundCorner(src, radius, 0, 0, false); + } + + /** + * Return the round corner bitmap. + * + * @param src The source of bitmap. + * @param radius The radius of corner. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the round corner bitmap + */ + public static Bitmap toRoundCorner(final Bitmap src, + final float radius, + final boolean recycle) { + return toRoundCorner(src, radius, 0, 0, recycle); + } + + /** + * Return the round corner bitmap. + * + * @param src The source of bitmap. + * @param radius The radius of corner. + * @param borderSize The size of border. + * @param borderColor The color of border. + * @return the round corner bitmap + */ + public static Bitmap toRoundCorner(final Bitmap src, + final float radius, + @IntRange(from = 0) int borderSize, + @ColorInt int borderColor) { + return toRoundCorner(src, radius, borderSize, borderColor, false); + } + + /** + * Return the round corner bitmap. + * + * @param src The source of bitmap. + * @param radius The radius of corner. + * @param borderSize The size of border. + * @param borderColor The color of border. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the round corner bitmap + */ + public static Bitmap toRoundCorner(final Bitmap src, + final float radius, + @IntRange(from = 0) int borderSize, + @ColorInt int borderColor, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + int width = src.getWidth(); + int height = src.getHeight(); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Bitmap ret = Bitmap.createBitmap(width, height, src.getConfig()); + BitmapShader shader = new BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + paint.setShader(shader); + Canvas canvas = new Canvas(ret); + RectF rectF = new RectF(0, 0, width, height); + float halfBorderSize = borderSize / 2f; + rectF.inset(halfBorderSize, halfBorderSize); + canvas.drawRoundRect(rectF, radius, radius, paint); + if (borderSize > 0) { + paint.setShader(null); + paint.setColor(borderColor); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(borderSize); + paint.setStrokeCap(Paint.Cap.ROUND); + canvas.drawRoundRect(rectF, radius, radius, paint); + } + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the round corner bitmap with border. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param color The color of border. + * @param cornerRadius The radius of corner. + * @return the round corner bitmap with border + */ + public static Bitmap addCornerBorder(final Bitmap src, + @IntRange(from = 1) final int borderSize, + @ColorInt final int color, + @FloatRange(from = 0) final float cornerRadius) { + return addBorder(src, borderSize, color, false, cornerRadius, false); + } + + /** + * Return the round corner bitmap with border. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param color The color of border. + * @param cornerRadius The radius of corner. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the round corner bitmap with border + */ + public static Bitmap addCornerBorder(final Bitmap src, + @IntRange(from = 1) final int borderSize, + @ColorInt final int color, + @FloatRange(from = 0) final float cornerRadius, + final boolean recycle) { + return addBorder(src, borderSize, color, false, cornerRadius, recycle); + } + + /** + * Return the round bitmap with border. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param color The color of border. + * @return the round bitmap with border + */ + public static Bitmap addCircleBorder(final Bitmap src, + @IntRange(from = 1) final int borderSize, + @ColorInt final int color) { + return addBorder(src, borderSize, color, true, 0, false); + } + + /** + * Return the round bitmap with border. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param color The color of border. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the round bitmap with border + */ + public static Bitmap addCircleBorder(final Bitmap src, + @IntRange(from = 1) final int borderSize, + @ColorInt final int color, + final boolean recycle) { + return addBorder(src, borderSize, color, true, 0, recycle); + } + + /** + * Return the bitmap with border. + * + * @param src The source of bitmap. + * @param borderSize The size of border. + * @param color The color of border. + * @param isCircle True to draw circle, false to draw corner. + * @param cornerRadius The radius of corner. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the bitmap with border + */ + private static Bitmap addBorder(final Bitmap src, + @IntRange(from = 1) final int borderSize, + @ColorInt final int color, + final boolean isCircle, + final float cornerRadius, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = recycle ? src : src.copy(src.getConfig(), true); + int width = ret.getWidth(); + int height = ret.getHeight(); + Canvas canvas = new Canvas(ret); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(borderSize); + if (isCircle) { + float radius = Math.min(width, height) / 2f - borderSize / 2f; + canvas.drawCircle(width / 2f, height / 2f, radius, paint); + } else { + int halfBorderSize = borderSize >> 1; + RectF rectF = new RectF(halfBorderSize, halfBorderSize, + width - halfBorderSize, height - halfBorderSize); + canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint); + } + return ret; + } + + /** + * Return the bitmap with reflection. + * + * @param src The source of bitmap. + * @param reflectionHeight The height of reflection. + * @return the bitmap with reflection + */ + public static Bitmap addReflection(final Bitmap src, final int reflectionHeight) { + return addReflection(src, reflectionHeight, false); + } + + /** + * Return the bitmap with reflection. + * + * @param src The source of bitmap. + * @param reflectionHeight The height of reflection. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the bitmap with reflection + */ + public static Bitmap addReflection(final Bitmap src, + final int reflectionHeight, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + final int REFLECTION_GAP = 0; + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + Matrix matrix = new Matrix(); + matrix.preScale(1, -1); + Bitmap reflectionBitmap = Bitmap.createBitmap(src, 0, srcHeight - reflectionHeight, + srcWidth, reflectionHeight, matrix, false); + Bitmap ret = Bitmap.createBitmap(srcWidth, srcHeight + reflectionHeight, src.getConfig()); + Canvas canvas = new Canvas(ret); + canvas.drawBitmap(src, 0, 0, null); + canvas.drawBitmap(reflectionBitmap, 0, srcHeight + REFLECTION_GAP, null); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + LinearGradient shader = new LinearGradient( + 0, srcHeight, + 0, ret.getHeight() + REFLECTION_GAP, + 0x70FFFFFF, + 0x00FFFFFF, + Shader.TileMode.MIRROR); + paint.setShader(shader); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + canvas.drawRect(0, srcHeight + REFLECTION_GAP, srcWidth, ret.getHeight(), paint); + if (!reflectionBitmap.isRecycled()) reflectionBitmap.recycle(); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the bitmap with text watermarking. + * + * @param src The source of bitmap. + * @param content The content of text. + * @param textSize The size of text. + * @param color The color of text. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @return the bitmap with text watermarking + */ + public static Bitmap addTextWatermark(final Bitmap src, + final String content, + final int textSize, + @ColorInt final int color, + final float x, + final float y) { + return addTextWatermark(src, content, textSize, color, x, y, false); + } + + /** + * Return the bitmap with text watermarking. + * + * @param src The source of bitmap. + * @param content The content of text. + * @param textSize The size of text. + * @param color The color of text. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the bitmap with text watermarking + */ + public static Bitmap addTextWatermark(final Bitmap src, + final String content, + final float textSize, + @ColorInt final int color, + final float x, + final float y, + final boolean recycle) { + if (isEmptyBitmap(src) || content == null) return null; + Bitmap ret = src.copy(src.getConfig(), true); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Canvas canvas = new Canvas(ret); + paint.setColor(color); + paint.setTextSize(textSize); + Rect bounds = new Rect(); + paint.getTextBounds(content, 0, content.length(), bounds); + canvas.drawText(content, x, y + textSize, paint); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the bitmap with image watermarking. + * + * @param src The source of bitmap. + * @param watermark The image watermarking. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @param alpha The alpha of watermark. + * @return the bitmap with image watermarking + */ + public static Bitmap addImageWatermark(final Bitmap src, + final Bitmap watermark, + final int x, final int y, + final int alpha) { + return addImageWatermark(src, watermark, x, y, alpha, false); + } + + /** + * Return the bitmap with image watermarking. + * + * @param src The source of bitmap. + * @param watermark The image watermarking. + * @param x The x coordinate of the first pixel. + * @param y The y coordinate of the first pixel. + * @param alpha The alpha of watermark. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the bitmap with image watermarking + */ + public static Bitmap addImageWatermark(final Bitmap src, + final Bitmap watermark, + final int x, + final int y, + final int alpha, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = src.copy(src.getConfig(), true); + if (!isEmptyBitmap(watermark)) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Canvas canvas = new Canvas(ret); + paint.setAlpha(alpha); + canvas.drawBitmap(watermark, x, y, paint); + } + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the alpha bitmap. + * + * @param src The source of bitmap. + * @return the alpha bitmap + */ + public static Bitmap toAlpha(final Bitmap src) { + return toAlpha(src, false); + } + + /** + * Return the alpha bitmap. + * + * @param src The source of bitmap. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the alpha bitmap + */ + public static Bitmap toAlpha(final Bitmap src, final Boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = src.extractAlpha(); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the gray bitmap. + * + * @param src The source of bitmap. + * @return the gray bitmap + */ + public static Bitmap toGray(final Bitmap src) { + return toGray(src, false); + } + + /** + * Return the gray bitmap. + * + * @param src The source of bitmap. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the gray bitmap + */ + public static Bitmap toGray(final Bitmap src, final boolean recycle) { + if (isEmptyBitmap(src)) return null; + Bitmap ret = Bitmap.createBitmap(src.getWidth(), src.getHeight(), src.getConfig()); + Canvas canvas = new Canvas(ret); + Paint paint = new Paint(); + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + ColorMatrixColorFilter colorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix); + paint.setColorFilter(colorMatrixColorFilter); + canvas.drawBitmap(src, 0, 0, paint); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the blur bitmap fast. + *

zoom out, blur, zoom in

+ * + * @param src The source of bitmap. + * @param scale The scale(0...1). + * @param radius The radius(0...25). + * @return the blur bitmap + */ + public static Bitmap fastBlur(final Bitmap src, + @FloatRange( + from = 0, to = 1, fromInclusive = false + ) final float scale, + @FloatRange( + from = 0, to = 25, fromInclusive = false + ) final float radius) { + return fastBlur(src, scale, radius, false, false); + } + + /** + * Return the blur bitmap fast. + *

zoom out, blur, zoom in

+ * + * @param src The source of bitmap. + * @param scale The scale(0...1). + * @param radius The radius(0...25). + * @return the blur bitmap + */ + public static Bitmap fastBlur(final Bitmap src, + @FloatRange( + from = 0, to = 1, fromInclusive = false + ) final float scale, + @FloatRange( + from = 0, to = 25, fromInclusive = false + ) final float radius, + final boolean recycle) { + return fastBlur(src, scale, radius, recycle, false); + } + + /** + * Return the blur bitmap fast. + *

zoom out, blur, zoom in

+ * + * @param src The source of bitmap. + * @param scale The scale(0...1). + * @param radius The radius(0...25). + * @param recycle True to recycle the source of bitmap, false otherwise. + * @param isReturnScale True to return the scale blur bitmap, false otherwise. + * @return the blur bitmap + */ + public static Bitmap fastBlur(final Bitmap src, + @FloatRange( + from = 0, to = 1, fromInclusive = false + ) final float scale, + @FloatRange( + from = 0, to = 25, fromInclusive = false + ) final float radius, + final boolean recycle, + final boolean isReturnScale) { + if (isEmptyBitmap(src)) return null; + int width = src.getWidth(); + int height = src.getHeight(); + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); + Bitmap scaleBitmap = + Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + Canvas canvas = new Canvas(); + PorterDuffColorFilter filter = new PorterDuffColorFilter( + Color.TRANSPARENT, PorterDuff.Mode.SRC_ATOP); + paint.setColorFilter(filter); + canvas.scale(scale, scale); + canvas.drawBitmap(scaleBitmap, 0, 0, paint); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + scaleBitmap = renderScriptBlur(scaleBitmap, radius, recycle); + } else { + scaleBitmap = stackBlur(scaleBitmap, (int) radius, recycle); + } + if (scale == 1 || isReturnScale) { + if (recycle && !src.isRecycled() && scaleBitmap != src) src.recycle(); + return scaleBitmap; + } + Bitmap ret = Bitmap.createScaledBitmap(scaleBitmap, width, height, true); + if (!scaleBitmap.isRecycled()) scaleBitmap.recycle(); + if (recycle && !src.isRecycled() && ret != src) src.recycle(); + return ret; + } + + /** + * Return the blur bitmap using render script. + * + * @param src The source of bitmap. + * @param radius The radius(0...25). + * @return the blur bitmap + */ + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Bitmap renderScriptBlur(final Bitmap src, + @FloatRange( + from = 0, to = 25, fromInclusive = false + ) final float radius) { + return renderScriptBlur(src, radius, false); + } + + /** + * Return the blur bitmap using render script. + * + * @param src The source of bitmap. + * @param radius The radius(0...25). + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the blur bitmap + */ + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Bitmap renderScriptBlur(final Bitmap src, + @FloatRange( + from = 0, to = 25, fromInclusive = false + ) final float radius, + final boolean recycle) { + RenderScript rs = null; + Bitmap ret = recycle ? src : src.copy(src.getConfig(), true); + try { + rs = RenderScript.create(App.getInstance()); + rs.setMessageHandler(new RenderScript.RSMessageHandler()); + Allocation input = Allocation.createFromBitmap(rs, + ret, + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT); + Allocation output = Allocation.createTyped(rs, input.getType()); + ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + blurScript.setInput(input); + blurScript.setRadius(radius); + blurScript.forEach(output); + output.copyTo(ret); + } finally { + if (rs != null) { + rs.destroy(); + } + } + return ret; + } + + /** + * Return the blur bitmap using stack. + * + * @param src The source of bitmap. + * @param radius The radius(0...25). + * @return the blur bitmap + */ + public static Bitmap stackBlur(final Bitmap src, final int radius) { + return stackBlur(src, radius, false); + } + + /** + * Return the blur bitmap using stack. + * + * @param src The source of bitmap. + * @param radius The radius(0...25). + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the blur bitmap + */ + public static Bitmap stackBlur(final Bitmap src, int radius, final boolean recycle) { + Bitmap ret = recycle ? src : src.copy(src.getConfig(), true); + if (radius < 1) { + radius = 1; + } + int w = ret.getWidth(); + int h = ret.getHeight(); + + int[] pix = new int[w * h]; + ret.getPixels(pix, 0, w, 0, 0, w, h); + + int wm = w - 1; + int hm = h - 1; + int wh = w * h; + int div = radius + radius + 1; + + int r[] = new int[wh]; + int g[] = new int[wh]; + int b[] = new int[wh]; + int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; + int vmin[] = new int[Math.max(w, h)]; + + int divsum = (div + 1) >> 1; + divsum *= divsum; + int dv[] = new int[256 * divsum]; + for (i = 0; i < 256 * divsum; i++) { + dv[i] = (i / divsum); + } + + yw = yi = 0; + + int[][] stack = new int[div][3]; + int stackpointer; + int stackstart; + int[] sir; + int rbs; + int r1 = radius + 1; + int routsum, goutsum, boutsum; + int rinsum, ginsum, binsum; + + for (y = 0; y < h; y++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + for (i = -radius; i <= radius; i++) { + p = pix[yi + Math.min(wm, Math.max(i, 0))]; + sir = stack[i + radius]; + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + rbs = r1 - Math.abs(i); + rsum += sir[0] * rbs; + gsum += sir[1] * rbs; + bsum += sir[2] * rbs; + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + } + stackpointer = radius; + + for (x = 0; x < w; x++) { + + r[yi] = dv[rsum]; + g[yi] = dv[gsum]; + b[yi] = dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm); + } + p = pix[yw + vmin[x]]; + + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[(stackpointer) % div]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi++; + } + yw += w; + } + for (x = 0; x < w; x++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + yp = -radius * w; + for (i = -radius; i <= radius; i++) { + yi = Math.max(0, yp) + x; + + sir = stack[i + radius]; + + sir[0] = r[yi]; + sir[1] = g[yi]; + sir[2] = b[yi]; + + rbs = r1 - Math.abs(i); + + rsum += r[yi] * rbs; + gsum += g[yi] * rbs; + bsum += b[yi] * rbs; + + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + + if (i < hm) { + yp += w; + } + } + yi = x; + stackpointer = radius; + for (y = 0; y < h; y++) { + // Preserve alpha channel: ( 0xff000000 & pix[yi] ) + pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w; + } + p = x + vmin[y]; + + sir[0] = r[p]; + sir[1] = g[p]; + sir[2] = b[p]; + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[stackpointer]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi += w; + } + } + ret.setPixels(pix, 0, w, 0, 0, w, h); + return ret; + } + + /** + * Save the bitmap. + * + * @param src The source of bitmap. + * @param filePath The path of file. + * @param format The format of the image. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean save(final Bitmap src, + final String filePath, + final CompressFormat format) { + return save(src, getFileByPath(filePath), format, false); + } + + /** + * Save the bitmap. + * + * @param src The source of bitmap. + * @param file The file. + * @param format The format of the image. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean save(final Bitmap src, final File file, final CompressFormat format) { + return save(src, file, format, false); + } + + /** + * Save the bitmap. + * + * @param src The source of bitmap. + * @param filePath The path of file. + * @param format The format of the image. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean save(final Bitmap src, + final String filePath, + final CompressFormat format, + final boolean recycle) { + return save(src, getFileByPath(filePath), format, recycle); + } + + /** + * Save the bitmap. + * + * @param src The source of bitmap. + * @param file The file. + * @param format The format of the image. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean save(final Bitmap src, + final File file, + final CompressFormat format, + final boolean recycle) { + if (isEmptyBitmap(src) || !createFileByDeleteOldFile(file)) return false; + OutputStream os = null; + boolean ret = false; + try { + os = new BufferedOutputStream(new FileOutputStream(file)); + ret = src.compress(format, 100, os); + if (recycle && !src.isRecycled()) src.recycle(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (os != null) { + os.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return ret; + } + + /** + * Return whether it is a image according to the file name. + * + * @param file The file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isImage(final File file) { + if (file == null || !file.exists()) { + return false; + } + return isImage(file.getPath()); + } + + /** + * Return whether it is a image according to the file name. + * + * @param filePath The path of file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isImage(final String filePath) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + try { + Bitmap bitmap = BitmapFactory.decodeFile(filePath, options); + return options.outWidth != -1 && options.outHeight != -1; + } catch (Exception e) { + return false; + } + } + + /** + * Return the type of image. + * + * @param filePath The path of file. + * @return the type of image + */ + public static ImageType getImageType(final String filePath) { + return getImageType(getFileByPath(filePath)); + } + + /** + * Return the type of image. + * + * @param file The file. + * @return the type of image + */ + public static ImageType getImageType(final File file) { + if (file == null) return null; + InputStream is = null; + try { + is = new FileInputStream(file); + ImageType type = getImageType(is); + if (type != null) { + return type; + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + + private static ImageType getImageType(final InputStream is) { + if (is == null) return null; + try { + byte[] bytes = new byte[12]; + return is.read(bytes) != -1 ? getImageType(bytes) : null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + private static ImageType getImageType(final byte[] bytes) { + String type = bytes2HexString(bytes).toUpperCase(); + if (type.contains("FFD8FF")) { + return ImageType.TYPE_JPG; + } else if (type.contains("89504E47")) { + return ImageType.TYPE_PNG; + } else if (type.contains("47494638")) { + return ImageType.TYPE_GIF; + } else if (type.contains("49492A00") || type.contains("4D4D002A")) { + return ImageType.TYPE_TIFF; + } else if (type.contains("424D")) { + return ImageType.TYPE_BMP; + } else if (type.startsWith("52494646") && type.endsWith("57454250")) {//524946461c57000057454250-12个字节 + return ImageType.TYPE_WEBP; + } else if (type.contains("00000100") || type.contains("00000200")) { + return ImageType.TYPE_ICO; + } else { + return ImageType.TYPE_UNKNOWN; + } + } + + private static final char[] hexDigits = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + private static String bytes2HexString(final byte[] bytes) { + if (bytes == null) return ""; + int len = bytes.length; + if (len <= 0) return ""; + char[] ret = new char[len << 1]; + for (int i = 0, j = 0; i < len; i++) { + ret[j++] = hexDigits[bytes[i] >> 4 & 0x0f]; + ret[j++] = hexDigits[bytes[i] & 0x0f]; + } + return new String(ret); + } + + + private static boolean isJPEG(final byte[] b) { + return b.length >= 2 + && (b[0] == (byte) 0xFF) && (b[1] == (byte) 0xD8); + } + + private static boolean isGIF(final byte[] b) { + return b.length >= 6 + && b[0] == 'G' && b[1] == 'I' + && b[2] == 'F' && b[3] == '8' + && (b[4] == '7' || b[4] == '9') && b[5] == 'a'; + } + + private static boolean isPNG(final byte[] b) { + return b.length >= 8 + && (b[0] == (byte) 137 && b[1] == (byte) 80 + && b[2] == (byte) 78 && b[3] == (byte) 71 + && b[4] == (byte) 13 && b[5] == (byte) 10 + && b[6] == (byte) 26 && b[7] == (byte) 10); + } + + private static boolean isBMP(final byte[] b) { + return b.length >= 2 + && (b[0] == 0x42) && (b[1] == 0x4d); + } + + private static boolean isEmptyBitmap(final Bitmap src) { + return src == null || src.getWidth() == 0 || src.getHeight() == 0; + } + + /////////////////////////////////////////////////////////////////////////// + // about compress + /////////////////////////////////////////////////////////////////////////// + + /** + * Return the compressed bitmap using scale. + * + * @param src The source of bitmap. + * @param newWidth The new width. + * @param newHeight The new height. + * @return the compressed bitmap + */ + public static Bitmap compressByScale(final Bitmap src, + final int newWidth, + final int newHeight) { + return scale(src, newWidth, newHeight, false); + } + + /** + * Return the compressed bitmap using scale. + * + * @param src The source of bitmap. + * @param newWidth The new width. + * @param newHeight The new height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the compressed bitmap + */ + public static Bitmap compressByScale(final Bitmap src, + final int newWidth, + final int newHeight, + final boolean recycle) { + return scale(src, newWidth, newHeight, recycle); + } + + /** + * Return the compressed bitmap using scale. + * + * @param src The source of bitmap. + * @param scaleWidth The scale of width. + * @param scaleHeight The scale of height. + * @return the compressed bitmap + */ + public static Bitmap compressByScale(final Bitmap src, + final float scaleWidth, + final float scaleHeight) { + return scale(src, scaleWidth, scaleHeight, false); + } + + /** + * Return the compressed bitmap using scale. + * + * @param src The source of bitmap. + * @param scaleWidth The scale of width. + * @param scaleHeight The scale of height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return he compressed bitmap + */ + public static Bitmap compressByScale(final Bitmap src, + final float scaleWidth, + final float scaleHeight, + final boolean recycle) { + return scale(src, scaleWidth, scaleHeight, recycle); + } + + /** + * Return the compressed data using quality. + * + * @param src The source of bitmap. + * @param quality The quality. + * @return the compressed data using quality + */ + public static byte[] compressByQuality(final Bitmap src, + @IntRange(from = 0, to = 100) final int quality) { + return compressByQuality(src, quality, false); + } + + /** + * Return the compressed data using quality. + * + * @param src The source of bitmap. + * @param quality The quality. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the compressed data using quality + */ + public static byte[] compressByQuality(final Bitmap src, + @IntRange(from = 0, to = 100) final int quality, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + src.compress(CompressFormat.JPEG, quality, baos); + byte[] bytes = baos.toByteArray(); + if (recycle && !src.isRecycled()) src.recycle(); + return bytes; + } + + /** + * Return the compressed data using quality. + * + * @param src The source of bitmap. + * @param maxByteSize The maximum size of byte. + * @return the compressed data using quality + */ + public static byte[] compressByQuality(final Bitmap src, final long maxByteSize) { + return compressByQuality(src, maxByteSize, false); + } + + /** + * Return the compressed data using quality. + * + * @param src The source of bitmap. + * @param maxByteSize The maximum size of byte. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the compressed data using quality + */ + public static byte[] compressByQuality(final Bitmap src, + final long maxByteSize, + final boolean recycle) { + if (isEmptyBitmap(src) || maxByteSize <= 0) return new byte[0]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + src.compress(CompressFormat.JPEG, 100, baos); + byte[] bytes; + if (baos.size() <= maxByteSize) { + bytes = baos.toByteArray(); + } else { + baos.reset(); + src.compress(CompressFormat.JPEG, 0, baos); + if (baos.size() >= maxByteSize) { + bytes = baos.toByteArray(); + } else { + // find the best quality using binary search + int st = 0; + int end = 100; + int mid = 0; + while (st < end) { + mid = (st + end) / 2; + baos.reset(); + src.compress(CompressFormat.JPEG, mid, baos); + int len = baos.size(); + if (len == maxByteSize) { + break; + } else if (len > maxByteSize) { + end = mid - 1; + } else { + st = mid + 1; + } + } + if (end == mid - 1) { + baos.reset(); + src.compress(CompressFormat.JPEG, st, baos); + } + bytes = baos.toByteArray(); + } + } + if (recycle && !src.isRecycled()) src.recycle(); + return bytes; + } + + /** + * Return the compressed bitmap using sample size. + * + * @param src The source of bitmap. + * @param sampleSize The sample size. + * @return the compressed bitmap + */ + + public static Bitmap compressBySampleSize(final Bitmap src, final int sampleSize) { + return compressBySampleSize(src, sampleSize, false); + } + + /** + * Return the compressed bitmap using sample size. + * + * @param src The source of bitmap. + * @param sampleSize The sample size. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the compressed bitmap + */ + public static Bitmap compressBySampleSize(final Bitmap src, + final int sampleSize, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + src.compress(CompressFormat.JPEG, 100, baos); + byte[] bytes = baos.toByteArray(); + if (recycle && !src.isRecycled()) src.recycle(); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + } + + /** + * Return the compressed bitmap using sample size. + * + * @param src The source of bitmap. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return the compressed bitmap + */ + public static Bitmap compressBySampleSize(final Bitmap src, + final int maxWidth, + final int maxHeight) { + return compressBySampleSize(src, maxWidth, maxHeight, false); + } + + /** + * Return the compressed bitmap using sample size. + * + * @param src The source of bitmap. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the compressed bitmap + */ + public static Bitmap compressBySampleSize(final Bitmap src, + final int maxWidth, + final int maxHeight, + final boolean recycle) { + if (isEmptyBitmap(src)) return null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + src.compress(CompressFormat.JPEG, 100, baos); + byte[] bytes = baos.toByteArray(); + BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + options.inJustDecodeBounds = false; + if (recycle && !src.isRecycled()) src.recycle(); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + } + + /** + * Return the size of bitmap. + * + * @param filePath The path of file. + * @return the size of bitmap + */ + public static int[] getSize(String filePath) { + return getSize(getFileByPath(filePath)); + } + + /** + * Return the size of bitmap. + * + * @param file The file. + * @return the size of bitmap + */ + public static int[] getSize(File file) { + if (file == null) return new int[]{0, 0}; + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), opts); + return new int[]{opts.outWidth, opts.outHeight}; + } + + /** + * Return the sample size. + * + * @param options The options. + * @param maxWidth The maximum width. + * @param maxHeight The maximum height. + * @return the sample size + */ + public static int calculateInSampleSize(final BitmapFactory.Options options, + final int maxWidth, + final int maxHeight) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + while (height > maxHeight || width > maxWidth) { + height >>= 1; + width >>= 1; + inSampleSize <<= 1; + } + return inSampleSize; + } + + /////////////////////////////////////////////////////////////////////////// + // other utils methods + /////////////////////////////////////////////////////////////////////////// + + private static File getFileByPath(final String filePath) { + return isSpace(filePath) ? null : new File(filePath); + } + + private static boolean createFileByDeleteOldFile(final File file) { + if (file == null) return false; + if (file.exists() && !file.delete()) return false; + if (!createOrExistsDir(file.getParentFile())) return false; + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + private static boolean createOrExistsDir(final File file) { + return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); + } + + private static boolean isSpace(final String s) { + if (s == null) return true; + for (int i = 0, len = s.length(); i < len; ++i) { + if (!Character.isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + + private static byte[] input2Byte(final InputStream is) { + if (is == null) return null; + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + int len; + while ((len = is.read(b, 0, 1024)) != -1) { + os.write(b, 0, len); + } + return os.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public enum ImageType { + TYPE_JPG("jpg"), + + TYPE_PNG("png"), + + TYPE_GIF("gif"), + + TYPE_TIFF("tiff"), + + TYPE_BMP("bmp"), + + TYPE_WEBP("webp"), + + TYPE_ICO("ico"), + + TYPE_UNKNOWN("unknown"); + + String value; + + ImageType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/IntentUtils.java b/app/src/main/java/com/example/baseframe/utils/IntentUtils.java new file mode 100644 index 0000000..3ea344a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/IntentUtils.java @@ -0,0 +1,781 @@ +package com.example.baseframe.utils; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.RequiresPermission; +import androidx.core.content.FileProvider; + +import com.blankj.ALog; +import com.example.baseframe.api.App; + +import java.io.File; +import java.util.HashMap; +import java.util.List; + +/** + * detail: Intent 相关工具类 + * @author Ttt + *
+ *     所需权限
+ *     
+ *     
+ *     
+ * 
+ */ +public final class IntentUtils { + + private IntentUtils() { + } + + // 日志 TAG + private static final String TAG = IntentUtils.class.getSimpleName(); + + /** + * 获取 Intent + * @param intent {@link Intent} + * @param isNewTask 是否开启新的任务栈 (Context 非 Activity 则需要设置 FLAG_ACTIVITY_NEW_TASK) + * @return {@link Intent} + */ + public static Intent getIntent(final Intent intent, final boolean isNewTask) { + if (intent != null) { + return isNewTask ? intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) : intent; + } + return null; + } + + /** + * 判断 Intent 是否可用 + * @param intent {@link Intent} + * @return {@code true} yes, {@code false} no + */ + public static boolean isIntentAvailable(final Intent intent) { + if (intent != null) { + try { + return AppUtils.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; + } catch (Exception e) { + ALog.eTag(TAG, e, "isIntentAvailable"); + } + } + return false; + } + + /** + * 获取安装 APP( 支持 8.0) 的意图 + * @param filePath 文件路径 + * @return 安装 APP( 支持 8.0) 的意图 + */ + public static Intent getInstallAppIntent(final String filePath) { + return getInstallAppIntent(FileUtils.getFileByPath(filePath)); + } + + /** + * 获取安装 APP( 支持 8.0) 的意图 + * @param file 文件 + * @return 安装 APP( 支持 8.0) 的意图 + */ + public static Intent getInstallAppIntent(final File file) { + return getInstallAppIntent(file, false); + } + + /** + * 获取安装 APP( 支持 8.0) 的意图 + * @param file 文件 + * @param isNewTask 是否开启新的任务栈 + * @return 安装 APP( 支持 8.0) 的意图 + */ + public static Intent getInstallAppIntent(final File file, final boolean isNewTask) { + if (file == null) return null; + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri data; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + data = Uri.fromFile(file); + } else { + data = FileProvider.getUriForFile(App.getInstance(), App.getInstance().getPackageName() + ".fileprovider", file); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + intent.setDataAndType(data, "application/vnd.android.package-archive"); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getInstallAppIntent"); + } + return null; + } + + /** + * 获取卸载 APP 的意图 + * @param packageName 应用包名 + * @return 卸载 APP 的意图 + */ + public static Intent getUninstallAppIntent(final String packageName) { + return getUninstallAppIntent(packageName, false); + } + + /** + * 获取卸载 APP 的意图 + * @param packageName 应用包名 + * @param isNewTask 是否开启新的任务栈 + * @return 卸载 APP 的意图 + */ + public static Intent getUninstallAppIntent(final String packageName, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_DELETE); + intent.setData(Uri.parse("package:" + packageName)); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getUninstallAppIntent"); + } + return null; + } + + /** + * 获取打开 APP 的意图 + * @param packageName 应用包名 + * @return 打开 APP 的意图 + */ + public static Intent getLaunchAppIntent(final String packageName) { + return getLaunchAppIntent(packageName, false); + } + + /** + * 获取打开 APP 的意图 + * @param packageName 应用包名 + * @param isNewTask 是否开启新的任务栈 + * @return 打开 APP 的意图 + */ + public static Intent getLaunchAppIntent(final String packageName, final boolean isNewTask) { + try { + Intent intent = AppUtils.getPackageManager().getLaunchIntentForPackage(packageName); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLaunchAppIntent"); + } + return null; + } + + /** + * 获取跳转到系统设置的意图 + * @param isNewTask 是否开启新的任务栈 + * @return 跳转到系统设置的意图 + */ + public static Intent getSystemSettingIntent(final boolean isNewTask) { + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSystemSettingIntent"); + } + return null; + } + + /** + * 获取 APP 安装权限设置的意图 + * @return APP 安装权限设置的意图 + */ + public static Intent getLaunchAppInstallPermissionSettingsIntent() { + return getLaunchAppInstallPermissionSettingsIntent(AppUtils.getPackageName(), false); + } + + /** + * 获取 APP 安装权限设置的意图 + * @param packageName 应用包名 + * @return APP 安装权限设置的意图 + */ + public static Intent getLaunchAppInstallPermissionSettingsIntent(final String packageName) { + return getLaunchAppInstallPermissionSettingsIntent(packageName, false); + } + + /** + * 获取 APP 安装权限设置的意图 + * @param packageName 应用包名 + * @param isNewTask 是否开启新的任务栈 + * @return APP 安装权限设置的意图 + */ + public static Intent getLaunchAppInstallPermissionSettingsIntent(final String packageName, final boolean isNewTask) { + try { + Uri uri = Uri.parse("package:" + packageName); + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, uri); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLaunchAppInstallPermissionSettingsIntent"); + } + return null; + } + + /** + * 获取 APP 通知权限设置的意图 + * @return APP 通知权限设置的意图 + */ + public static Intent getLaunchAppNotificationSettingsIntent() { + return getLaunchAppNotificationSettingsIntent(AppUtils.getPackageName(), false); + } + + /** + * 获取 APP 通知权限设置的意图 + * @param packageName 应用包名 + * @return APP 通知权限设置的意图 + */ + public static Intent getLaunchAppNotificationSettingsIntent(final String packageName) { + return getLaunchAppNotificationSettingsIntent(packageName, false); + } + + /** + * 获取 APP 通知权限设置的意图 + * @param packageName 应用包名 + * @param isNewTask 是否开启新的任务栈 + * @return APP 通知权限设置的意图 + */ + public static Intent getLaunchAppNotificationSettingsIntent(final String packageName, final boolean isNewTask) { + try { + ApplicationInfo applicationInfo = AppUtils.getPackageInfo(packageName, 0).applicationInfo; + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + // 这种方案适用于 API 26 即 8.0 ( 含 8.0) 以上可以用 + intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, applicationInfo.uid); + // 这种方案适用于 API 21 - 25 即 5.0 - 7.1 之间的版本可以使用 + intent.putExtra("app_package", packageName); + intent.putExtra("app_uid", applicationInfo.uid); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLaunchAppNotificationSettingsIntent"); + } + return null; + } + + /** + * 获取 APP 通知使用权页面 + * @return APP 通知使用权页面 + */ + public static Intent getLaunchAppNotificationListenSettingsIntent() { + return getLaunchAppNotificationListenSettingsIntent(false); + } + + /** + * 获取 APP 通知使用权页面 + * @param isNewTask 是否开启新的任务栈 + * @return APP 通知使用权页面 + */ + public static Intent getLaunchAppNotificationListenSettingsIntent(final boolean isNewTask) { + Intent intent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS); + } else { + intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + } + return getIntent(intent, isNewTask); + } + + /** + * 获取 APP 悬浮窗口权限详情页的意图 + * @return APP 悬浮窗口权限详情页的意图 + */ + public static Intent getManageOverlayPermissionIntent() { + return getManageOverlayPermissionIntent(false); + } + + /** + * 获取 APP 悬浮窗口权限详情页的意图 + * @param isNewTask 是否开启新的任务栈 + * @return APP 悬浮窗口权限详情页的意图 + */ + public static Intent getManageOverlayPermissionIntent(final boolean isNewTask) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.setData(Uri.parse("package:" + AppUtils.getPackageName())); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getManageOverlayPermissionIntent"); + } + return null; + } + + /** + * 获取 APP 具体设置的意图 + * @return APP 具体设置的意图 + */ + public static Intent getLaunchAppDetailsSettingsIntent() { + return getLaunchAppDetailsSettingsIntent(AppUtils.getPackageName(), false); + } + + /** + * 获取 APP 具体设置的意图 + * @param packageName 应用包名 + * @return APP 具体设置的意图 + */ + public static Intent getLaunchAppDetailsSettingsIntent(final String packageName) { + return getLaunchAppDetailsSettingsIntent(packageName, false); + } + + /** + * 获取 APP 具体设置的意图 + * @param packageName 应用包名 + * @param isNewTask 是否开启新的任务栈 + * @return APP 具体设置的意图 + */ + public static Intent getLaunchAppDetailsSettingsIntent(final String packageName, final boolean isNewTask) { + try { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + packageName)); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLaunchAppDetailsSettingsIntent"); + } + return null; + } + + /** + * 获取到应用商店 APP 详情界面的意图 + * @param packageName 应用包名 + * @param marketPkg 应用商店包名, 如果为 "" 则由系统弹出应用商店列表供用户选择, 否则调转到目标市场的应用详情界面, 某些应用商店可能会失败 + * @return 到应用商店 APP 详情界面的意图 + */ + public static Intent getLaunchAppDetailIntent(final String packageName, final String marketPkg) { + return getLaunchAppDetailIntent(packageName, marketPkg, false); + } + + /** + * 获取到应用商店 APP 详情界面的意图 + * @param packageName 应用包名 + * @param marketPkg 应用商店包名, 如果为 "" 则由系统弹出应用商店列表供用户选择, 否则调转到目标市场的应用详情界面, 某些应用商店可能会失败 + * @param isNewTask 是否开启新的任务栈 + * @return 到应用商店 APP 详情界面的意图 + */ + public static Intent getLaunchAppDetailIntent(final String packageName, final String marketPkg, final boolean isNewTask) { + try { + if (TextUtils.isEmpty(packageName)) return null; + + Uri uri = Uri.parse("market://details?id=" + packageName); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + if (!TextUtils.isEmpty(marketPkg)) { + intent.setPackage(marketPkg); + } + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getLaunchAppDetailIntent"); + } + return null; + } + + // = + + /** + * 获取分享文本的意图 + * @param content 分享文本 + * @return 分享文本的意图 + */ + public static Intent getShareTextIntent(final String content) { + return getShareTextIntent(content, false); + } + + /** + * 获取分享文本的意图 + * @param content 分享文本 + * @param isNewTask 是否开启新的任务栈 + * @return 分享文本的意图 + */ + public static Intent getShareTextIntent(final String content, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, content); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getShareTextIntent"); + } + return null; + } + + /** + * 获取分享图片的意图 + * @param content 文本 + * @param imagePath 图片文件路径 + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final String imagePath) { + return getShareImageIntent(content, imagePath, false); + } + + /** + * 获取分享图片的意图 + * @param content 文本 + * @param imagePath 图片文件路径 + * @param isNewTask 是否开启新的任务栈 + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final String imagePath, final boolean isNewTask) { + try { + return getShareImageIntent(content, FileUtils.getFileByPath(imagePath), isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getShareImageIntent"); + } + return null; + } + + /** + * 获取分享图片的意图 + * @param content 文本 + * @param image 图片文件 + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final File image) { + return getShareImageIntent(content, image, false); + } + + /** + * 获取分享图片的意图 + * @param content 文本 + * @param image 图片文件 + * @param isNewTask 是否开启新的任务栈 + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final File image, final boolean isNewTask) { + try { + return getShareImageIntent(content, Uri.fromFile(image), isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getShareImageIntent"); + } + return null; + } + + /** + * 获取分享图片的意图 + * @param content 分享文本 + * @param uri 图片 uri + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final Uri uri) { + return getShareImageIntent(content, uri, false); + } + + /** + * 获取分享图片的意图 + * @param content 分享文本 + * @param uri 图片 uri + * @param isNewTask 是否开启新的任务栈 + * @return 分享图片的意图 + */ + public static Intent getShareImageIntent(final String content, final Uri uri, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, content); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.setType("image/*"); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getShareImageIntent"); + } + return null; + } + + /** + * 获取其他应用组件的意图 + * @param packageName 应用包名 + * @param className class.getCanonicalName() + * @return 其他应用组件的意图 + */ + public static Intent getComponentIntent(final String packageName, final String className) { + return getComponentIntent(packageName, className, null, false); + } + + /** + * 获取其他应用组件的意图 + * @param packageName 应用包名 + * @param className class.getCanonicalName() + * @param isNewTask 是否开启新的任务栈 + * @return 其他应用组件的意图 + */ + public static Intent getComponentIntent(final String packageName, final String className, final boolean isNewTask) { + return getComponentIntent(packageName, className, null, isNewTask); + } + + /** + * 获取其他应用组件的意图 + * @param packageName 应用包名 + * @param className class.getCanonicalName() + * @param bundle {@link Bundle} + * @return 其他应用组件的意图 + */ + public static Intent getComponentIntent(final String packageName, final String className, final Bundle bundle) { + return getComponentIntent(packageName, className, bundle, false); + } + + /** + * 获取其他应用组件的意图 + * @param packageName 应用包名 + * @param className class.getCanonicalName() + * @param bundle {@link Bundle} + * @param isNewTask 是否开启新的任务栈 + * @return 其他应用组件的意图 + */ + public static Intent getComponentIntent(final String packageName, final String className, final Bundle bundle, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + if (bundle != null) intent.putExtras(bundle); + ComponentName componentName = new ComponentName(packageName, className); + intent.setComponent(componentName); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getComponentIntent"); + } + return null; + } + + /** + * 获取关机的意图 + * @return 关机的意图 + */ + public static Intent getShutdownIntent() { + return getShutdownIntent(false); + } + + /** + * 获取关机的意图 + * @param isNewTask 是否开启新的任务栈 + * @return 关机的意图 + */ + public static Intent getShutdownIntent(final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_SHUTDOWN); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getShutdownIntent"); + } + return null; + } + + /** + * 获取跳至拨号界面意图 + * @param phoneNumber 电话号码 + * @return 跳至拨号界面意图 + */ + public static Intent getDialIntent(final String phoneNumber) { + return getDialIntent(phoneNumber, false); + } + + /** + * 获取跳至拨号界面意图 + * @param phoneNumber 电话号码 + * @param isNewTask 是否开启新的任务栈 + * @return 跳至拨号界面意图 + */ + public static Intent getDialIntent(final String phoneNumber, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber)); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDialIntent"); + } + return null; + } + + /** + * 获取拨打电话意图 + * @param phoneNumber 电话号码 + * @return 拨打电话意图 + */ + @RequiresPermission(android.Manifest.permission.CALL_PHONE) + public static Intent getCallIntent(final String phoneNumber) { + return getCallIntent(phoneNumber, false); + } + + /** + * 获取拨打电话意图 + * @param phoneNumber 电话号码 + * @param isNewTask 是否开启新的任务栈 + * @return 拨打电话意图 + */ + @RequiresPermission(android.Manifest.permission.CALL_PHONE) + public static Intent getCallIntent(final String phoneNumber, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneNumber)); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getCallIntent"); + } + return null; + } + + /** + * 获取发送短信界面的意图 + * @param phoneNumber 接收号码 + * @param content 短信内容 + * @return 发送短信界面的意图 + */ + public static Intent getSendSmsIntent(final String phoneNumber, final String content) { + return getSendSmsIntent(phoneNumber, content, false); + } + + /** + * 获取跳至发送短信界面的意图 + * @param phoneNumber 接收号码 + * @param content 短信内容 + * @param isNewTask 是否开启新的任务栈 + * @return 发送短信界面的意图 + */ + public static Intent getSendSmsIntent(final String phoneNumber, final String content, final boolean isNewTask) { + try { + Uri uri = Uri.parse("smsto:" + phoneNumber); + Intent intent = new Intent(Intent.ACTION_SENDTO, uri); + intent.putExtra("sms_body", content); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSendSmsIntent"); + } + return null; + } + + /** + * 获取拍照的意图 + * @param outUri 输出的 uri ( 保存地址 ) + * @return 拍照的意图 + */ + public static Intent getCaptureIntent(final Uri outUri) { + return getCaptureIntent(outUri, false); + } + + /** + * 获取拍照的意图 + * @param outUri 输出的 uri ( 保存地址 ) + * @param isNewTask 是否开启新的任务栈 + * @return 拍照的意图 + */ + public static Intent getCaptureIntent(final Uri outUri, final boolean isNewTask) { + try { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getCaptureIntent"); + } + return null; + } + + /** + * 获取存储访问框架的意图 + * @return 存储访问框架的意图 + */ + public static Intent getOpenDocumentIntent() { + return getOpenDocumentIntent("*/*"); + } + + /** + * 获取存储访问框架的意图 + * @param type 跳转类型 + * @return 存储访问框架的意图 + */ + public static Intent getOpenDocumentIntent(final String type) { + try { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(type); + return intent; + } catch (Exception e) { + ALog.eTag(TAG, e, "getOpenDocumentIntent"); + } + return null; + } + + /** + * 获取创建文件的意图 + *
+     *     getCreateDocumentIntent("text/plain", "foobar.txt");
+     *     getCreateDocumentIntent("image/png", "mypicture.png");
+     *     

+ * 创建后在 onActivityResult 中获取到 Uri, 对 Uri 进行读写 + *
+ * @param mimeType 资源类型 + * @param fileName 文件名 + * @return 创建文件的意图 + */ + public static Intent getCreateDocumentIntent(final String mimeType, final String fileName) { + try { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + return intent; + } catch (Exception e) { + ALog.eTag(TAG, e, "getCreateDocumentIntent"); + } + return null; + } + + /** + * 获取打开浏览器的意图 + *
+     *     Uri uri = Uri.parse("https://www.baidu.com")
+     *     如果手机本身安装了多个浏览器而又没有设置默认浏览器的话, 系统将让用户选择使用哪个浏览器来打开链接
+     * 
+ * @param uri 链接地址 + * @param isNewTask 是否开启新的任务栈 + * @return 打开浏览器的意图 + */ + public static Intent getOpenBrowserIntent(final Uri uri, final boolean isNewTask) { + return getOpenBrowserIntent(uri, null, null, isNewTask); + } + + /** + * 获取打开 Android 浏览器的意图 + * @param uri 链接地址 + * @param isNewTask 是否开启新的任务栈 + * @return 打开 Android 浏览器的意图 + */ + public static Intent getOpenAndroidBrowserIntent(final Uri uri, final boolean isNewTask) { + return getOpenBrowserIntent(uri, "com.android.browser", "com.android.browser.BrowserActivity", isNewTask); + } + + /** + * 获取打开指定浏览器的意图 + *
+     *     打开指定浏览器, 如:
+     *     intent.setClassName("com.UCMobile", "com.uc.browser.InnerUCMobile"); // 打开 UC 浏览器
+     *     intent.setClassName("com.tencent.mtt", "com.tencent.mtt.MainActivity"); // 打开 QQ 浏览器
+     *     intent.setClassName("com.android.browser", "com.android.browser.BrowserActivity"); // 系统指定浏览器
+     * 
+ * @param uri 链接地址 + * @param packageName 应用包名 + * @param className 完整类名 ( 可不传 ) + * @param isNewTask 是否开启新的任务栈 + * @return 打开指定浏览器的意图 + */ + public static Intent getOpenBrowserIntent(final Uri uri, final String packageName, final String className, final boolean isNewTask) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + if (!TextUtils.isEmpty(packageName)) { +// intent.setClassName(packageName, className); + List lists = AppUtils.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + HashMap browsers = new HashMap<>(); + for (ResolveInfo resolveInfo : lists) { + ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo != null) { // 包名, Activity Name + browsers.put(activityInfo.packageName, activityInfo.targetActivity); + } + } + if (browsers.containsKey(packageName)) { + if (TextUtils.isEmpty(className)) { + intent.setComponent(new ComponentName(packageName, browsers.get(packageName))); + } else { + intent.setComponent(new ComponentName(packageName, className)); + } + } + } + return getIntent(intent, isNewTask); + } catch (Exception e) { + ALog.eTag(TAG, e, "getOpenBrowserIntent"); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/LanguageUtil.java b/app/src/main/java/com/example/baseframe/utils/LanguageUtil.java new file mode 100644 index 0000000..8870ae0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/LanguageUtil.java @@ -0,0 +1,148 @@ +package com.example.baseframe.utils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; + +import com.blankj.ALog; + +import java.util.Locale; + +/** + * 语言工具类 + */ +public class LanguageUtil { + private static LanguageUtil instance; + public static final String spName = "LanguageSetting";//sp文件名 + private static final String LANGUAGE = "Language";//语言名key + private static final String LANGUAGETYPE = "LanguageType";//语言type key + public final static int FOLLOW_SYSTEM = 0;//系统默认语言 + public final static int SIMPLIFIED_CHINESE = 1;//中文 + public final static int ENGLISH = 2;//英文 + + private BroadcastReceiver receiver = new LocaleChangeReceiver(); + + private LanguageUtil() { + } + + public static LanguageUtil getInstance() { + //单利模式获取LanguageUtil + if (instance == null) { + synchronized (LanguageUtil.class) { + instance = new LanguageUtil(); + } + } + return instance; + } + + + /** + * 中英文切换 + * + * @param context context必须是activity的context + */ + public void changeEnglishOrChineseLanguage(Context context) { + String appLanguage = getAppLanguage(context); + + ALog.d("======================" + appLanguage); + if (appLanguage.equals("en")) { + //如果是英文的话就切换中文 + changeAppLanguage(context, SIMPLIFIED_CHINESE); + } else if (appLanguage.equals("zh")) { + //如果是中文就切换英文 + changeAppLanguage(context, ENGLISH); + } else { + //如果既不是中文也不是英文就切换中文 + changeAppLanguage(context, SIMPLIFIED_CHINESE); + } + } + + /** + * 修改语言设置 + */ + public void changeAppLanguage(Context context, int type) { + Resources resources = context.getResources(); + Configuration config = resources.getConfiguration(); + DisplayMetrics dm = resources.getDisplayMetrics(); + switch (type) { + case FOLLOW_SYSTEM: + config.locale = Locale.getDefault(); + break; + case ENGLISH: + config.locale = Locale.US; + break; + case SIMPLIFIED_CHINESE: + config.locale = Locale.SIMPLIFIED_CHINESE; + break; + } + + //注册广播 暂时不需要这么做 +// context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); + saveLanguageSetting(context, type, config.locale.getLanguage()); + resources.updateConfiguration(config, dm); + } + + /** + * 判断是否与设定的语言相同. + * + * @param context + * @return + */ + public boolean isSameWithSetting(Context context) { + return true; + } + + /** + * 设置当前的语言的自定义编号 + */ + private void saveLanguageSetting(Context context, int type, String name) { + SharedPreferences sharedPreferences = context.getSharedPreferences(spName, Context.MODE_PRIVATE); + + sharedPreferences.edit() + .putInt(LANGUAGETYPE, type) + .commit(); + + sharedPreferences.edit() + .putString(LANGUAGE, name) + .commit(); + + } + + /** + * 得到当前的语言的自定义编号 + */ + public int getAppLanguageSetting(Context context) { + return context + .getSharedPreferences(spName, Context.MODE_PRIVATE) + .getInt(LANGUAGETYPE, FOLLOW_SYSTEM); + } + + /** + * 得到当前的语言名称 + */ + public String getAppLanguage(Context context) { + SharedPreferences preferences = context.getSharedPreferences(spName, + Context.MODE_PRIVATE); + return preferences.getString(LANGUAGE, + Locale.getDefault().getLanguage()); + } + + class LocaleChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + //改变系统语言会接收到 ACTION_LOCALE_CHANGED + //改变应用内部语言不会收到 ACTION_LOCALE_CHANGED + if (!Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) { + int languageType = context.getSharedPreferences(spName, Context.MODE_PRIVATE).getInt(LANGUAGETYPE, FOLLOW_SYSTEM); +// languageType=FOLLOW_SYSTEM 表示用户重来没有设置过语言 这时候我们就要根据系统的变化来显示语言 + if (languageType == FOLLOW_SYSTEM) { + changeAppLanguage(context, languageType); + } + } + } + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/QRCodeUtil.java b/app/src/main/java/com/example/baseframe/utils/QRCodeUtil.java new file mode 100644 index 0000000..c977d46 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/QRCodeUtil.java @@ -0,0 +1,332 @@ +package com.example.baseframe.utils;//package com.example.baseframe.utils; +// +//import android.graphics.Bitmap; +//import android.graphics.Matrix; +// +//import com.google.zxing.BarcodeFormat; +//import com.google.zxing.EncodeHintType; +//import com.google.zxing.WriterException; +//import com.google.zxing.common.BitMatrix; +//import com.google.zxing.qrcode.QRCodeWriter; +//import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +// +//import java.util.Hashtable; +// +///** +// * 二维码 工具类 +// */ +//public class QRCodeUtil { +// private static int IMAGE_HALFWIDTH = 50; +// +// /** +// * 生成二维码,默认大小为500*500 +// * +// * @param text 需要生成二维码的文字、网址等 +// * @return bitmap +// */ +// public static Bitmap createQRCode(String text) { +// return createQRCode(text, 500); +// } +// +// /** +// * 生成二维码 +// * +// * @param text 文字或网址 +// * @param size 生成二维码的大小 +// * @return bitmap +// */ +// public static Bitmap createQRCode(String text, int size) { +// try { +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// int[] pixels = new int[size * size]; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (bitMatrix.get(x, y)) { +// pixels[y * size + x] = 0xff000000; +// } else { +// pixels[y * size + x] = 0xffffffff; +// } +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * bitmap的颜色代替黑色的二维码 +// * +// * @param text +// * @param size +// * @param mBitmap +// * @return +// */ +// public static Bitmap createQRCodeWithLogo2(String text, int size, Bitmap mBitmap) { +// try { +// IMAGE_HALFWIDTH = size / 10; +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// +// hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createScaledBitmap(mBitmap, size, size, false); +// +// int[] pixels = new int[size * size]; +// int color = 0xffffffff; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (bitMatrix.get(x, y)) { +// pixels[y * size + x] = mBitmap.getPixel(x, y); +// } else { +// pixels[y * size + x] = color; +// } +// +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * bitmap作为底色 +// * +// * @param text +// * @param size +// * @param mBitmap +// * @return +// */ +// public static Bitmap createQRCodeWithLogo3(String text, int size, Bitmap mBitmap) { +// try { +// IMAGE_HALFWIDTH = size / 10; +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// +// hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createScaledBitmap(mBitmap, size, size, false); +// +// int[] pixels = new int[size * size]; +// int color = 0xfff92736; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (bitMatrix.get(x, y)) { +// pixels[y * size + x] = color; +// } else { +// pixels[y * size + x] = mBitmap.getPixel(x, y) & 0x66ffffff; +// } +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 比方法2的颜色黑一些 +// * +// * @param text +// * @param size +// * @param mBitmap +// * @return +// */ +// public static Bitmap createQRCodeWithLogo4(String text, int size, Bitmap mBitmap) { +// try { +// IMAGE_HALFWIDTH = size / 10; +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// +// hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createScaledBitmap(mBitmap, size, size, false); +// +// int[] pixels = new int[size * size]; +// boolean flag = true; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (bitMatrix.get(x, y)) { +// if (flag) { +// flag = false; +// pixels[y * size + x] = 0xff000000; +// } else { +// pixels[y * size + x] = mBitmap.getPixel(x, y); +// flag = true; +// } +// } else { +// pixels[y * size + x] = 0xffffffff; +// } +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 生成带logo的二维码 +// * @param text +// * @param size +// * @param mBitmap +// * @return +// */ +// public static Bitmap createQRCodeWithLogo5(String text, int size, Bitmap mBitmap) { +// try { +// IMAGE_HALFWIDTH = size / 10; +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// +// hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createScaledBitmap(mBitmap, size, size, false); +// +// int width = bitMatrix.getWidth();//矩阵高度 +// int height = bitMatrix.getHeight();//矩阵宽度 +// int halfW = width / 2; +// int halfH = height / 2; +// +// Matrix m = new Matrix(); +// float sx = (float) 2 * IMAGE_HALFWIDTH / mBitmap.getWidth(); +// float sy = (float) 2 * IMAGE_HALFWIDTH +// / mBitmap.getHeight(); +// m.setScale(sx, sy); +// //设置缩放信息 +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createBitmap(mBitmap, 0, 0, +// mBitmap.getWidth(), mBitmap.getHeight(), m, false); +// +// int[] pixels = new int[size * size]; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (x > halfW - IMAGE_HALFWIDTH && x < halfW + IMAGE_HALFWIDTH +// && y > halfH - IMAGE_HALFWIDTH +// && y < halfH + IMAGE_HALFWIDTH) { +// //该位置用于存放图片信息 +// //记录图片每个像素信息 +// pixels[y * width + x] = mBitmap.getPixel(x - halfW +// + IMAGE_HALFWIDTH, y - halfH + IMAGE_HALFWIDTH); +// } else { +// if (bitMatrix.get(x, y)) { +// //设置二维码的颜色 +//// pixels[y * size + x] = 0xff000000; +// pixels[y * size + x] = 0xff84c832; +// } else { +// pixels[y * size + x] = 0xffffffff; +// } +// } +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 修改三个顶角颜色的,带logo的二维码 +// * @param text +// * @param size +// * @param mBitmap +// * @return +// */ +// public static Bitmap createQRCodeWithLogo6(String text, int size, Bitmap mBitmap) { +// try { +// IMAGE_HALFWIDTH = size / 10; +// Hashtable hints = new Hashtable<>(); +// hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); +// /* +// * 设置容错级别,默认为ErrorCorrectionLevel.L +// * 因为中间加入logo所以建议你把容错级别调至H,否则可能会出现识别不了 +// */ +// hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); +// BitMatrix bitMatrix = new QRCodeWriter().encode(text, +// BarcodeFormat.QR_CODE, size, size, hints); +// +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createScaledBitmap(mBitmap, size, size, false); +// +// int width = bitMatrix.getWidth();//矩阵高度 +// int height = bitMatrix.getHeight();//矩阵宽度 +// int halfW = width / 2; +// int halfH = height / 2; +// +// Matrix m = new Matrix(); +// float sx = (float) 2 * IMAGE_HALFWIDTH / mBitmap.getWidth(); +// float sy = (float) 2 * IMAGE_HALFWIDTH +// / mBitmap.getHeight(); +// m.setScale(sx, sy); +// //设置缩放信息 +// //将logo图片按martix设置的信息缩放 +// mBitmap = Bitmap.createBitmap(mBitmap, 0, 0, +// mBitmap.getWidth(), mBitmap.getHeight(), m, false); +// +// int[] pixels = new int[size * size]; +// for (int y = 0; y < size; y++) { +// for (int x = 0; x < size; x++) { +// if (x > halfW - IMAGE_HALFWIDTH && x < halfW + IMAGE_HALFWIDTH +// && y > halfH - IMAGE_HALFWIDTH +// && y < halfH + IMAGE_HALFWIDTH) { +// //该位置用于存放图片信息 +// //记录图片每个像素信息 +// pixels[y * width + x] = mBitmap.getPixel(x - halfW +// + IMAGE_HALFWIDTH, y - halfH + IMAGE_HALFWIDTH); +// } else { +// if (bitMatrix.get(x, y)) { +// pixels[y * size + x] = 0xff111111; +// if(x<115&&(y<115||y>=size-115)||(y<115&&x>=size-115)){ +// pixels[y * size + x] = 0xfff92736; +// } +// } else { +// pixels[y * size + x] = 0xffffffff; +// } +// } +// } +// } +// Bitmap bitmap = Bitmap.createBitmap(size, size, +// Bitmap.Config.ARGB_8888); +// bitmap.setPixels(pixels, 0, size, 0, 0, size, size); +// return bitmap; +// } catch (WriterException e) { +// e.printStackTrace(); +// return null; +// } +// } +//} diff --git a/app/src/main/java/com/example/baseframe/utils/ResourceUtils.java b/app/src/main/java/com/example/baseframe/utils/ResourceUtils.java new file mode 100644 index 0000000..b27a8cb --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ResourceUtils.java @@ -0,0 +1,924 @@ +package com.example.baseframe.utils; + +import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +import androidx.annotation.AnimRes; +import androidx.annotation.AnimatorRes; +import androidx.annotation.AnyRes; +import androidx.annotation.ArrayRes; +import androidx.annotation.BoolRes; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntegerRes; +import androidx.annotation.RawRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +import com.blankj.ALog; +import com.example.baseframe.api.App; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * detail: 资源文件工具类 + * @author Ttt + */ +public final class ResourceUtils { + + private ResourceUtils() { + } + + // 日志 TAG + private static final String TAG = ResourceUtils.class.getSimpleName(); + + // ================ + // = 快捷获取方法 = + // ================ + + /** + * 获取 Resources + * @return {@link Resources} + */ + public static Resources getResources() { + try { + return App.getInstance().getResources(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getResources"); + } + return null; + } + + /** + * 获取 Resources.Theme + * @return {@link Resources.Theme} + */ + public static Resources.Theme getTheme() { + try { + return App.getInstance().getTheme(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getTheme"); + } + return null; + } + + /** + * 获取 AssetManager + * @return {@link AssetManager} + */ + public static AssetManager getAssets() { + try { + return App.getInstance().getAssets(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAssets"); + } + return null; + } + + /** + * 获取 ContentResolver + * @return {@link ContentResolver} + */ + public static ContentResolver getContentResolver() { + try { + return App.getInstance().getContentResolver(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getContentResolver"); + } + return null; + } + + /** + * 获取 DisplayMetrics + * @return {@link DisplayMetrics} + */ + public static DisplayMetrics getDisplayMetrics() { + try { + return App.getInstance().getResources().getDisplayMetrics(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDisplayMetrics"); + } + return null; + } + + /** + * 获取 Configuration + * @return {@link Configuration} + */ + public static Configuration getConfiguration() { + try { + return App.getInstance().getResources().getConfiguration(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getConfiguration"); + } + return null; + } + + /** + * 获取 ColorStateList + * @param id resource identifier of a {@link ColorStateList} + * @return {@link ColorStateList} + */ + public static ColorStateList getColorStateList(@ColorRes final int id) { + try { + return ContextCompat.getColorStateList(App.getInstance(), id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getColorStateList"); + } + return null; + } + + /** + * 获取 String + * @param id R.string.id + * @return String + */ + public static String getString(@StringRes final int id) { + try { + return App.getInstance().getResources().getString(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getString"); + } + return null; + } + + /** + * 获取 String + * @param id R.string.id + * @param formatArgs 格式化参数 + * @return String + */ + public static String getString(@StringRes final int id, final Object... formatArgs) { + try { + return App.getInstance().getResources().getString(id, formatArgs); + } catch (Exception e) { + ALog.eTag(TAG, e, "getString"); + } + return null; + } + + /** + * 获取 Color + * @param colorId R.color.id + * @return Color + */ + public static int getColor(@ColorRes final int colorId) { + try { + return ContextCompat.getColor(App.getInstance(), colorId); + } catch (Exception e) { + ALog.eTag(TAG, e, "getColor"); + } + return -1; + } + + /** + * 获取 Drawable + * @param drawableId R.drawable.id + * @return {@link Drawable} + */ + public static Drawable getDrawable(@DrawableRes final int drawableId) { + try { + return ContextCompat.getDrawable(App.getInstance(), drawableId); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDrawable"); + } + return null; + } + + /** + * 获取 .9 Drawable + * @param drawableId R.drawable.id + * @return .9 {@link NinePatchDrawable} + */ + public static NinePatchDrawable getNinePatchDrawable(@DrawableRes final int drawableId) { + try { + return (NinePatchDrawable) getDrawable(drawableId); + } catch (Exception e) { + ALog.eTag(TAG, e, "getNinePatchDrawable"); + } + return null; + } + + /** + * 获取指定颜色 Drawable + * @param color 颜色值 + * @return 指定颜色 Drawable + */ + public static ColorDrawable getColorDrawable(@ColorInt final int color) { + try { + return new ColorDrawable(color); + } catch (Exception e) { + ALog.eTag(TAG, e, "getColorDrawable"); + } + return null; + } + + /** + * 获取十六进制颜色值 Drawable + * @param color 十六进制颜色值 + * @return 十六进制颜色值 Drawable + */ + public static ColorDrawable getColorDrawable(final String color) { + try { + return new ColorDrawable(Color.parseColor(color)); + } catch (Exception e) { + ALog.eTag(TAG, e, "getColorDrawable"); + } + return null; + } + + /** + * 获取 Bitmap + * @param resId resource identifier + * @return {@link Bitmap} + */ + public static Bitmap getBitmap(final int resId) { + try { + return BitmapFactory.decodeResource(App.getInstance().getResources(), resId); + } catch (Exception e) { + ALog.eTag(TAG, e, "getBitmap"); + } + return null; + } + + /** + * 获取 Bitmap + * @param resId resource identifier + * @param options {@link BitmapFactory.Options} + * @return {@link Bitmap} + */ + public static Bitmap getBitmap(final int resId, final BitmapFactory.Options options) { + try { + return BitmapFactory.decodeResource(App.getInstance().getResources(), resId, options); + } catch (Exception e) { + ALog.eTag(TAG, e, "getBitmap"); + } + return null; + } + + /** + * 获取 Dimension + * @param id resource identifier + * @return Dimension + */ + public static float getDimension(@DimenRes final int id) { + try { + return App.getInstance().getResources().getDimension(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDimension"); + } + return 0f; + } + + /** + * 获取 Boolean + * @param id resource identifier + * @return Boolean + */ + public static boolean getBoolean(@BoolRes final int id) { + try { + return App.getInstance().getResources().getBoolean(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getBoolean"); + } + return false; + } + + /** + * 获取 Integer + * @param id resource identifier + * @return Integer + */ + public static int getInteger(@IntegerRes final int id) { + try { + return App.getInstance().getResources().getInteger(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getInteger"); + } + return -1; + } + + /** + * 获取 Animation + * @param id resource identifier + * @return XmlResourceParser + */ + public static XmlResourceParser getAnimation(@AnimatorRes @AnimRes final int id) { + try { + return App.getInstance().getResources().getAnimation(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getAnimation"); + } + return null; + } + + /** + * 获取给定资源标识符的全名 + * @param id resource identifier + * @return Integer + */ + public static String getResourceName(@AnyRes final int id) { + try { + return App.getInstance().getResources().getResourceName(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getResourceName"); + } + return null; + } + + /** + * 获取 int[] + * @param id resource identifier + * @return int[] + */ + public static int[] getIntArray(@ArrayRes final int id) { + try { + return App.getInstance().getResources().getIntArray(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getIntArray"); + } + return null; + } + + /** + * 获取 String[] + * @param id resource identifier + * @return String[] + */ + public static String[] getStringArray(@ArrayRes final int id) { + try { + return App.getInstance().getResources().getStringArray(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getStringArray"); + } + return null; + } + + /** + * 获取 CharSequence[] + * @param id resource identifier + * @return CharSequence[] + */ + public static CharSequence[] getTextArray(@ArrayRes final int id) { + try { + return App.getInstance().getResources().getTextArray(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getTextArray"); + } + return null; + } + + // = + + /** + * 获取 layout id + * @param resName layout xml fileName + * @return layout id + */ + public static int getLayoutId(final String resName) { + return getIdentifier(resName, "layout"); + } + + /** + * 获取 drawable id + * @param resName drawable name + * @return drawable id + */ + public static int getDrawableId(final String resName) { + return getIdentifier(resName, "drawable"); + } + + /** + * 获取 mipmap id + * @param resName mipmap name + * @return mipmap id + */ + public static int getMipmapId(final String resName) { + return getIdentifier(resName, "mipmap"); + } + + /** + * 获取 menu id + * @param resName menu name + * @return menu id + */ + public static int getMenuId(final String resName) { + return getIdentifier(resName, "menu"); + } + + /** + * 获取 raw id + * @param resName raw name + * @return raw id + */ + public static int getRawId(final String resName) { + return getIdentifier(resName, "raw"); + } + + /** + * 获取 anim id + * @param resName anim xml fileName + * @return anim id + */ + public static int getAnimId(final String resName) { + return getIdentifier(resName, "anim"); + } + + /** + * 获取 color id + * @param resName color name + * @return color id + */ + public static int getColorId(final String resName) { + return getIdentifier(resName, "color"); + } + + /** + * 获取 dimen id + * @param resName dimen name + * @return dimen id + */ + public static int getDimenId(final String resName) { + return getIdentifier(resName, "dimen"); + } + + /** + * 获取 attr id + * @param resName attr name + * @return attr id + */ + public static int getAttrId(final String resName) { + return getIdentifier(resName, "attr"); + } + + /** + * 获取 style id + * @param resName style name + * @return style id + */ + public static int getStyleId(final String resName) { + return getIdentifier(resName, "style"); + } + + /** + * 获取 styleable id + * @param resName styleable name + * @return styleable id + */ + public static int getStyleableId(final String resName) { + return getIdentifier(resName, "styleable"); + } + + /** + * 获取 id + * @param resName id name + * @return id + */ + public static int getId(final String resName) { + return getIdentifier(resName, "id"); + } + + /** + * 获取 string id + * @param resName string name + * @return string id + */ + public static int getStringId(final String resName) { + return getIdentifier(resName, "string"); + } + + /** + * 获取 bool id + * @param resName bool name + * @return bool id + */ + public static int getBoolId(final String resName) { + return getIdentifier(resName, "bool"); + } + + /** + * 获取 integer id + * @param resName integer name + * @return integer id + */ + public static int getIntegerId(final String resName) { + return getIdentifier(resName, "integer"); + } + + /** + * 获取资源 id + * @param resName 资源名 + * @param defType 资源类型 + * @return 资源 id + */ + public static int getIdentifier(final String resName, final String defType) { + return getIdentifier(resName, defType, AppUtils.getPackageName()); + } + + /** + * 获取资源 id + * @param resName 资源名 + * @param defType 资源类型 + * @param packageName 应用包名 + * @return 资源 id + */ + public static int getIdentifier(final String resName, final String defType, final String packageName) { + try { + return App.getInstance().getResources().getIdentifier(resName, defType, packageName); + } catch (Exception e) { + ALog.eTag(TAG, e, "getIdentifier - " + resName + " " + defType + ": " + packageName); + } + return 0; + } + + // = + + /** + * 获取 AssetManager 指定资源 InputStream + * @param fileName 文件名 + * @return {@link InputStream} + */ + public static InputStream open(final String fileName) { + try { + return App.getInstance().getAssets().open(fileName); + } catch (Exception e) { + ALog.eTag(TAG, e, "open"); + } + return null; + } + + /** + * 获取 AssetManager 指定资源 AssetFileDescriptor + * @param fileName 文件名 + * @return {@link AssetFileDescriptor} + */ + public static AssetFileDescriptor openFd(final String fileName) { + try { + return App.getInstance().getAssets().openFd(fileName); + } catch (Exception e) { + ALog.eTag(TAG, e, "openFd"); + } + return null; + } + + /** + * 获取 AssetManager 指定资源 AssetFileDescriptor + * @param fileName 文件名 + * @return {@link AssetFileDescriptor} + */ + public static AssetFileDescriptor openNonAssetFd(final String fileName) { + try { + return App.getInstance().getAssets().openNonAssetFd(fileName); + } catch (Exception e) { + ALog.eTag(TAG, e, "openNonAssetFd"); + } + return null; + } + + /** + * 获取对应资源 InputStream + * @param id resource identifier + * @return {@link InputStream} + */ + public static InputStream openRawResource(@RawRes final int id) { + try { + return App.getInstance().getResources().openRawResource(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "openRawResource"); + } + return null; + } + + /** + * 获取对应资源 AssetFileDescriptor + * @param id resource identifier + * @return {@link AssetFileDescriptor} + */ + public static AssetFileDescriptor openRawResourceFd(@RawRes final int id) { + try { + return App.getInstance().getResources().openRawResourceFd(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "openRawResourceFd"); + } + return null; + } + + /** + * 获取 Uri InputStream + *
+     *     主要用于获取到分享的 FileProvider Uri 存储起来 {@link FileIOUtils#writeFileFromIS(File, InputStream)}
+     * 
+ * @param uri {@link Uri} FileProvider Uri、Content Uri、File Uri + * @return Uri InputStream + */ + public static InputStream openInputStream(final Uri uri) { + if (uri == null) return null; + try { + return ResourceUtils.getContentResolver().openInputStream(uri); + } catch (Exception e) { + ALog.eTag(TAG, e, "openInputStream " + uri.toString()); + } + return null; + } + + /** + * 获取 Uri OutputStream + * @param uri {@link Uri} FileProvider Uri、Content Uri、File Uri + * @return Uri OutputStream + */ + public static OutputStream openOutputStream(final Uri uri) { + if (uri == null) return null; + try { + return ResourceUtils.getContentResolver().openOutputStream(uri); + } catch (Exception e) { + ALog.eTag(TAG, e, "openOutputStream " + uri.toString()); + } + return null; + } + + /** + * 获取 Uri OutputStream + * @param uri {@link Uri} FileProvider Uri、Content Uri、File Uri + * @param mode 读写模式 + * @return Uri OutputStream + */ + public static OutputStream openOutputStream(final Uri uri, final String mode) { + if (uri == null) return null; + try { + return ResourceUtils.getContentResolver().openOutputStream(uri, mode); + } catch (Exception e) { + ALog.eTag(TAG, e, "openOutputStream mode: " + mode + ", " + uri.toString()); + } + return null; + } + + /** + * 获取 Uri ParcelFileDescriptor + *
+     *     通过 new FileInputStream(openFileDescriptor().getFileDescriptor()) 进行文件操作
+     * 
+ * @param uri {@link Uri} FileProvider Uri、Content Uri、File Uri + * @param mode 读写模式 + * @return Uri ParcelFileDescriptor + */ + public static ParcelFileDescriptor openFileDescriptor(final Uri uri, final String mode) { + if (uri == null || TextUtils.isEmpty(mode)) return null; + try { + return ResourceUtils.getContentResolver().openFileDescriptor(uri, mode); + } catch (Exception e) { + ALog.eTag(TAG, e, "openFileDescriptor mode: " + mode + ", " + uri.toString()); + } + return null; + } + + /** + * 获取 Uri AssetFileDescriptor + *
+     *     通过 new FileInputStream(openAssetFileDescriptor().getFileDescriptor()) 进行文件操作
+     * 
+ * @param uri {@link Uri} FileProvider Uri、Content Uri、File Uri + * @param mode 读写模式 + * @return Uri AssetFileDescriptor + */ + public static AssetFileDescriptor openAssetFileDescriptor(final Uri uri, final String mode) { + if (uri == null || TextUtils.isEmpty(mode)) return null; + try { + return ResourceUtils.getContentResolver().openAssetFileDescriptor(uri, mode); + } catch (Exception e) { + ALog.eTag(TAG, e, "openAssetFileDescriptor mode: " + mode + ", " + uri.toString()); + } + return null; + } + + // ================ + // = 读取资源文件 = + // ================ + + /** + * 获取 Assets 资源文件数据 + *
+     *     直接传入文件名、文件夹 / 文件名 等
+     *     根目录 a.txt
+     *     子目录 /www/a.html
+     * 
+ * @param fileName 文件名 + * @return 文件 byte[] 数据 + */ + public static byte[] readBytesFromAssets(final String fileName) { + InputStream is = null; + try { + is = open(fileName); + int length = is.available(); + byte[] buffer = new byte[length]; + is.read(buffer); + return buffer; + } catch (Exception e) { + ALog.eTag(TAG, e, "readBytesFromAssets"); + } finally { + CloseUtils.closeIOQuietly(is); + } + return null; + } + + /** + * 获取 Assets 资源文件数据 + * @param fileName 文件名 + * @return 文件字符串内容 + */ + public static String readStringFromAssets(final String fileName) { + try { + return new String(readBytesFromAssets(fileName), "UTF-8"); + } catch (Exception e) { + ALog.eTag(TAG, e, "readStringFromAssets"); + } + return null; + } + + // = + + /** + * 获取 Raw 资源文件数据 + * @param resId 资源 id + * @return 文件 byte[] 数据 + */ + public static byte[] readBytesFromRaw(@RawRes final int resId) { + InputStream is = null; + try { + is = openRawResource(resId); + int length = is.available(); + byte[] buffer = new byte[length]; + is.read(buffer); + return buffer; + } catch (Exception e) { + ALog.eTag(TAG, e, "readBytesFromRaw"); + } finally { + CloseUtils.closeIOQuietly(is); + } + return null; + } + + /** + * 获取 Raw 资源文件数据 + * @param resId 资源 id + * @return 文件字符串内容 + */ + public static String readStringFromRaw(@RawRes final int resId) { + try { + return new String(readBytesFromRaw(resId), "UTF-8"); + } catch (Exception e) { + ALog.eTag(TAG, e, "readStringFromRaw"); + } + return null; + } + + // = + + /** + * 获取 Assets 资源文件数据 ( 返回 List 一行的全部内容属于一个索引 ) + * @param fileName 文件名 + * @return {@link List} + */ + public static List geFileToListFromAssets(final String fileName) { + InputStream is = null; + BufferedReader br = null; + try { + is = open(fileName); + br = new BufferedReader(new InputStreamReader(is)); + + List lists = new ArrayList<>(); + String line; + while ((line = br.readLine()) != null) { + lists.add(line); + } + return lists; + } catch (Exception e) { + ALog.eTag(TAG, e, "geFileToListFromAssets"); + } finally { + CloseUtils.closeIOQuietly(is, br); + } + return null; + } + + /** + * 获取 Raw 资源文件数据 ( 返回 List 一行的全部内容属于一个索引 ) + * @param resId 资源 id + * @return {@link List} + */ + public static List geFileToListFromRaw(@RawRes final int resId) { + InputStream is = null; + BufferedReader br = null; + try { + is = openRawResource(resId); + br = new BufferedReader(new InputStreamReader(is)); + + List lists = new ArrayList<>(); + String line; + while ((line = br.readLine()) != null) { + lists.add(line); + } + return lists; + } catch (Exception e) { + ALog.eTag(TAG, e, "geFileToListFromRaw"); + } finally { + CloseUtils.closeIOQuietly(is, br); + } + return null; + } + + // = + + /** + * 获取 Assets 资源文件数据并保存到本地 + * @param fileName 文件名 + * @param file 文件保存地址 + * @return {@code true} success, {@code false} fail + */ + public static boolean saveAssetsFormFile(final String fileName, final File file) { + try { + // 获取 Assets 文件 + InputStream is = open(fileName); + // 存入 SDCard + FileOutputStream fos = new FileOutputStream(file); + // 设置数据缓冲 + byte[] buffer = new byte[1024]; + // 创建输入输出流 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + // 保存数据 + byte[] bytes = baos.toByteArray(); + // 写入保存的文件 + fos.write(bytes); + // 关闭流 + CloseUtils.closeIOQuietly(baos, is); + fos.flush(); + CloseUtils.closeIOQuietly(fos); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "saveAssetsFormFile"); + } + return false; + } + + /** + * 获取 Raw 资源文件数据并保存到本地 + * @param resId 资源 id + * @param file 文件保存地址 + * @return {@code true} success, {@code false} fail + */ + public static boolean saveRawFormFile(@RawRes final int resId, final File file) { + try { + // 获取 raw 文件 + InputStream is = openRawResource(resId); + // 存入 SDCard + FileOutputStream fos = new FileOutputStream(file); + // 设置数据缓冲 + byte[] buffer = new byte[1024]; + // 创建输入输出流 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + // 保存数据 + byte[] bytes = baos.toByteArray(); + // 写入保存的文件 + fos.write(bytes); + // 关闭流 + CloseUtils.closeIOQuietly(baos, is); + fos.flush(); + CloseUtils.closeIOQuietly(fos); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "saveRawFormFile"); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/ScreenUtils.java b/app/src/main/java/com/example/baseframe/utils/ScreenUtils.java new file mode 100644 index 0000000..c16253a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ScreenUtils.java @@ -0,0 +1,593 @@ +package com.example.baseframe.utils; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.view.Surface; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; + +import com.blankj.ALog; +import com.example.baseframe.api.App; + +import java.lang.reflect.Method; +import java.text.DecimalFormat; + +/** + * detail: 屏幕相关工具类 + * @author Ttt + *
+ *     计算屏幕尺寸
+ *     @see 
+ *     

+ * 所需权限 + * + *
+ */ +public final class ScreenUtils { + + private ScreenUtils() { + } + + // 日志 TAG + private static final String TAG = ScreenUtils.class.getSimpleName(); + + /** + * 获取 DisplayMetrics + * @return {@link DisplayMetrics} + */ + public static DisplayMetrics getDisplayMetrics() { + try { + WindowManager windowManager = AppUtils.getWindowManager(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + return displayMetrics; + } catch (Exception e) { + ALog.eTag(TAG, e, "getDisplayMetrics"); + } + return null; + } + + // ============ + // = 宽高获取 = + // ============ + + /** + * 获取屏幕宽度 + * @return 屏幕宽度 + */ + public static int getScreenWidth() { + return getScreenWidthHeight()[0]; + } + + /** + * 获取屏幕高度 + * @return 屏幕高度 + */ + public static int getScreenHeight() { + return getScreenWidthHeight()[1]; + } + + // = + + /** + * 获取屏幕宽高 + * @return int[], 0 = 宽度, 1 = 高度 + */ + public static int[] getScreenWidthHeight() { + try { +// DisplayMetrics displayMetrics = ResourceUtils.getDisplayMetrics(); +// return new int[]{displayMetrics.widthPixels, displayMetrics.heightPixels}; + + WindowManager windowManager = AppUtils.getWindowManager(); + Point point = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + windowManager.getDefaultDisplay().getRealSize(point); + } else { + windowManager.getDefaultDisplay().getSize(point); + } + return new int[]{point.x, point.y}; + } catch (Exception e) { + ALog.eTag(TAG, e, "getScreenWidthHeight"); + } + return new int[]{0, 0}; + } + + /** + * 获取屏幕宽高 + * @return {@link Point}, point.x 宽, point.y 高 + */ + public static Point getScreenWidthHeightToPoint() { + try { +// DisplayMetrics displayMetrics = ResourceUtils.getDisplayMetrics(); +// return new Point(displayMetrics.widthPixels, displayMetrics.heightPixels); + + WindowManager windowManager = AppUtils.getWindowManager(); + Point point = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + windowManager.getDefaultDisplay().getRealSize(point); + } else { + windowManager.getDefaultDisplay().getSize(point); + } + return point; + } catch (Exception e) { + ALog.eTag(TAG, e, "getScreenWidthHeightToPoint"); + } + return null; + } + + // = + + /** + * 获取屏幕分辨率 + * @return 屏幕分辨率 + */ + public static String getScreenSize() { + return getScreenSize("x"); + } + + /** + * 获取屏幕分辨率 + * @param symbol 拼接符号 + * @return 屏幕分辨率 + */ + public static String getScreenSize(final String symbol) { + // 获取分辨率 + int[] widthHeight = getScreenWidthHeight(); + // 返回分辨率信息 + return widthHeight[1] + symbol + widthHeight[0]; + } + + /** + * 获取屏幕英寸 - 例 5.5 英寸 + * @return 屏幕英寸 + */ + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) + public static String getScreenSizeOfDevice() { + try { + Point point = new Point(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + WindowManager windowManager = AppUtils.getWindowManager(); + windowManager.getDefaultDisplay().getRealSize(point); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + // 计算尺寸 + double x = Math.pow(point.x / displayMetrics.xdpi, 2); + double y = Math.pow(point.y / displayMetrics.ydpi, 2); + double screenInches = Math.sqrt(x + y); + // 转换大小 + return new DecimalFormat("#.0").format(screenInches); + } catch (Exception e) { + ALog.eTag(TAG, e, "getScreenSizeOfDevice"); + } + return "unknown"; + } + + // = + + /** + * 获取屏幕密度 + * @return 屏幕密度 + */ + public static float getDensity() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + // 屏幕密度, 如 (0.75 / 1.0 / 1.5 / 2.0) + return (displayMetrics != null) ? displayMetrics.density : 0f; + } + + /** + * 获取屏幕密度 dpi + * @return 屏幕密度 dpi + */ + public static int getDensityDpi() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + // 屏幕密度 DPI, 如 (120 / 160 / 240 / 320) + return (displayMetrics != null) ? displayMetrics.densityDpi : 0; + } + + /** + * 获取屏幕缩放密度 + * @return 屏幕缩放密度 + */ + public static float getScaledDensity() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + return (displayMetrics != null) ? displayMetrics.scaledDensity : 0f; + } + + /** + * 获取 X 轴 dpi + * @return X 轴 dpi + */ + public static float getXDpi() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + return (displayMetrics != null) ? displayMetrics.xdpi : 0f; + } + + /** + * 获取 Y 轴 dpi + * @return Y 轴 dpi + */ + public static float getYDpi() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + return (displayMetrics != null) ? displayMetrics.ydpi : 0f; + } + + /** + * 获取宽度比例 dpi 基准 + * @return 宽度比例 dpi 基准 + */ + public static float getWidthDpi() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + return (displayMetrics != null) ? (displayMetrics.widthPixels / displayMetrics.density) : 0f; + } + + /** + * 获取高度比例 dpi 基准 + * @return 高度比例 dpi 基准 + */ + public static float getHeightDpi() { + DisplayMetrics displayMetrics = getDisplayMetrics(); + return (displayMetrics != null) ? (displayMetrics.heightPixels / displayMetrics.density) : 0f; + } + + /** + * 获取屏幕信息 + * @return 屏幕信息 + */ + public static String getScreenInfo() { + StringBuilder builder = new StringBuilder(); + DisplayMetrics displayMetrics = getDisplayMetrics(); + if (displayMetrics != null) { + try { + int heightPixels = displayMetrics.heightPixels; + int widthPixels = displayMetrics.widthPixels; + + float xdpi = displayMetrics.xdpi; + float ydpi = displayMetrics.ydpi; + int densityDpi = displayMetrics.densityDpi; + + float density = displayMetrics.density; + float scaledDensity = displayMetrics.scaledDensity; + + float heightDpi = heightPixels / density; + float widthDpi = widthPixels / density; + // = + builder.append("heightPixels: " + heightPixels + "px"); + builder.append("\nwidthPixels: " + widthPixels + "px"); + + builder.append("\nxdpi: " + xdpi + "dpi"); + builder.append("\nydpi: " + ydpi + "dpi"); + builder.append("\ndensityDpi: " + densityDpi + "dpi"); + + builder.append("\ndensity: " + density); + builder.append("\nscaledDensity: " + scaledDensity); + + builder.append("\nheightDpi: " + heightDpi + "dpi"); + builder.append("\nwidthDpi: " + widthDpi + "dpi"); + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getScreenInfo"); + } + } + return builder.toString(); + } + + // = + + /** + * 设置禁止截屏 + * @param activity {@link Activity} + * @return {@code true} success, {@code false} fail + */ + public static boolean setWindowSecure(final Activity activity) { + try { + // 禁止截屏 + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "setWindowSecure"); + } + return false; + } + + /** + * 设置屏幕为全屏 + * @param activity {@link Activity} + * @return {@code true} success, {@code false} fail + */ + public static boolean setFullScreen(final Activity activity) { + try { + // 隐藏标题 + activity.requestWindowFeature(Window.FEATURE_NO_TITLE); + // 设置全屏 + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "setFullScreen"); + } + return false; + } + + /** + * 设置屏幕为横屏 + *
+     *     还有一种就是在 Activity 中加属性 android:screenOrientation="landscape"
+     *     不设置 Activity 的 android:configChanges 时
+     *     切屏会重新调用各个生命周期, 切横屏时会执行一次, 切竖屏时会执行两次
+     *     设置 Activity 的 android:configChanges="orientation" 时
+     *     切屏还是会重新调用各个生命周期, 切横、竖屏时只会执行一次
+     *     设置 Activity 的 android:configChanges="orientation|keyboardHidden|screenSize"
+     *     4.0 以上必须带最后一个参数时
+     *     切屏不会重新调用各个生命周期, 只会执行 onConfigurationChanged 方法
+     * 
+ * @param activity {@link Activity} + * @return {@code true} success, {@code false} fail + */ + public static boolean setLandscape(final Activity activity) { + try { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "setLandscape"); + } + return false; + } + + /** + * 设置屏幕为竖屏 + * @param activity {@link Activity} + * @return {@code true} success, {@code false} fail + */ + public static boolean setPortrait(final Activity activity) { + try { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "setPortrait"); + } + return false; + } + + /** + * 判断是否横屏 + * @return {@code true} yes, {@code false} no + */ + public static boolean isLandscape() { + return isLandscape(App.getInstance()); + } + + /** + * 判断是否横屏 + * @param context {@link Context} + * @return {@code true} yes, {@code false} no + */ + public static boolean isLandscape(final Context context) { + try { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } catch (Exception e) { + ALog.eTag(TAG, e, "isLandscape"); + } + return false; + } + + /** + * 判断是否竖屏 + * @return {@code true} yes, {@code false} no + */ + public static boolean isPortrait() { + return isPortrait(App.getInstance()); + } + + /** + * 判断是否竖屏 + * @param context {@link Context} + * @return {@code true} yes, {@code false} no + */ + public static boolean isPortrait(final Context context) { + try { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } catch (Exception e) { + ALog.eTag(TAG, e, "isPortrait"); + } + return false; + } + + /** + * 切换屏幕方向 + * @param activity {@link Activity} + * @return {@code true} 横屏, {@code false} 竖屏 + */ + public static boolean toggleScreenOrientation(final Activity activity) { + try { + // 判断是否竖屏 + if (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + return true; // 切换横屏, 并且表示属于横屏 + } else { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + return false; // 切换竖屏, 并且表示属于竖屏 + } + } catch (Exception e) { + ALog.eTag(TAG, e, "toggleScreenOrientation"); + } + return false; + } + + // = + + /** + * 获取屏幕旋转角度 + * @param activity {@link Activity} + * @return 屏幕旋转角度 + */ + public static int getScreenRotation(final Activity activity) { + try { + switch (activity.getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getScreenRotation"); + } + return 0; + } + + /** + * 判断是否锁屏 + * @return {@code true} yes, {@code false} no + */ + public static boolean isScreenLock() { + try { + KeyguardManager keyguardManager = AppUtils.getKeyguardManager(); + return keyguardManager != null && keyguardManager.inKeyguardRestrictedInputMode(); + } catch (Exception e) { + ALog.eTag(TAG, e, "isScreenLock"); + } + return false; + } + + /** + * 判断是否是平板 + * @return {@code true} yes, {@code false} no + */ + public static boolean isTablet() { + try { + return (ResourceUtils.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } catch (Exception e) { + ALog.eTag(TAG, e, "isTablet"); + } + return false; + } + + // = + + /** + * 获取状态栏的高度 ( 无关 android:theme 获取状态栏高度 ) + * @return 状态栏的高度 + */ + public static int getStatusHeight() { + try { + int id = ResourceUtils.getIdentifier("status_bar_height", "dimen", "android"); + return ResourceUtils.getResources().getDimensionPixelSize(id); + } catch (Exception e) { + ALog.eTag(TAG, e, "getStatusHeight"); + } + return 0; + } + + /** + * 获取应用区域 TitleBar 高度 ( 顶部灰色 TitleBar 高度, 没有设置 android:theme 的 NoTitleBar 时会显示 ) + * @param activity {@link Activity} + * @return 应用区域 TitleBar 高度 + */ + public static int getStatusBarHeight(final Activity activity) { + try { + Rect rect = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); + return rect.top; + } catch (Exception e) { + ALog.eTag(TAG, e, "getStatusBarHeight"); + } + return 0; + } + + /** + * 设置进入休眠时长 + * @param duration 时长 + * @return {@code true} success, {@code false} fail + */ + @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) + public static boolean setSleepDuration(final int duration) { + try { + Settings.System.putInt(ResourceUtils.getContentResolver(), Settings.System.SCREEN_OFF_TIMEOUT, duration); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "setSleepDuration"); + } + return false; + } + + /** + * 获取进入休眠时长 + * @return 进入休眠时长 + */ + public static int getSleepDuration() { + try { + return Settings.System.getInt(ResourceUtils.getContentResolver(), Settings.System.SCREEN_OFF_TIMEOUT); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSleepDuration"); + return -1; + } + } + + // = + + /** + * 获取底部导航栏高度 + * @return 底部导航栏高度 + */ + public static int getNavigationBarHeight() { + try { + Resources resources = ResourceUtils.getResources(); + // 获取对应方向字符串 + String orientation = resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape"; + // 获取对应的 id + int resourceId = resources.getIdentifier(orientation, "dimen", "android"); + if (resourceId > 0 && checkDeviceHasNavigationBar()) { + return resources.getDimensionPixelSize(resourceId); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getNavigationBarHeight"); + } + return 0; + } + + /** + * 检测是否具有底部导航栏 + *
+     *     一加手机上判断不准确
+     * 
+ * @return {@code true} yes, {@code false} no + */ + public static boolean checkDeviceHasNavigationBar() { + boolean hasNavigationBar = false; + try { + Resources resources = ResourceUtils.getResources(); + int id = resources.getIdentifier("config_showNavigationBar", "bool", "android"); + if (id > 0) { + hasNavigationBar = resources.getBoolean(id); + } + try { + Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method method = systemPropertiesClass.getMethod("get", String.class); + String navBarOverride = (String) method.invoke(systemPropertiesClass, "qemu.hw.mainkeys"); + if ("1".equals(navBarOverride)) { + hasNavigationBar = false; + } else if ("0".equals(navBarOverride)) { + hasNavigationBar = true; + } + } catch (Exception e) { + ALog.eTag(TAG, e, "checkDeviceHasNavigationBar - SystemProperties"); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "checkDeviceHasNavigationBar"); + } + return hasNavigationBar; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/ShellUtils.java b/app/src/main/java/com/example/baseframe/utils/ShellUtils.java new file mode 100644 index 0000000..da5553d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ShellUtils.java @@ -0,0 +1,218 @@ +package com.example.baseframe.utils; + +import com.blankj.ALog; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.util.List; + +/** + * detail: Shell 命令工具类 + * @author Ttt + */ +public final class ShellUtils { + + private ShellUtils() { + } + + // 日志 TAG + private static final String TAG = ShellUtils.class.getSimpleName(); + // 操作成功状态码 + private static final int SUCCESS = 0; + // 换行符 + private static final String NEW_LINE_STR = System.getProperty("line.separator"); + + /** + * 执行 shell 命令 + * @param command 待执行命令 + * @param isRoot 是否以 root 权限执行 + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final String command, final boolean isRoot) { + return execCmd(new String[]{command}, isRoot, true); + } + + /** + * 执行 shell 命令 + * @param commands 多条待执行命令 + * @param isRoot 是否以 root 权限执行 + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final List commands, final boolean isRoot) { + return execCmd(commands == null ? null : commands.toArray(new String[]{}), isRoot, true); + } + + /** + * 执行 shell 命令 + * @param commands 多条待执行命令 + * @param isRoot 是否以 root 权限执行 + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final String[] commands, final boolean isRoot) { + return execCmd(commands, isRoot, true); + } + + /** + * 执行 shell 命令 + * @param command 待执行命令 + * @param isRoot 是否以 root 权限执行 + * @param isNeedResultMsg 是否需要返回结果消息 (error、success message) + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final String command, final boolean isRoot, final boolean isNeedResultMsg) { + return execCmd(new String[]{command}, isRoot, isNeedResultMsg); + } + + /** + * 执行 shell 命令 + * @param commands 多条待执行命令 + * @param isRoot 是否以 root 权限执行 + * @param isNeedResultMsg 是否需要结果消息 (error、success message) + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final List commands, final boolean isRoot, final boolean isNeedResultMsg) { + return execCmd(commands == null ? null : commands.toArray(new String[]{}), isRoot, isNeedResultMsg); + } + + /** + * 执行 shell 命令 + * @param commands 多条待执行命令 + * @param isRoot 是否以 root 权限执行 + * @param isNeedResultMsg 是否需要结果消息 (error、success message) + * @return {@link CommandResult} + */ + public static CommandResult execCmd(final String[] commands, final boolean isRoot, final boolean isNeedResultMsg) { + int result = -1; + if (commands == null || commands.length == 0) { + return new CommandResult(result, null, null); + } + Process process = null; + DataOutputStream dos = null; + String successMsg = null; + String errorMsg = null; + try { + process = Runtime.getRuntime().exec(isRoot ? "su" : "sh"); + dos = new DataOutputStream(process.getOutputStream()); + // 循环写入待执行命令 + for (String command : commands) { + if (command == null) continue; + dos.write(command.getBytes()); + dos.writeBytes(NEW_LINE_STR); + dos.flush(); + } + dos.writeBytes("exit" + NEW_LINE_STR); + dos.flush(); + // 为了避免 Process.waitFor() 导致主线程堵塞问题, 最好读取信息 + if (isNeedResultMsg) { // 如果程序不断在向输出流和错误流写数据, 而 JVM 不读取的话, 当缓冲区满之后将无法继续写入数据, 最终造成阻塞在 waitFor() 这里 + // 读取成功数据 + successMsg = consumeInputStream(new InputStreamReader(process.getInputStream(), "UTF-8")); + // 读取异常数据 + errorMsg = consumeInputStream(new InputStreamReader(process.getErrorStream(), "UTF-8")); + } + // 执行结果状态码 + result = process.waitFor(); + } catch (Exception e) { + ALog.eTag(TAG, e, "execCmd"); + } finally { + CloseUtils.closeIOQuietly(dos); + // 进程销毁 + if (process != null) { + process.destroy(); + } + } + return new CommandResult(result, successMsg, errorMsg); + } + + /** + * 消费 InputStream 并且返回字符串 + * @param reader {@link InputStreamReader} + * @return 读取流数据 + */ + private static String consumeInputStream(final InputStreamReader reader) { + BufferedReader br = null; + try { + StringBuilder builder = new StringBuilder(); + br = new BufferedReader(reader); + String str; + if ((str = br.readLine()) != null) { + builder.append(str); + while ((str = br.readLine()) != null) { + builder.append(NEW_LINE_STR).append(str); + } + } + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "consumeInputStream"); + } finally { + CloseUtils.closeIOQuietly(br); + } + return null; + } + + // ============== + // = 对外实体类 = + // ============== + + /** + * detail: 命令执行结果实体类 + * @author Ttt + */ + public static final class CommandResult { + + // 执行结果状态码 + public int result; + // 成功信息 + public String successMsg; + // 错误信息 + public String errorMsg; + + /** + * 构造函数 + * @param result 执行结果状态码 + * @param successMsg 成功信息 + * @param errorMsg 错误信息 + */ + public CommandResult(final int result, final String successMsg, final String errorMsg) { + this.result = result; + this.successMsg = successMsg; + this.errorMsg = errorMsg; + } + + /** + * 判断是否执行成功 + * @return {@code true} yes, {@code false} no + */ + public boolean isSuccess() { + return result == SUCCESS; + } + + /** + * 判断是否执行成功 ( 判断 errorMsg) + * @return {@code true} yes, {@code false} no + */ + public boolean isSuccess2() { + return result == SUCCESS && (errorMsg == null || errorMsg.length() == 0); + } + + /** + * 判断是否执行成功 ( 判断 successMsg) + * @return {@code true} yes, {@code false} no + */ + public boolean isSuccess3() { + return result == SUCCESS && successMsg != null && successMsg.length() != 0; + } + + /** + * 判断是否执行成功 ( 判断 successMsg), 并且 successMsg 是否包含某个字符串 + * @param contains 待校验包含字符串 + * @return {@code true} yes, {@code false} no + */ + public boolean isSuccess4(final String contains) { + if (result == SUCCESS && successMsg != null && successMsg.length() != 0) { + return contains != null && contains.length() != 0 && successMsg.toLowerCase().contains(contains); + } + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/StatusBarUtil.java b/app/src/main/java/com/example/baseframe/utils/StatusBarUtil.java new file mode 100644 index 0000000..a483005 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/StatusBarUtil.java @@ -0,0 +1,671 @@ +package com.example.baseframe.utils; + + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.LinearLayout; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.example.baseframe.R; + +/** + * Created by Jaeger on 16/2/14. + *

+ * Email: chjie.jaeger@gmail.com + * GitHub: https://github.com/laobie + */ +public class StatusBarUtil { + + public static final int DEFAULT_STATUS_BAR_ALPHA = 112; + private static final int FAKE_STATUS_BAR_VIEW_ID = R.id.statusbarutil_fake_status_bar_view; + private static final int FAKE_TRANSLUCENT_VIEW_ID = R.id.statusbarutil_translucent_view; + private static final int TAG_KEY_HAVE_SET_OFFSET = -123; + + /** + * 设置状态栏颜色 + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + */ + public static void setColor(Activity activity, @ColorInt int color) { + setColor(activity, color, DEFAULT_STATUS_BAR_ALPHA); + } + + /** + * 设置状态栏颜色 + * + * @param activity 需要设置的activity + * @param color 状态栏颜色值 + * @param statusBarAlpha 状态栏透明度 + */ + + public static void setColor(Activity activity, @ColorInt int color, @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha)); + } else { + decorView.addView(createStatusBarView(activity, color, statusBarAlpha)); + } + setRootView(activity); + } + } + + /** + * 为滑动返回界面设置状态栏颜色 + * + * @param activity 需要设置的activity + * @param color 状态栏颜色值 + */ + public static void setColorForSwipeBack(Activity activity, int color) { + setColorForSwipeBack(activity, color, DEFAULT_STATUS_BAR_ALPHA); + } + + /** + * 为滑动返回界面设置状态栏颜色 + * + * @param activity 需要设置的activity + * @param color 状态栏颜色值 + * @param statusBarAlpha 状态栏透明度 + */ + public static void setColorForSwipeBack(Activity activity, @ColorInt int color, + @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + + ViewGroup contentView = ((ViewGroup) activity.findViewById(android.R.id.content)); + View rootView = contentView.getChildAt(0); + int statusBarHeight = getStatusBarHeight(activity); + if (rootView != null && rootView instanceof CoordinatorLayout) { + final CoordinatorLayout coordinatorLayout = (CoordinatorLayout) rootView; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + coordinatorLayout.setFitsSystemWindows(false); + contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha)); + boolean isNeedRequestLayout = contentView.getPaddingTop() < statusBarHeight; + if (isNeedRequestLayout) { + contentView.setPadding(0, statusBarHeight, 0, 0); + coordinatorLayout.post(new Runnable() { + @Override + public void run() { + coordinatorLayout.requestLayout(); + } + }); + } + } else { + coordinatorLayout.setStatusBarBackgroundColor(calculateStatusColor(color, statusBarAlpha)); + } + } else { + contentView.setPadding(0, statusBarHeight, 0, 0); + contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha)); + } + setTransparentForWindow(activity); + } + } + + /** + * 设置状态栏纯色 不加半透明效果 + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + */ + public static void setColorNoTranslucent(Activity activity, @ColorInt int color) { + setColor(activity, color, 0); + } + + /** + * 设置状态栏颜色(5.0以下无半透明效果,不建议使用) + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + */ + @Deprecated + public static void setColorDiff(Activity activity, @ColorInt int color) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + transparentStatusBar(activity); + ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content); + // 移除半透明矩形,以免叠加 + View fakeStatusBarView = contentView.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(color); + } else { + contentView.addView(createStatusBarView(activity, color)); + } + setRootView(activity); + } + + /** + * 使状态栏半透明 + *

+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏 + * + * @param activity 需要设置的activity + */ + public static void setTranslucent(Activity activity) { + setTranslucent(activity, DEFAULT_STATUS_BAR_ALPHA); + } + + /** + * 使状态栏半透明 + *

+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏 + * + * @param activity 需要设置的activity + * @param statusBarAlpha 状态栏透明度 + */ + public static void setTranslucent(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + setTransparent(activity); + addTranslucentView(activity, statusBarAlpha); + } + + /** + * 针对根布局是 CoordinatorLayout, 使状态栏半透明 + *

+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏 + * + * @param activity 需要设置的activity + * @param statusBarAlpha 状态栏透明度 + */ + public static void setTranslucentForCoordinatorLayout(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + transparentStatusBar(activity); + addTranslucentView(activity, statusBarAlpha); + } + + /** + * 设置状态栏全透明 + * + * @param activity 需要设置的activity + */ + public static void setTransparent(Activity activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + transparentStatusBar(activity); + setRootView(activity); + } + + /** + * 使状态栏透明(5.0以上半透明效果,不建议使用) + *

+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏 + * + * @param activity 需要设置的activity + */ + @Deprecated + public static void setTranslucentDiff(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // 设置状态栏透明 + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + setRootView(activity); + } + } + + /** + * 为DrawerLayout 布局设置状态栏变色 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + * @param color 状态栏颜色值 + */ + public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) { + setColorForDrawerLayout(activity, drawerLayout, color, DEFAULT_STATUS_BAR_ALPHA); + } + + /** + * 为DrawerLayout 布局设置状态栏颜色,纯色 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + * @param color 状态栏颜色值 + */ + public static void setColorNoTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) { + setColorForDrawerLayout(activity, drawerLayout, color, 0); + } + + /** + * 为DrawerLayout 布局设置状态栏变色 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + * @param color 状态栏颜色值 + * @param statusBarAlpha 状态栏透明度 + */ + public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color, + @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + } else { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + // 生成一个状态栏大小的矩形 + // 添加 statusBarView 到布局中 + ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0); + View fakeStatusBarView = contentLayout.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(color); + } else { + contentLayout.addView(createStatusBarView(activity, color), 0); + } + // 内容布局不是 LinearLayout 时,设置padding top + if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) { + contentLayout.getChildAt(1) + .setPadding(contentLayout.getPaddingLeft(), getStatusBarHeight(activity) + contentLayout.getPaddingTop(), + contentLayout.getPaddingRight(), contentLayout.getPaddingBottom()); + } + // 设置属性 + setDrawerLayoutProperty(drawerLayout, contentLayout); + addTranslucentView(activity, statusBarAlpha); + } + + /** + * 设置 DrawerLayout 属性 + * + * @param drawerLayout DrawerLayout + * @param drawerLayoutContentLayout DrawerLayout 的内容布局 + */ + private static void setDrawerLayoutProperty(DrawerLayout drawerLayout, ViewGroup drawerLayoutContentLayout) { + ViewGroup drawer = (ViewGroup) drawerLayout.getChildAt(1); + drawerLayout.setFitsSystemWindows(false); + drawerLayoutContentLayout.setFitsSystemWindows(false); + drawerLayoutContentLayout.setClipToPadding(true); + drawer.setFitsSystemWindows(false); + } + + /** + * 为DrawerLayout 布局设置状态栏变色(5.0以下无半透明效果,不建议使用) + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + * @param color 状态栏颜色值 + */ + @Deprecated + public static void setColorForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // 生成一个状态栏大小的矩形 + ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0); + View fakeStatusBarView = contentLayout.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, DEFAULT_STATUS_BAR_ALPHA)); + } else { + // 添加 statusBarView 到布局中 + contentLayout.addView(createStatusBarView(activity, color), 0); + } + // 内容布局不是 LinearLayout 时,设置padding top + if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) { + contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0); + } + // 设置属性 + setDrawerLayoutProperty(drawerLayout, contentLayout); + } + } + + /** + * 为 DrawerLayout 布局设置状态栏透明 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + */ + public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) { + setTranslucentForDrawerLayout(activity, drawerLayout, DEFAULT_STATUS_BAR_ALPHA); + } + + /** + * 为 DrawerLayout 布局设置状态栏透明 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + */ + public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout, + @IntRange(from = 0, to = 255) int statusBarAlpha) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + setTransparentForDrawerLayout(activity, drawerLayout); + addTranslucentView(activity, statusBarAlpha); + } + + /** + * 为 DrawerLayout 布局设置状态栏透明 + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + */ + public static void setTransparentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + } else { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + + ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0); + // 内容布局不是 LinearLayout 时,设置padding top + if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) { + contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0); + } + + // 设置属性 + setDrawerLayoutProperty(drawerLayout, contentLayout); + } + + /** + * 为 DrawerLayout 布局设置状态栏透明(5.0以上半透明效果,不建议使用) + * + * @param activity 需要设置的activity + * @param drawerLayout DrawerLayout + */ + @Deprecated + public static void setTranslucentForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // 设置状态栏透明 + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // 设置内容布局属性 + ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0); + contentLayout.setFitsSystemWindows(true); + contentLayout.setClipToPadding(true); + // 设置抽屉布局属性 + ViewGroup vg = (ViewGroup) drawerLayout.getChildAt(1); + vg.setFitsSystemWindows(false); + // 设置 DrawerLayout 属性 + drawerLayout.setFitsSystemWindows(false); + } + } + + /** + * 为头部是 ImageView 的界面设置状态栏全透明 + * + * @param activity 需要设置的activity + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTransparentForImageView(Activity activity, View needOffsetView) { + setTranslucentForImageView(activity, 0, needOffsetView); + } + + /** + * 为头部是 ImageView 的界面设置状态栏透明(使用默认透明度) + * + * @param activity 需要设置的activity + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTranslucentForImageView(Activity activity, View needOffsetView) { + setTranslucentForImageView(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView); + } + + /** + * 为头部是 ImageView 的界面设置状态栏透明 + * + * @param activity 需要设置的activity + * @param statusBarAlpha 状态栏透明度 + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTranslucentForImageView(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha, + View needOffsetView) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + setTransparentForWindow(activity); + addTranslucentView(activity, statusBarAlpha); + if (needOffsetView != null) { + Object haveSetOffset = needOffsetView.getTag(TAG_KEY_HAVE_SET_OFFSET); + if (haveSetOffset != null && (Boolean) haveSetOffset) { + return; + } + ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) needOffsetView.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin + getStatusBarHeight(activity), + layoutParams.rightMargin, layoutParams.bottomMargin); + needOffsetView.setTag(TAG_KEY_HAVE_SET_OFFSET, true); + } + } + + /** + * 为 fragment 头部是 ImageView 的设置状态栏透明 + * + * @param activity fragment 对应的 activity + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTranslucentForImageViewInFragment(Activity activity, View needOffsetView) { + setTranslucentForImageViewInFragment(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView); + } + + /** + * 为 fragment 头部是 ImageView 的设置状态栏透明 + * + * @param activity fragment 对应的 activity + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTransparentForImageViewInFragment(Activity activity, View needOffsetView) { + setTranslucentForImageViewInFragment(activity, 0, needOffsetView); + } + + /** + * 为 fragment 头部是 ImageView 的设置状态栏透明 + * + * @param activity fragment 对应的 activity + * @param statusBarAlpha 状态栏透明度 + * @param needOffsetView 需要向下偏移的 View + */ + public static void setTranslucentForImageViewInFragment(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha, + View needOffsetView) { + setTranslucentForImageView(activity, statusBarAlpha, needOffsetView); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + clearPreviousSetting(activity); + } + } + + /** + * 隐藏伪状态栏 View + * + * @param activity 调用的 Activity + */ + public static void hideFakeStatusBarView(Activity activity) { + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + fakeStatusBarView.setVisibility(View.GONE); + } + View fakeTranslucentView = decorView.findViewById(FAKE_TRANSLUCENT_VIEW_ID); + if (fakeTranslucentView != null) { + fakeTranslucentView.setVisibility(View.GONE); + } + } + + /////////////////////////////////////////////////////////////////////////////////// + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static void clearPreviousSetting(Activity activity) { + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID); + if (fakeStatusBarView != null) { + decorView.removeView(fakeStatusBarView); + ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); + rootView.setPadding(0, 0, 0, 0); + } + } + + /** + * 添加半透明矩形条 + * + * @param activity 需要设置的 activity + * @param statusBarAlpha 透明值 + */ + private static void addTranslucentView(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) { + ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content); + View fakeTranslucentView = contentView.findViewById(FAKE_TRANSLUCENT_VIEW_ID); + if (fakeTranslucentView != null) { + if (fakeTranslucentView.getVisibility() == View.GONE) { + fakeTranslucentView.setVisibility(View.VISIBLE); + } + fakeTranslucentView.setBackgroundColor(Color.argb(statusBarAlpha, 0, 0, 0)); + } else { + contentView.addView(createTranslucentStatusBarView(activity, statusBarAlpha)); + } + } + + /** + * 生成一个和状态栏大小相同的彩色矩形条 + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + * @return 状态栏矩形条 + */ + private static View createStatusBarView(Activity activity, @ColorInt int color) { + return createStatusBarView(activity, color, 0); + } + + /** + * 生成一个和状态栏大小相同的半透明矩形条 + * + * @param activity 需要设置的activity + * @param color 状态栏颜色值 + * @param alpha 透明值 + * @return 状态栏矩形条 + */ + private static View createStatusBarView(Activity activity, @ColorInt int color, int alpha) { + // 绘制一个和状态栏一样高的矩形 + View statusBarView = new View(activity); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity)); + statusBarView.setLayoutParams(params); + statusBarView.setBackgroundColor(calculateStatusColor(color, alpha)); + statusBarView.setId(FAKE_STATUS_BAR_VIEW_ID); + return statusBarView; + } + + /** + * 设置根布局参数 + */ + private static void setRootView(Activity activity) { + ViewGroup parent = (ViewGroup) activity.findViewById(android.R.id.content); + for (int i = 0, count = parent.getChildCount(); i < count; i++) { + View childView = parent.getChildAt(i); + if (childView instanceof ViewGroup) { + childView.setFitsSystemWindows(true); + ((ViewGroup) childView).setClipToPadding(true); + } + } + } + + /** + * 设置透明 + */ + private static void setTransparentForWindow(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + activity.getWindow() + .getDecorView() + .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + activity.getWindow() + .setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + /** + * 使状态栏透明 + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static void transparentStatusBar(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + } else { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + /** + * 创建半透明矩形 View + * + * @param alpha 透明值 + * @return 半透明 View + */ + private static View createTranslucentStatusBarView(Activity activity, int alpha) { + // 绘制一个和状态栏一样高的矩形 + View statusBarView = new View(activity); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity)); + statusBarView.setLayoutParams(params); + statusBarView.setBackgroundColor(Color.argb(alpha, 0, 0, 0)); + statusBarView.setId(FAKE_TRANSLUCENT_VIEW_ID); + return statusBarView; + } + + /** + * 获取状态栏高度 + * + * @param context context + * @return 状态栏高度 + */ + private static int getStatusBarHeight(Context context) { + // 获得状态栏高度 + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + return context.getResources().getDimensionPixelSize(resourceId); + } + + /** + * 计算状态栏颜色 + * + * @param color color值 + * @param alpha alpha值 + * @return 最终的状态栏颜色 + */ + private static int calculateStatusColor(@ColorInt int color, int alpha) { + if (alpha == 0) { + return color; + } + float a = 1 - alpha / 255f; + int red = color >> 16 & 0xff; + int green = color >> 8 & 0xff; + int blue = color & 0xff; + red = (int) (red * a + 0.5); + green = (int) (green * a + 0.5); + blue = (int) (blue * a + 0.5); + return 0xff << 24 | red << 16 | green << 8 | blue; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/StringUtil.java b/app/src/main/java/com/example/baseframe/utils/StringUtil.java new file mode 100644 index 0000000..345aff2 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/StringUtil.java @@ -0,0 +1,643 @@ +package com.example.baseframe.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.widget.EditText; +import android.widget.TextView; + +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class StringUtil { + //private static Context context; + /** + * 验证 + * @param s + * @return + */ + public static boolean isEmpty(String s) { + return s == null || s.length() == 0 || s.equals("null"); + } + + public static boolean isNoEmpty(String s) { + boolean bool=s == null || s.length() == 0 || s.equals("null"); + return !bool; + } + + /** + * 验证邮箱的合法性 + * ^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$ + * @param email + * @return + */ + public static boolean isValidEmail(String email) { + if (isEmpty(email)) + return false; + String pattern_ = "^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(email); + return mat.matches(); + } + + + /** + * 判断日期格式是否正确 + * + * @param sDate + * @return + */ + public static boolean isValidDate(String sDate) { + String datePattern1 = "\\d{4}-\\d{2}-\\d{2}"; + String datePattern2 = "^((\\d{2}(([02468][048])|([13579][26]))" + + "[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|" + + "(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?" + + "((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?(" + + "(((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?" + + "((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))"; + if ((sDate != null)) { + Pattern pattern = Pattern.compile(datePattern1); + Matcher match = pattern.matcher(sDate); + if (match.matches()) { + pattern = Pattern.compile(datePattern2); + match = pattern.matcher(sDate); + return match.matches(); + } else { + return false; + } + } + return false; + } + + /** + * 正则表达式判断字符串是数字,可以为正数,可以为负数,可含有小数点,不能含有字符。 + */ + public static boolean isNumeric(String values) { + if (StringUtil.isEmpty(values)) { + return false; + } + Pattern pattern = Pattern.compile("-?[0-9]*.?[0-9]*"); + Matcher isNum = pattern.matcher(values); + if( !isNum.matches() ) + { + return false; + } + return true; + } + + /** + * 检查指定的字符串列表是否不为空。 + */ + public static boolean areNotEmpty(String... values) { + boolean result = true; + if (values == null || values.length == 0) { + result = false; + } else { + for (String value : values) { + result &= !isEmpty(value); + } + } + return result; + } + + /** + * 把通用字符编码的字符串转化为汉字编码。 + */ + public static String unicodeToChinese(String unicode) { + StringBuilder out = new StringBuilder(); + if (!isEmpty(unicode)) { + for (int i = 0; i < unicode.length(); i++) { + out.append(unicode.charAt(i)); + } + } + return out.toString(); + } + + /** + * 验证姓名是中文 + */ + public static boolean isValidUserName(String name) { + if (isEmpty(name)) { + return false; + } else { + + // ^[_a-zA-Z0-9+\.\u4e00-\u9fa5]{2,6}$ + name = new String(name.getBytes());// 用GBK编码 + // final String pattern_ = "^[_a-zA-Z0-9+\\.\u4e00-\u9fa5]{2,6}$"; + String pattern_ = "^[\u4e00-\u9fa5]{2,6}$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(name); + return mat.matches(); + } + + } + + /** + * 验证密码为字母、数字、下划线两者及以上8-20个字符 + * ^(?![0-9]+$)(?![a-zA-Z]+$)(?![_]+$)\\w{8,20}$ + * (?!^(\\d+|[a-zA-Z]+|[_]+)$)^[\\w]{8,20}$ + */ + public static boolean checkPassword(String password) { + boolean tag = false; + if (isEmpty(password)) { + tag = false; + } else { + String pattern_ = "^(?![0-9]+$)(?![a-zA-Z]+$)(?![_]+$)\\w{8,20}$"; + + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(password); + return mat.matches(); + } + + return tag; + } + + /** + * 验证密码为英文加数字 + */ + public static boolean checkPasswordVar(String password) { + boolean tag = false; + if (isEmpty(password)) { + tag = false; + } else { + String pattern_ = "^[A-Za-z0-9]{6,16}$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(password); + return mat.matches(); + } + + return tag; + } + + public static boolean isValidMobile(String mobile) { + if (StringUtil.isEmpty(mobile)) + return false; + //TODO:oyj 2015-4-23 手机验证放开13-19开头 + Pattern pattern= Pattern.compile("^(1[3-9])\\d{9}$"); +// Pattern pattern = Pattern.compile("^((13[0-9])|(14[7])|(15[^4,\\D])|(18[0-9]))\\d{8}$"); + Matcher matcher = pattern.matcher(mobile); + return matcher.matches(); + } + + public static boolean isValidQQ(String qq) { + if (StringUtil.isEmpty(qq)) + return false; + // Pattern pattern=Pattern.compile("^[13,14,15,18,19]\\d{9}$"); + Pattern pattern = Pattern.compile("^[1-9]\\d{3,}$"); + Matcher matcher = pattern.matcher(qq); + return matcher.matches(); + } + + + public static boolean isValidAmount(String amount) { + if (StringUtil.isEmpty(amount)) + return false; + Pattern pattern = Pattern.compile("^((\\d{1,})|([0-9]+\\.[0-9]{1,2}))$"); + Matcher matcher = pattern.matcher(amount); + return matcher.matches(); + } + + public static boolean isValidNumber(String url) { + if (StringUtil.isEmpty(url)) + return false; + Pattern pattern = Pattern.compile("^\\d{1,10}$"); + Matcher matcher = pattern.matcher(url); + return matcher.matches(); + } + + /** + * 文本框中过滤特殊字符 + * [~@#$%^&*_+<>@#¥%] + * < > % ' " $ = (后台限制的字符) + */ + public static String StringFilter(String content) { + if (isEmpty(content)) { + return ""; + } else { + String pattern_ = "[<>%'\"$=&]"; + + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(content); + return mat.replaceAll("*").trim(); + } + + } + + + /** + * 文本框中过滤特殊字符 + * [~@#$%^&*_+<>@#¥%] + * < > % ' " $ = (后台限制的字符) + */ + public static boolean StringFilters(String content) { + + String regEx="[`~!@#$%^&*()+=|{}':;,\\[\\]<>/?!¥…()—【】‘;:”“’。,、?]"; + Pattern p = Pattern.compile(regEx); + Matcher m = p.matcher(content); + + if( m.find())return true; + + return false; + } + /** + * 文本框中过滤特殊字符用空取代 + * [~@#$%^&*_+<>@#¥%] + * < > % ' " $ = (后台限制的字符) + * @param content + * @return + */ + public static String StringFilter2(String content){ + if (isEmpty(content)) { + return ""; + } else { + String speChat="[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]"; + Pattern pattern = Pattern.compile(speChat); + Matcher matcher = pattern.matcher(content.toString()); + if(matcher.find()){ + //JLog.e("hasLawlessStr=ggggggg "); + return ""; + } + else return content.toString(); + } + } + /** + * 验证文本框中是否包含特殊字符 + * [~@#$%^&*_+<>@#¥%] + * < > % ' " $ = (后台限制的字符) + */ + public static boolean hasLawlessStr(String content) { + if (isEmpty(content)) { + return false; + } else { + String pattern_ = "[<>%'\"$=&]"; + + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(content); + return mat.find(); + } + } + + + /** + * 验证是否 不包含指定的字符 < > / \ % + */ + public static boolean isContains(String name) { + boolean bo; + // ^[_a-zA-Z0-9+\.\u4e00-\u9fa5]{2,6}$ + // name = new String(name.getBytes());// 用GBK编码 + // final String pattern_ = "^[_a-zA-Z0-9+\\.\u4e00-\u9fa5]{2,6}$"; + String pattern_ = "^(?:(?!(<|>|/|\\\\|%)).)*$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(name); + if(mat.matches()){ + // showToast("不包含"); + bo=true; + }else{ + //showToast(R.string.xwarn); + bo=false; + } + return bo; + + } + + /** + * 验证是否境外手机 + * (^\+?\d{1,3}?(\(\d{2,5})\))(\d{3,19})(-(\d{4,8}))?$ + */ + @Deprecated //并不准建议弃用 + public static boolean isValidMobileForeign(String content) { + if (isEmpty(content)) { + return false; + } else { + String pattern_ = "(^\\+?\\d{1,3}?(\\(\\d{2,5})\\))(\\d{3,19})(-(\\d{4,8}))?$"; + + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(content); + return mat.matches(); + } + } + + /** + * 验证港澳台身份证 + */ + public static boolean checkForeignIdCard(String idCard) { + boolean tag = false; + if (isEmpty(idCard)) { + tag = false; + } else { + String pattern_ = "^[A-Za-z0-9()]{6,25}$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(idCard); + return mat.matches(); + } + + return tag; + } + + /** + * 验证密身份证 + */ + public static boolean checkIdCard(String idCard) { + boolean tag = false; + if (isEmpty(idCard)) { + tag = false; + } else { + String pattern_ = "^[0-9xX]{15,18}$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(idCard); + return mat.matches(); + } + + return tag; + } + + /** + * 领取红包卡号校验 + */ + public static boolean checkReceiveCard(String card) { + boolean tag = false; + if (isEmpty(card)) { + tag = false; + } else { + String pattern_ = "^[0-9]{9,10}$"; + Pattern pattern = Pattern.compile(pattern_); + Matcher mat = pattern.matcher(card); + return mat.matches(); + } + + return tag; + } + + /** + * 将String转换为Long + * + * @param str + * @return + */ + public static long parserLong(String str) { + if (str == null || (str = str.trim()).length() <= 0) { + return 0; + } + try { + return Long.parseLong(str); + } catch (Exception e) { + } + return 0; + } + + /** + * 使用java正则表达式去掉多余的.与0 + * + * @return + */ + public static String subZeroAndDot(String input) { + if (input.indexOf(".") > 0) { + input = input.replaceAll("0+?$", "");// 去掉多余的0 + input = input.replaceAll("[.]$", "");// 如最后一位是.则去掉 + } + return input; + } + + + /** + * 11位电话号码3 4 4显示 + * @param editText + */ + public static void showPhoneNo(final EditText editText){ + editText.addTextChangedListener(new TextWatcher() + { + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) + { + String inputStr = ""; + String newStr = s.toString(); + newStr = newStr.replace(" ", ""); + int index = 0; + if((index + 3)< newStr.length()){ + inputStr += (newStr.substring(index, index + 3)+ " "); + index += 3; + } + while((index + 4)< newStr.length()){ + inputStr += (newStr.substring(index, index + 4)+ " "); + index += 4; + } + inputStr += (newStr.substring(index, newStr.length())); + editText.setText(inputStr); + editText.setSelection(inputStr.length()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { + + } + + @Override + public void afterTextChanged(Editable s) + { + + } + }); + } + + /** + * 手机号 3 4 4 ..显示 + * @param TextView + * @param newStr + */ + public static void showPhoneNo(final TextView TextView, String newStr){ + if(isNoEmpty(newStr)){ + String inputStr = ""; + newStr = newStr.replace(" ", ""); + int index = 0; + if((index + 3)< newStr.length()){ + inputStr += (newStr.substring(index, index + 3)+ " "); + index += 3; + } + while((index + 4)< newStr.length()){ + inputStr += (newStr.substring(index, index + 4)+ " "); + index += 4; + } + inputStr += (newStr.substring(index, newStr.length())); + TextView.setText(inputStr); + } + } + + + /** + * + */ + + public static void showCardNo(final TextView editText, String newStr){ + String inputStr = ""; + newStr = newStr.replace(" ", ""); + int index = 0; +// if((index + 3)< newStr.length()){ +// inputStr += (newStr.substring(index, index + 3)+ " "); +// index += 3; +// } + while((index + 4)< newStr.length()){ + inputStr += (newStr.substring(index, index + 4)+ " "); + index += 4; + } + inputStr += (newStr.substring(index, newStr.length())); + editText.setText(inputStr); + } + + + /** + * 过滤+86(邮箱除外),中间空格 + * @param idNumber + * @return + */ + public static String filterIdNum(String idNumber){ + if (idNumber.contains("+86") && !idNumber.contains("@")) + { + idNumber=idNumber.replace("+86", ""); + } + if (idNumber.contains("(") && !idNumber.contains("@")) + { + idNumber=idNumber.replace("(", ""); + } + if (idNumber.contains(")") && !idNumber.contains("@")) + { + idNumber=idNumber.replace(")", ""); + } + if (idNumber.contains(" ")) + { + idNumber=idNumber.replace(" ", ""); + } + return idNumber; + + } + + + /** + * 字符串打码显示 + * @param str + * @param starNum 打码*个数 4个为手机号打码,10个为身份证或者银行卡号 + * @return + */ + public static String hideString(String str, int starNum){ + + if(StringUtil.isNoEmpty(str)&&str.length()>4){ + String tempStr=str.substring(3,str.length()-4); + if (starNum==4) + { + str=str.replace(tempStr,"****"); + } + else if (starNum==10) + { + str=str.replace(tempStr,"**********"); + } + } + + + return str; + + } + + + /** + * 判断list 是否为空 + * @param list + * @param + * @return + */ + public static boolean isListNotNull(List list){ + if(list!=null&&!list.isEmpty()){ + return true; + } + return false; + } + /** + * 判断list 是否为空 + * @param list + * @param + * @return + */ + public static boolean isListNull(List list){ + if(list!=null&&!list.isEmpty()){ + return false; + } + return true; + } + + /** + * Android 手机唯一标识 + * @param context + * @return + */ + public static String getPhoneSign(Context context) { + StringBuilder deviceId = new StringBuilder(); + // 渠道标志 +// deviceId.append("a"); + try { + //IMEI(imei) + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + String imei = tm.getDeviceId(); + if (!TextUtils.isEmpty(imei)) { +// deviceId.append("imei"); + deviceId.append(imei); + return deviceId.toString(); + } + //序列号(sn) + String sn = tm.getSimSerialNumber(); + if (!TextUtils.isEmpty(sn)) { +// deviceId.append("sn"); + deviceId.append(sn); + return deviceId.toString(); + } + //如果上面都没有, 则生成一个id:随机码 + String uuid = getUUID(context); + if (!TextUtils.isEmpty(uuid)) { +// deviceId.append("id"); + deviceId.append(uuid); + return deviceId.toString(); + } + } catch (Exception e) { + e.printStackTrace(); +// deviceId.append("id").append(getUUID()); + deviceId.append(getUUID(context)); + } + return deviceId.toString(); + } + + /** + * UUID + */ + private static String uuid; + + public static String getUUID(Context context) { + SharedPreferences mShare = context.getSharedPreferences("uuid", context.MODE_PRIVATE); + if (mShare != null) { + uuid = mShare.getString("uuid", ""); + } + if (TextUtils.isEmpty(uuid)) { + uuid = UUID.randomUUID().toString(); + mShare.edit().putString("uuid", uuid).commit(); + } + return uuid; + } + + + /** + * 冒号分割处理 + * @param str 待处理字符串 + * @return 冒号分割后的字符串 + */ + public static String colonSplit(final String str) { + if (!isEmpty(str)) { + return str.replaceAll("(?<=[0-9A-F]{2})[0-9A-F]{2}", ":$0"); + } + return str; + } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/ToastUtils.java b/app/src/main/java/com/example/baseframe/utils/ToastUtils.java new file mode 100644 index 0000000..0f37ff7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/ToastUtils.java @@ -0,0 +1,451 @@ +package com.example.baseframe.utils; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.example.baseframe.R; +import com.example.baseframe.api.App; + +import java.lang.ref.WeakReference; + +/** + * Created by goldze on 2017/5/14. + * 吐司工具类 + */ +public final class ToastUtils { + + private static final int DEFAULT_COLOR = 0x12000000; + private static Toast sToast; + private static int gravity = Gravity.CENTER; + // private static int gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + private static int xOffset = 0; + private static int yOffset = (int) (64 * App.getInstance().getResources().getDisplayMetrics().density + 0.5); + private static int backgroundColor = DEFAULT_COLOR; + private static int bgResource = -1; + private static int messageColor = DEFAULT_COLOR; + private static WeakReference sViewWeakReference; + private static Handler sHandler = new Handler(Looper.getMainLooper()); + + private ToastUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 设置吐司位置 + * + * @param gravity 位置 + * @param xOffset x偏移 + * @param yOffset y偏移 + */ + public static void setGravity(int gravity, int xOffset, int yOffset) { + ToastUtils.gravity = gravity; + ToastUtils.xOffset = xOffset; + ToastUtils.yOffset = yOffset; + } + + /** + * 设置吐司view + * + * @param layoutId 视图 + */ + public static void setView(@LayoutRes int layoutId) { + LayoutInflater inflate = (LayoutInflater) App.getInstance().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + sViewWeakReference = new WeakReference<>(inflate.inflate(layoutId, null)); + } + + /** + * 设置吐司view + * + * @param view 视图 + */ + public static void setView(@Nullable View view) { + sViewWeakReference = view == null ? null : new WeakReference<>(view); + } + + /** + * 获取吐司view + * + * @return view + */ + public static View getView() { + if (sViewWeakReference != null) { + final View view = sViewWeakReference.get(); + if (view != null) { + return view; + } + } + if (sToast != null) return sToast.getView(); + return null; + } + + /** + * 设置背景颜色 + * + * @param backgroundColor 背景色 + */ + public static void setBackgroundColor(@ColorInt int backgroundColor) { + ToastUtils.backgroundColor = backgroundColor; + } + + /** + * 设置背景资源 + * + * @param bgResource 背景资源 + */ + public static void setBgResource(@DrawableRes int bgResource) { + ToastUtils.bgResource = bgResource; + } + + /** + * 设置消息颜色 + * + * @param messageColor 颜色 + */ + public static void setMessageColor(@ColorInt int messageColor) { + ToastUtils.messageColor = messageColor; + } + + /** + * 安全地显示短时吐司 + * + * @param text 文本 + */ + public static void showShortSafe(final CharSequence text,int delay) { + sHandler.postDelayed(new Runnable() { + @Override + public void run() { + show(text, Toast.LENGTH_SHORT); + } + },delay); + } + + /** + * 安全地显示短时吐司 + * + * @param text 文本 + */ + public static void showShortSafe(final CharSequence text) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(text, Toast.LENGTH_SHORT); + } + }); + } + + /** + * 安全地显示短时吐司 + * + * @param resId 资源Id + */ + public static void showShortSafe(final @StringRes int resId) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(resId, Toast.LENGTH_SHORT); + } + }); + } + + /** + * 安全地显示短时吐司 + * + * @param resId 资源Id + * @param args 参数 + */ + public static void showShortSafe(final @StringRes int resId, final Object... args) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(resId, Toast.LENGTH_SHORT, args); + } + }); + } + + /** + * 安全地显示短时吐司 + * + * @param format 格式 + * @param args 参数 + */ + public static void showShortSafe(final String format, final Object... args) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(format, Toast.LENGTH_SHORT, args); + } + }); + } + + /** + * 安全地显示长时吐司 + * + * @param text 文本 + */ + public static void showLongSafe(final CharSequence text) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(text, Toast.LENGTH_LONG); + } + }); + } + + /** + * 安全地显示长时吐司 + * + * @param resId 资源Id + */ + public static void showLongSafe(final @StringRes int resId) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(resId, Toast.LENGTH_LONG); + } + }); + } + + /** + * 安全地显示长时吐司 + * + * @param resId 资源Id + * @param args 参数 + */ + public static void showLongSafe(final @StringRes int resId, final Object... args) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(resId, Toast.LENGTH_LONG, args); + } + }); + } + + /** + * 安全地显示长时吐司 + * + * @param format 格式 + * @param args 参数 + */ + public static void showLongSafe(final String format, final Object... args) { + sHandler.post(new Runnable() { + @Override + public void run() { + show(format, Toast.LENGTH_LONG, args); + } + }); + } + + /** + * 显示短时吐司 + * + * @param text 文本 + */ + public static void showShort(CharSequence text) { + show(text, Toast.LENGTH_SHORT); + } + + /** + * 显示短时吐司 + * + * @param resId 资源Id + */ + public static void showShort(@StringRes int resId) { + show(resId, Toast.LENGTH_SHORT); + } + + /** + * 显示短时吐司 + * + * @param resId 资源Id + * @param args 参数 + */ + public static void showShort(@StringRes int resId, Object... args) { + show(resId, Toast.LENGTH_SHORT, args); + } + + /** + * 显示短时吐司 + * + * @param format 格式 + * @param args 参数 + */ + public static void showShort(String format, Object... args) { + show(format, Toast.LENGTH_SHORT, args); + } + + /** + * 显示长时吐司 + * + * @param text 文本 + */ + public static void showLong(CharSequence text) { + show(text, Toast.LENGTH_LONG); + } + + /** + * 显示长时吐司 + * + * @param resId 资源Id + */ + public static void showLong(@StringRes int resId) { + show(resId, Toast.LENGTH_LONG); + } + + /** + * 显示长时吐司 + * + * @param resId 资源Id + * @param args 参数 + */ + public static void showLong(@StringRes int resId, Object... args) { + show(resId, Toast.LENGTH_LONG, args); + } + + /** + * 显示长时吐司 + * + * @param format 格式 + * @param args 参数 + */ + public static void showLong(String format, Object... args) { + show(format, Toast.LENGTH_LONG, args); + } + + /** + * 安全地显示短时自定义吐司 + */ + public static void showCustomShortSafe() { + sHandler.post(new Runnable() { + @Override + public void run() { + show("", Toast.LENGTH_SHORT); + } + }); + } + + /** + * 安全地显示长时自定义吐司 + */ + public static void showCustomLongSafe() { + sHandler.post(new Runnable() { + @Override + public void run() { + show("", Toast.LENGTH_LONG); + } + }); + } + + /** + * 显示短时自定义吐司 + */ + public static void showCustomShort() { + show("", Toast.LENGTH_SHORT); + } + + /** + * 显示长时自定义吐司 + */ + public static void showCustomLong() { + show("", Toast.LENGTH_LONG); + } + + /** + * 显示吐司 + * + * @param resId 资源Id + * @param duration 显示时长 + */ + private static void show(@StringRes int resId, int duration) { + show(App.getInstance().getResources().getText(resId).toString(), duration); + } + + /** + * 显示吐司 + * + * @param resId 资源Id + * @param duration 显示时长 + * @param args 参数 + */ + private static void show(@StringRes int resId, int duration, Object... args) { + show(String.format(App.getInstance().getResources().getString(resId), args), duration); + } + + /** + * 显示吐司 + * + * @param format 格式 + * @param duration 显示时长 + * @param args 参数 + */ + private static void show(String format, int duration, Object... args) { + show(String.format(format, args), duration); + } + + /** + * 显示吐司 + * + * @param text 文本 + * @param duration 显示时长 + */ + private static void show(CharSequence text, int duration) { + if(sViewWeakReference==null){ + setView(R.layout.toast); + } + cancel(); + boolean isCustom = false; + if (sViewWeakReference != null) { + final View view = sViewWeakReference.get(); + if (view != null) { + sToast = new Toast(App.getInstance()); + sToast.setView(view); + sToast.setDuration(duration); + isCustom = true; + } + } + if (!isCustom) { + if (messageColor != DEFAULT_COLOR) { + SpannableString spannableString = new SpannableString(text); + ForegroundColorSpan colorSpan = new ForegroundColorSpan(messageColor); + spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + sToast = Toast.makeText(App.getInstance(), spannableString, duration); + } else { + sToast = Toast.makeText(App.getInstance(), text, duration); + } + } + View view = sToast.getView(); + TextView textView=view.findViewById(R.id.text); + textView.setText(text); + if (bgResource != -1) { + view.setBackgroundResource(bgResource); + } else if (backgroundColor != DEFAULT_COLOR) { + view.setBackgroundColor(backgroundColor); + } + sToast.setGravity(gravity, xOffset, yOffset); + sToast.show(); + } + + /** + * 取消吐司显示 + */ + public static void cancel() { + if (sToast != null) { + sToast.cancel(); + sToast = null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/VoicePlayer.java b/app/src/main/java/com/example/baseframe/utils/VoicePlayer.java new file mode 100644 index 0000000..219ad6f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/VoicePlayer.java @@ -0,0 +1,219 @@ +package com.example.baseframe.utils; + +import android.content.res.AssetFileDescriptor; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.util.Log; + +import androidx.annotation.RawRes; + +import com.blankj.ALog; +import com.example.baseframe.api.App; + +import java.io.IOException; +import java.util.List; + +/** + * 播放本地音频 + * Created by yzh on 2020/1/6 11:00. + */ +public class VoicePlayer implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener { + + private MediaPlayer mediaPlayer; + private boolean isCompletion = false;//是否播放完毕 + private boolean isPrepared = false; + private Listener mListener; + + public void setListener(Listener listener) { + mListener = listener; + } + + private static volatile VoicePlayer mInstance; + public static VoicePlayer get() { + if (mInstance == null) { + synchronized (VoicePlayer.class) { + if (mInstance == null) { + mInstance = new VoicePlayer(); + } + } + } + return mInstance; + } + + private VoicePlayer() { + initPlayer(); + } + + private void initPlayer(){ + if(mediaPlayer==null){ + try { + mediaPlayer = new MediaPlayer(); + mediaPlayer.setLooping(false);//是否循环 + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setOnCompletionListener(this); + mediaPlayer.setOnPreparedListener(this); + } catch (Exception e) { + Log.e("mediaPlayer", "error", e); + } + } + } + + /** + * 播放raw目录下的音乐mp3文件 + * @param rawId R.raw.abc + */ + public void playRawMusic(@RawRes int rawId) { + // mediaPlayer = MediaPlayer.create(PrimaryApplication.get(), rawId);//也可以直接这么创建 + initPlayer(); + AssetFileDescriptor file = App.getInstance().getResources().openRawResourceFd(rawId); + try { + mediaPlayer.reset();//每次播放前最好reset一下 + mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength()); + mediaPlayer.prepare(); + file.close(); + play(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private List mRawIds; + /** + * 连续播放多个raw目录下的音乐mp3文件 + */ + public VoicePlayer playRawList(List rawIds) { + if(CommUtils.isListNull(rawIds)){ + return this; + } + mRawIds=rawIds; + playRawMusic(mRawIds.get(0)); + return this; + } + + /** + * 播放assets下的音乐mp3文件 + * @param fileName abc.mp3 + */ + public void playAssetMusic(String fileName) { + initPlayer(); + try { + //播放 assets/abc.mp3 音乐文件 + AssetFileDescriptor file = App.getInstance().getAssets().openFd(fileName); + //mediaPlayer = new MediaPlayer(); + mediaPlayer.reset(); + mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength()); + mediaPlayer.prepare(); + file.close(); + play(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 根据路径播放 + * @param videoUrl 资源文件名称 本地地址 和网络地址 + */ + public void playUrl(String videoUrl) { + initPlayer(); + try { + mediaPlayer.reset(); + mediaPlayer.setDataSource(videoUrl); + mediaPlayer.prepare(); + play(); + } catch (IllegalArgumentException | IllegalStateException | IOException e) { + e.printStackTrace(); + } + } + + public boolean isPlaying() { + return mediaPlayer.isPlaying(); + } + + /** + * 播放 + */ + public void play() { + if (mediaPlayer != null &&!isPlaying()) { + isCompletion = false; + // mediaPlayer.prepare(); + mediaPlayer.start(); + } + } + + /** + * 暂停 + */ + public void pause() { + if (mediaPlayer.isPlaying()){ + mediaPlayer.pause(); + } + } + + /** + * 释放资源 + */ + public void destory() { + if (mediaPlayer != null) { + mediaPlayer.stop(); + mediaPlayer.release(); + mediaPlayer = null; + if(CommUtils.isListNotNull(mRawIds)){ + mRawIds.clear(); + } + } + } + + /** + * 重播 + */ + public void replay() { + if (mediaPlayer != null) { + mediaPlayer.seekTo(0); + } + } + + @Override + public void onPrepared(MediaPlayer arg0) { + isPrepared = true; + } + + @Override + public void onCompletion(MediaPlayer arg0) { + isCompletion = true; + + //如果存在需要连续播放的资源 + if(CommUtils.isListNotNull(mRawIds)){ + mRawIds.remove(0); + if(CommUtils.isListNotNull(mRawIds)){ + ALog.v("播放完一个移除一个,移除了还有继续播放"); + playRawMusic(mRawIds.get(0)); + }else{ + destory(); + if(mListener!=null){ + mListener.onCompletion(); + } + + ALog.v("播放完毕"); + } + }else{ + destory(); + if(mListener!=null){ + mListener.onCompletion(); + } + ALog.v("播放完毕"); + } + } + + /** + * 是否播放完毕 + */ + public boolean isCompletion() { + return isCompletion; + } + + public interface Listener { + void onCompletion(); + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Attention.java b/app/src/main/java/com/example/baseframe/utils/animations/Attention.java new file mode 100644 index 0000000..e6e1b85 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Attention.java @@ -0,0 +1,146 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; + +/* +Kotlin version of the Attention class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Attention.kt +*/ + +public class Attention{ + public static AnimatorSet Bounce(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "translationY", 0, 0, -30, 0, -15, 0, 0); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet Flash(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "alpha", 1, 0, 1, 0, 1); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet Pulse(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleY", 1, 1.1f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 1.1f, 1); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet RuberBand(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 1, 1.25f, 0.75f, 1.15f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.75f, 1.25f, 0.85f, 1); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet Shake(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "translationX", 0, 25, -25, 25, -25, 15, -15, 6, -6, 0); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet StandUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = (view.getWidth() - view.getPaddingLeft() - view.getPaddingRight()) / 2 + view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "pivotX", x, x, x, x, x); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "pivotY", y, y, y, y, y); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "rotationX", 55, -30, 15, -15, 0); + + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet Swing(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "rotation", 0, 10, -10, 6, -6, 3, -3, 0); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet Tada(View view,int...repeatCount){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.9f, 0.9f, 1.1f, 1.1f, 1.1f, 1.1f, 1.1f, 1.1f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.9f, 0.9f, 1.1f, 1.1f, 1.1f, 1.1f, 1.1f, 1.1f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "rotation", 0, -3, -3, 3, -3, 3, -3, 3, -3, 0); + if(repeatCount!=null&&repeatCount.length>0){ + object1.setRepeatCount(repeatCount[0]); + object2.setRepeatCount(repeatCount[0]); + object3.setRepeatCount(repeatCount[0]); + } + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet Wave(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = (view.getWidth() - view.getPaddingLeft() - view.getPaddingRight()) / 2 + view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "rotation", 12, -12, 3, -3, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "pivotX", x, x, x, x, x); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotY", y, y, y, y, y); + + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet Wobble(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float width = view.getWidth(); + float one = (float) (width / 100.0); + float x = 0 * one; + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "translationX", x, -25 * one, 20 * one, -15 * one, 10 * one, -5 * one, x, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotation", 0, -5, 3, -3, 2, -1, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Bounce.java b/app/src/main/java/com/example/baseframe/utils/animations/Bounce.java new file mode 100644 index 0000000..5599897 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Bounce.java @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; + +/* +Kotlin version of the Bounce class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Bounce.kt +*/ + +public class Bounce { + public static AnimatorSet In(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 0.3f, 1.05f, 0.9f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 0.3f, 1.05f, 0.9f, 1); + + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet InLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = - view.getWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "translationX", width, 30, -10, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1, 1); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = view.getWidth(); + long measured_width = view.getMeasuredWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "translationX", measured_width + width, -30, 10, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1, 1); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long measured_height = view.getMeasuredHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "translationY", measured_height, -30, 10, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1, 1); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long height = - view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", height, 30, -10, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Fade.java b/app/src/main/java/com/example/baseframe/utils/animations/Fade.java new file mode 100644 index 0000000..0b283a2 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Fade.java @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; + +/* +Kotlin version of the Fade class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Fade.kt +*/ + +public class Fade { + + /* + In + */ + + public static AnimatorSet In(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet InLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = - view.getWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationX", width / 4, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = view.getWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationX", width / 4, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long height = view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", height / 4, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long height = - view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", height / 4, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + /* + Out + */ + + public static AnimatorSet Out(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + + animatorSet.playTogether(object); + return animatorSet; + } + + public static AnimatorSet OutLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = - view.getWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationX", 0, width / 4); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long width = view.getWidth(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationX", 0, width / 4); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long height = - view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", 0, height / 4); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + long height = view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", 0, height / 4); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Flip.java b/app/src/main/java/com/example/baseframe/utils/animations/Flip.java new file mode 100644 index 0000000..56f3869 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Flip.java @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; + +/* +Kotlin version of the Flip class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Flip.kt +*/ + +public class Flip { + + /* + In + */ + + public static AnimatorSet InX(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0.25f, 0.5f, 0.75f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotationX", 90, -15, 15, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InY(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0.25f, 0.5f, 0.75f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotationY", 90, -15, 15, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + /* + Out + */ + + public static AnimatorSet OutX(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotationX", 0, 90); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutY(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotationY", 0, 90); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Other.java b/app/src/main/java/com/example/baseframe/utils/animations/Other.java new file mode 100644 index 0000000..e2e2836 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Other.java @@ -0,0 +1,40 @@ +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.view.View; + +/** + * anthor yzh + * time 2019/12/5 14:37 + */ +public class Other { + + + + + public static AnimatorSet pulseAnimator(View view,int...repeatCount) { + AnimatorSet animatorSet = new AnimatorSet(); + ObjectAnimator object1 =ObjectAnimator.ofFloat(view, "scaleY", 1, 1.1f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 1.1f, 1); + + if(repeatCount!=null&&repeatCount.length>0){ + object1.setRepeatCount(repeatCount[0]); + object2.setRepeatCount(repeatCount[0]); + } + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + + public static AnimatorSet Rotation(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view,"rotation",0f,360f); + //objectAnimator.setDuration(2000); + objectAnimator.setRepeatCount(ValueAnimator.INFINITE); + animatorSet.play(objectAnimator); + return animatorSet; + } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Render.java b/app/src/main/java/com/example/baseframe/utils/animations/Render.java new file mode 100644 index 0000000..ca95055 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Render.java @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.content.Context; +import android.view.animation.AccelerateDecelerateInterpolator; + +/* +Kotlin version of the Render class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Render.kt +*/ + +public class Render { + private long duration = 1000; + private Context cx; + private AnimatorSet animatorSet; + + // Render render = new Render(this) + public Render (Context cx) { + this.cx = cx; + } + + // render.setAnimation(Bounce.In(textView)) + public void setAnimation (AnimatorSet animationSet) { + this.animatorSet = animationSet; + } + + // render.start() + public void start() { + animatorSet.setDuration(duration); + animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); + animatorSet.start(); + } + + // render.setDuration(2000) + public void setDuration(long duration){ + this.duration = duration; + } + + +} diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Rotate.java b/app/src/main/java/com/example/baseframe/utils/animations/Rotate.java new file mode 100644 index 0000000..5386fc8 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Rotate.java @@ -0,0 +1,178 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; + +/* +Kotlin version of the Rotate class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Rotate.kt +*/ + +public class Rotate { + + /* + In + */ + + public static AnimatorSet In(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"rotation", -200, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InDownLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"rotation", -90, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"alpha", 0, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view,"pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view,"pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InDownRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getWidth() - view.getPaddingRight(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "rotation", 90, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InUpLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "rotation", 90, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InUpRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getWidth() - view.getPaddingRight(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "rotation", -90, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + /* + Out + */ + + public static AnimatorSet Out(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"rotation", 0, 200); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutDownLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"rotation", 0, 90); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view,"pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view,"pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutDownRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getWidth() - view.getPaddingRight(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotation", 0, -90); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutUpLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getPaddingLeft(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotation", 0, -90); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutUpRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float x = view.getWidth() - view.getPaddingRight(); + float y = view.getHeight() - view.getPaddingBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "rotation", 0, 90); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "pivotX", x, x); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "pivotY", y, y); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/animations/RxAnimation.java b/app/src/main/java/com/example/baseframe/utils/animations/RxAnimation.java new file mode 100644 index 0000000..4b89f8c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/RxAnimation.java @@ -0,0 +1,125 @@ + + +package com.example.baseframe.utils.animations; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import com.blankj.ALog; + +/** + * 属性动画工具类 + * @author yzh 2019/10/30 17:37 + */ +public class RxAnimation { + private static RxAnimation mAnimation; + public static RxAnimation get(){ + if(mAnimation==null){ + synchronized (RxAnimation.class){ + if(mAnimation==null){ + mAnimation=new RxAnimation(); + } + } + } + return mAnimation; + } + + public AnimatorSet getAnimator() { + return animatorSet; + } + + private AnimatorSet animatorSet; + // setAnimation(Bounce.In(textView)) + public RxAnimation setAnimation (AnimatorSet animationSet) { + this.animatorSet = animationSet; + // animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); + animatorSet.setInterpolator(new LinearInterpolator());//匀速 + return mAnimation; + } + + public void start() { + if(animatorSet!=null) { + if (animatorSet.isPaused()) { + animatorSet.resume(); + ALog.w("resume----------"); + } else { + animatorSet.start(); + } + } + } + + public void pause() { + if(animatorSet!=null){ + animatorSet.pause(); + } + } + + public void cancel(View v) { + if(animatorSet!=null){ + animatorSet.cancel(); + reset(v); + } + } + + /** + * 恢复view 默认状态 + * + * @param target + */ + public static void reset(View target) { + target.setAlpha( 1); + target.setScaleX( 1); + target.setScaleY( 1); + target.setTranslationX( 0); + target.setTranslationY( 0); + target.setRotation( 0); + target.setRotationY( 0); + target.setRotationX( 0); + } + + + /** + * 需要 执行动画 并控制隐藏 显示view时设置 + * @param v + * @param type 0执行前显示view 1执行前显示view且执行完后隐藏view + */ + public RxAnimation visableOrGone(View v,int type){ + + if(animatorSet!=null){ + animatorSet.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + v.setVisibility(View.VISIBLE); + } + @Override + public void onAnimationEnd(Animator animation) { + if(type==1){ + v.setVisibility(View.GONE); + } + + } + @Override + public void onAnimationCancel(Animator animation) { } + @Override + public void onAnimationRepeat(Animator animation) { } + }); + }else{ + throw new NullPointerException("请先设置animatorSet!"); + } + return mAnimation; + } + + + + public RxAnimation setDuration(long duration){ + if(animatorSet!=null){ + animatorSet.setDuration(duration); + } + + return mAnimation; + } + + +} diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Slide.java b/app/src/main/java/com/example/baseframe/utils/animations/Slide.java new file mode 100644 index 0000000..cc3d2e8 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Slide.java @@ -0,0 +1,139 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.ViewGroup; + +/* +Kotlin version of the Slide class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Slide.kt +*/ + +public class Slide { + + /* + In + */ + + public static AnimatorSet InDown(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + int distance = view.getTop() + view.getHeight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"translationY", - distance, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getWidth() - view.getLeft(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"translationX", - distance, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getWidth() - view.getLeft(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationX", distance, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet InUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getHeight() - view.getTop(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", distance, 0); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + /* + Out + */ + + public static AnimatorSet OutDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getHeight() - view.getTop(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", 0, distance); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutLeft(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + float right = view.getRight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"translationX", 0, - right); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getWidth() - view.getLeft(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view,"alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view,"translationX", 0, distance); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } + + public static AnimatorSet OutUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float bottom = - view.getBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "translationY", 0, bottom); + + animatorSet.playTogether(object1, object2); + return animatorSet; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/animations/Zoom.java b/app/src/main/java/com/example/baseframe/utils/animations/Zoom.java new file mode 100644 index 0000000..2c72c5e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/animations/Zoom.java @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2019 Gayan Kuruppu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.example.baseframe.utils.animations; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.ViewGroup; + +/* +Kotlin version of the Zoom class +https://github.com/gayankuruppu/android-view-animations-kotlin/blob/master/library/src/main/java/render/animations/Zoom.kt +*/ + +public class Zoom { + + /* + In + */ + + public static AnimatorSet In(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 0.45f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 0.45f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "alpha", 0, 1); + + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet InDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float bottom = - view.getBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.475f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 0.475f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "translationY", bottom, 60, 0); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float right = - view.getRight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.475f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 0.475f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "translationX", right, 48, 0); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float width = view.getWidth(); + float right = view.getPaddingRight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.475f, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 0.475f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "translationX", width + right, -48, 0); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet InUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getHeight() - view.getTop(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 0, 1, 1); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.475f, 1); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 0.475f, 1); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "translationY", distance, -60, 0); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + /* + Out + */ + + public static AnimatorSet Out(View view) { + AnimatorSet animatorSet = new AnimatorSet(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 0, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.3f, 0); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.3f, 0); + + animatorSet.playTogether(object1, object2, object3); + return animatorSet; + } + + public static AnimatorSet OutDown(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getHeight() - view.getTop(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.475f, 0.1f); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.475f, 0.1f); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "translationY", 0, -60, distance); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutLeft(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float right = - view.getRight(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.475f, 0.1f); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.475f, 0.1f); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "translationX", 0, 42, right); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutRight(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + ViewGroup parent = (ViewGroup) view.getParent(); + int distance = parent.getWidth() - parent.getLeft(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.475f, 0.1f); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.475f, 0.1f); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "translationX", 0, -42, distance); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } + + public static AnimatorSet OutUp(View view){ + AnimatorSet animatorSet = new AnimatorSet(); + float bottom = -view.getBottom(); + + ObjectAnimator object1 = ObjectAnimator.ofFloat(view, "alpha", 1, 1, 0); + ObjectAnimator object2 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.475f, 0.1f); + ObjectAnimator object3 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.475f, 0.1f); + ObjectAnimator object4 = ObjectAnimator.ofFloat(view, "translationY", 0, 60, bottom); + + animatorSet.playTogether(object1, object2, object3, object4); + return animatorSet; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/Base64.java b/app/src/main/java/com/example/baseframe/utils/cipher/Base64.java new file mode 100644 index 0000000..bc66812 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/Base64.java @@ -0,0 +1,735 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.baseframe.utils.cipher; + +import java.io.UnsupportedEncodingException; + +/** + * detail: Base64 工具类 + * @author Android + *

+ *     Utilities for encoding and decoding the Base64 representation of
+ *     binary data.  See RFCs 2045 and 3548.
+ * 
+ */ +public final class Base64 { + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + + /** + * Flag to pass to {@link Base64OutputStream} to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + + /* package */ static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int[] DECODE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int[] DECODE_WEBSAFE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Non-data values in the DECODE arrays. + */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + final private int[] alphabet; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3 / 4 + 10; + } + + /** + * Decode another block of input data. + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p + 1] & 0xff] << 12) | + (alphabet[input[p + 2] & 0xff] << 6) | + (alphabet[input[p + 3] & 0xff]))) >= 0) { + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + try { + return new String(encode(input, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + try { + return new String(encode(input, offset, len, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: + break; + case 1: + output_len += 2; + break; + case 2: + output_len += 3; + break; + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + + assert encoder.op == output_len; + + return encoder.output; + } + + /* package */ static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * RFC 2045). + */ + public static final int LINE_GROUPS = 19; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte[] ENCODE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte[] ENCODE_WEBSAFE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + + final private byte[] tail; + /* package */ int tailLen; + private int count; + + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] alphabet; + + public Encoder(int flags, byte[] output) { + this.output = output; + + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8 / 5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + + int p = offset; + len += offset; + int v = -1; + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + switch (tailLen) { + case 0: + // There was no tail. + break; + + case 1: + if (p + 2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + ; + break; + + case 2: + if (p + 1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p + 1] & 0xff) << 8) | + (input[p + 2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op + 1] = alphabet[(v >> 12) & 0x3f]; + output[op + 2] = alphabet[(v >> 6) & 0x3f]; + output[op + 3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p - tailLen == len - 1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p - tailLen == len - 2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len - 1) { + tail[tailLen++] = input[p]; + } else if (p == len - 2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p + 1]; + } + } + + this.op = op; + this.count = count; + + return true; + } + } + + private Base64() { + } // don't instantiate +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/Base64Cipher.java b/app/src/main/java/com/example/baseframe/utils/cipher/Base64Cipher.java new file mode 100644 index 0000000..f8faa2c --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/Base64Cipher.java @@ -0,0 +1,75 @@ +package com.example.baseframe.utils.cipher; + +/** + * detail: Base64 编解码 ( 并进行 ) 加解密 + * @author Ttt + */ +public class Base64Cipher implements Cipher { + + // 中间加密层 + private Cipher mCipher; + // Base64 编解码 flags + private int mFlags = Base64.DEFAULT; + + /** + * 构造函数 + * @param flags Base64 编解码 flags + */ + public Base64Cipher(final int flags) { + this.mFlags = flags; + } + + /** + * 构造函数 + * @param cipher 加解密中间层 + */ + public Base64Cipher(final Cipher cipher) { + this.mCipher = cipher; + } + + /** + * 构造函数 + * @param cipher 加解密中间层 + * @param flags Base64 编解码 flags + */ + public Base64Cipher(final Cipher cipher, final int flags) { + this.mCipher = cipher; + this.mFlags = flags; + } + + // = + + /** + * 解码 + * @param data 待解码数据 + * @return 解码后的 byte[] + */ + @Override + public byte[] decrypt(byte[] data) { + if (data == null) return null; + // 先解码 + data = Base64.decode(data, mFlags); + // 再解密 + if (mCipher != null) { + data = mCipher.decrypt(data); + } + return data; + } + + /** + * 编码 + * @param data 待编码数据 + * @return 编码后的 byte[] + */ + @Override + public byte[] encrypt(byte[] data) { + if (data == null) return null; + // 先加密 + if (mCipher != null) { + data = mCipher.encrypt(data); + } + if (data == null) return null; + // 再编码 + return Base64.encode(data, mFlags); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/Cipher.java b/app/src/main/java/com/example/baseframe/utils/cipher/Cipher.java new file mode 100644 index 0000000..1ac6ecc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/Cipher.java @@ -0,0 +1,8 @@ +package com.example.baseframe.utils.cipher; + +/** + * detail: 通用加解密中间层 + * @author Ttt + */ +public interface Cipher extends Decrypt, Encrypt { +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/CipherUtils.java b/app/src/main/java/com/example/baseframe/utils/cipher/CipherUtils.java new file mode 100644 index 0000000..3273715 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/CipherUtils.java @@ -0,0 +1,59 @@ +package com.example.baseframe.utils.cipher; + +import com.example.baseframe.utils.ConvertUtils; + +/** + * detail: 加密工具类 + * @author Ttt + */ +public final class CipherUtils { + + private CipherUtils() { + } + + /** + * 加密方法 + * @param object 待加密对象 + * @return 加密后的十六进制字符串 + */ + public static String encrypt(final Object object) { + return encrypt(object, null); + } + + /** + * 加密方法 + * @param object 待加密对象 + * @param cipher 加解密中间层 + * @return 加密后的十六进制字符串 + */ + public static String encrypt(final Object object, final Cipher cipher) { + if (object == null) return null; + byte[] bytes = ConvertUtils.objectToBytes(object); + if (cipher != null) bytes = cipher.encrypt(bytes); + return ConvertUtils.toHexString(bytes); + } + + // = + + /** + * 解密方法 + * @param hex 十六进制字符串 + * @return 解密后的对象 + */ + public static Object decrypt(final String hex) { + return decrypt(hex, null); + } + + /** + * 解密方法 + * @param hex 十六进制字符串 + * @param cipher 加解密中间层 + * @return 解密后的对象 + */ + public static Object decrypt(final String hex, final Cipher cipher) { + if (hex == null) return null; + byte[] bytes = ConvertUtils.decodeHex(hex.toCharArray()); + if (cipher != null) bytes = cipher.decrypt(bytes); + return ConvertUtils.bytesToObject(bytes); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/Decrypt.java b/app/src/main/java/com/example/baseframe/utils/cipher/Decrypt.java new file mode 100644 index 0000000..21fbe71 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/Decrypt.java @@ -0,0 +1,15 @@ +package com.example.baseframe.utils.cipher; + +/** + * detail: 解密 ( 解码 ) 接口 + * @author Ttt + */ +public interface Decrypt { + + /** + * 解密 ( 解码 ) 方法 + * @param data 待解码数据 + * @return 解码后的 byte[] + */ + byte[] decrypt(byte[] data); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/cipher/Encrypt.java b/app/src/main/java/com/example/baseframe/utils/cipher/Encrypt.java new file mode 100644 index 0000000..1d7bf73 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/cipher/Encrypt.java @@ -0,0 +1,15 @@ +package com.example.baseframe.utils.cipher; + +/** + * detail: 加密 ( 编码 ) 接口 + * @author Ttt + */ +public interface Encrypt { + + /** + * 加密 ( 编码 ) 方法 + * @param data 待编码数据 + * @return 编码后的 byte[] + */ + byte[] encrypt(byte[] data); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/AESUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/AESUtils.java new file mode 100644 index 0000000..f040258 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/AESUtils.java @@ -0,0 +1,79 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * detail: AES 对称加密工具类 + * @author Ttt + *

+ *     Advanced Encryption Standard 高级数据加密标准 ( 对称加密算法 )
+ *     AES 算法可以有效抵制针对 DES 的攻击算法
+ * 
+ */ +public final class AESUtils { + + private AESUtils() { + } + + // 日志 TAG + private static final String TAG = AESUtils.class.getSimpleName(); + + /** + * 生成密钥 + * @return 密钥 byte[] + */ + public static byte[] initKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); // 192 256 + SecretKey secretKey = keyGen.generateKey(); + return secretKey.getEncoded(); + } catch (Exception e) { + ALog.eTag(TAG, e, "initKey"); + } + return null; + } + + /** + * AES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return 加密后的 byte[] + */ + public static byte[] encrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "encrypt"); + } + return null; + } + + /** + * AES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @return 解密后的 byte[] + */ + public static byte[] decrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "decrypt"); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/CRCUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/CRCUtils.java new file mode 100644 index 0000000..cfe7da1 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/CRCUtils.java @@ -0,0 +1,89 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.zip.CRC32; + + +/** + * detail: CRC 工具类 + * @author Ttt + *
+ *     Cyclic Redundancy Check 循环冗余校验
+ *     CRC 是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数
+ * 
+ */ +public final class CRCUtils { + + private CRCUtils() { + } + + // 日志 TAG + private static final String TAG = CRCUtils.class.getSimpleName(); + + /** + * 获取 CRC32 值 + * @param data 字符串数据 + * @return CRC32 long 值 + */ + public static long getCRC32(final String data) { + if (data == null) return -1L; + try { + CRC32 crc32 = new CRC32(); + crc32.update(data.getBytes()); + return crc32.getValue(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getCRC32"); + } + return -1L; + } + + /** + * 获取 CRC32 值 + * @param data 字符串数据 + * @return CRC32 字符串 + */ + public static String getCRC32ToHexString(final String data) { + if (data == null) return null; + try { + CRC32 crc32 = new CRC32(); + crc32.update(data.getBytes()); + return Long.toHexString(crc32.getValue()); + } catch (Exception e) { + ALog.eTag(TAG, e, "getCRC32ToHexString"); + } + return null; + } + + /** + * 获取文件 CRC32 值 + * @param filePath 文件路径 + * @return 文件 CRC32 值 + */ + public static String getFileCRC32(final String filePath) { + if (filePath == null) return null; + InputStream is = null; + try { + is = new FileInputStream(filePath); + byte[] buffer = new byte[1024]; + CRC32 crc32 = new CRC32(); + int numRead; + while ((numRead = is.read(buffer)) > 0) { + crc32.update(buffer, 0, numRead); + } + return Long.toHexString(crc32.getValue()); + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileCRC32"); + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception e) { + } + } + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/DESUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/DESUtils.java new file mode 100644 index 0000000..5a5092d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/DESUtils.java @@ -0,0 +1,82 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; + +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * detail: DES 对称加密工具类 + * @author Ttt + *
+ *     Data Encryption Standard 数据加密标准 ( 对称加密算法 )
+ * 
+ */ +public final class DESUtils { + + private DESUtils() { + } + + // 日志 TAG + private static final String TAG = DESUtils.class.getSimpleName(); + + /** + * 获取可逆算法 DES 的密钥 + * @param key 前八个字节将被用来生成密钥 + * @return 可逆算法 DES 的密钥 + */ + public static Key getDESKey(final byte[] key) { + if (key == null) return null; + try { + DESKeySpec desKey = new DESKeySpec(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + return keyFactory.generateSecret(desKey); + } catch (Exception e) { + ALog.eTag(TAG, e, "getDESKey"); + } + return null; + } + + /** + * DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return 加密后的 byte[] + */ + public static byte[] encrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "DES"); + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "encrypt"); + } + return null; + } + + /** + * DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @return 解密后的 byte[] + */ + public static byte[] decrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "DES"); + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "decrypt"); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/EncryptUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/EncryptUtils.java new file mode 100644 index 0000000..1d00a50 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/EncryptUtils.java @@ -0,0 +1,994 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; +import com.example.baseframe.utils.ArrayUtils; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.ConvertUtils; +import com.example.baseframe.utils.cipher.Base64; + +import java.io.File; +import java.io.FileInputStream; +import java.security.DigestInputStream; +import java.security.Key; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + + +/** + * detail: 加解密通用工具类 + * @author Blankj + * @author Ttt ( 重写、规范注释、逻辑判断等 ) + */ +public final class EncryptUtils { + + private EncryptUtils() { + } + + // 日志 TAG + private static final String TAG = EncryptUtils.class.getSimpleName(); + + /** + * MD2 加密 + * @param data 待加密数据 + * @return MD2 加密后的数据 + */ + public static byte[] encryptMD2(final byte[] data) { + return hashTemplate(data, "MD2"); + } + + /** + * MD2 加密 + * @param data 待加密数据 + * @return MD2 加密后的十六进制字符串 + */ + public static String encryptMD2ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptMD2ToHexString(data.getBytes()); + } + + /** + * MD2 加密 + * @param data 待加密数据 + * @return MD2 加密后的十六进制字符串 + */ + public static String encryptMD2ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptMD2(data)); + } + + // = + + /** + * MD5 加密 + * @param data 待加密数据 + * @return MD5 加密后的数据 + */ + public static byte[] encryptMD5(final byte[] data) { + return hashTemplate(data, "MD5"); + } + + /** + * MD5 加密 + * @param data 待加密数据 + * @return MD5 加密后的十六进制字符串 + */ + public static String encryptMD5ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptMD5ToHexString(data.getBytes()); + } + + /** + * MD5 加密 + * @param data 待加密数据 + * @param salt salt + * @return MD5 加密后的十六进制字符串 + */ + public static String encryptMD5ToHexString(final String data, final String salt) { + if (data == null && salt == null) return null; + if (salt == null) return ConvertUtils.toHexString(encryptMD5(data.getBytes())); + if (data == null) return ConvertUtils.toHexString(encryptMD5(salt.getBytes())); + return ConvertUtils.toHexString(encryptMD5((data + salt).getBytes())); + } + + // = + + /** + * MD5 加密 + * @param data 待加密数据 + * @return MD5 加密后的十六进制字符串 + */ + public static String encryptMD5ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptMD5(data)); + } + + /** + * MD5 加密 + * @param data 待加密数据 + * @param salt salt + * @return MD5 加密后的十六进制字符串 + */ + public static String encryptMD5ToHexString(final byte[] data, final byte[] salt) { + if (data == null && salt == null) return null; + if (salt == null) return ConvertUtils.toHexString(encryptMD5(data)); + if (data == null) return ConvertUtils.toHexString(encryptMD5(salt)); + // 拼接数据 + byte[] bytes = ArrayUtils.arraycopy(data, salt); + return ConvertUtils.toHexString(encryptMD5(bytes)); + } + + // = + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值 + */ + public static byte[] encryptMD5File(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return encryptMD5File(file); + } + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String encryptMD5FileToHexString(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return encryptMD5FileToHexString(file); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String encryptMD5FileToHexString(final File file) { + return ConvertUtils.toHexString(encryptMD5File(file)); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值 byte[] + */ + public static byte[] encryptMD5File(final File file) { + if (file == null) return null; + DigestInputStream dis = null; + try { + FileInputStream fis = new FileInputStream(file); + MessageDigest digest = MessageDigest.getInstance("MD5"); + dis = new DigestInputStream(fis, digest); + byte[] buffer = new byte[256 * 1024]; + while (true) { + if (!(dis.read(buffer) > 0)) break; + } + digest = dis.getMessageDigest(); + return digest.digest(); + } catch (Exception e) { + ALog.eTag(TAG, e, "encryptMD5File"); + return null; + } finally { + if (dis != null) { + try { + dis.close(); + } catch (Exception e) { + } + } + } + } + + // = + + /** + * SHA1 加密 + * @param data 待加密数据 + * @return SHA1 加密后的数据 + */ + public static byte[] encryptSHA1(final byte[] data) { + return hashTemplate(data, "SHA-1"); + } + + /** + * SHA1 加密 + * @param data 待加密数据 + * @return SHA1 加密后的数据转十六进制字符串 + */ + public static String encryptSHA1ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptSHA1ToHexString(data.getBytes()); + } + + /** + * SHA1 加密 + * @param data 待加密数据 + * @return SHA1 加密后的数据转十六进制字符串 + */ + public static String encryptSHA1ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptSHA1(data)); + } + + // = + + /** + * SHA224 加密 + * @param data 待加密数据 + * @return SHA224 加密后的数据 + */ + public static byte[] encryptSHA224(final byte[] data) { + return hashTemplate(data, "SHA224"); + } + + /** + * SHA224 加密 + * @param data 待加密数据 + * @return SHA224 加密后的数据转十六进制字符串 + */ + public static String encryptSHA224ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptSHA224ToHexString(data.getBytes()); + } + + /** + * SHA224 加密 + * @param data 待加密数据 + * @return SHA224 加密后的数据转十六进制字符串 + */ + public static String encryptSHA224ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptSHA224(data)); + } + + // = + + /** + * SHA256 加密 + * @param data 待加密数据 + * @return SHA256 加密后的数据 + */ + public static byte[] encryptSHA256(final byte[] data) { + return hashTemplate(data, "SHA-256"); + } + + /** + * SHA256 加密 + * @param data 待加密数据 + * @return SHA256 加密后的数据转十六进制字符串 + */ + public static String encryptSHA256ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptSHA256ToHexString(data.getBytes()); + } + + /** + * SHA256 加密 + * @param data 待加密数据 + * @return SHA256 加密后的数据转十六进制 + */ + public static String encryptSHA256ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptSHA256(data)); + } + + // = + + /** + * SHA384 加密 + * @param data 待加密数据 + * @return SHA384 加密后的数据 + */ + public static byte[] encryptSHA384(final byte[] data) { + return hashTemplate(data, "SHA-384"); + } + + /** + * SHA384 加密 + * @param data 待加密数据 + * @return SHA384 加密后的数据转十六进制 + */ + public static String encryptSHA384ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptSHA384ToHexString(data.getBytes()); + } + + /** + * SHA384 加密 + * @param data 待加密数据 + * @return SHA384 加密后的数据转十六进制 + */ + public static String encryptSHA384ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptSHA384(data)); + } + + // = + + /** + * SHA512 加密 + * @param data 待加密数据 + * @return SHA512 加密后的数据 + */ + public static byte[] encryptSHA512(final byte[] data) { + return hashTemplate(data, "SHA-512"); + } + + /** + * SHA512 加密 + * @param data 待加密数据 + * @return SHA512 加密后的数据转十六进制 + */ + public static String encryptSHA512ToHexString(final String data) { + if (data == null || data.length() == 0) return null; + return encryptSHA512ToHexString(data.getBytes()); + } + + /** + * SHA512 加密 + * @param data 待加密数据 + * @return SHA512 加密后的数据转十六进制 + */ + public static String encryptSHA512ToHexString(final byte[] data) { + return ConvertUtils.toHexString(encryptSHA512(data)); + } + + /** + * Hash 加密模版方法 + * @param data 待加密数据 + * @param algorithm 算法 + * @return 指定加密算法加密后的数据 + */ + public static byte[] hashTemplate(final byte[] data, final String algorithm) { + if (data == null || data.length == 0) return null; + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + digest.update(data); + return digest.digest(); + } catch (Exception e) { + ALog.eTag(TAG, e, "hashTemplate"); + return null; + } + } + + // = + + /** + * HmacMD5 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacMD5 加密后的数据 + */ + public static byte[] encryptHmacMD5(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacMD5"); + } + + /** + * HmacMD5 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacMD5 加密后的数据转十六进制 + */ + public static String encryptHmacMD5ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacMD5ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacMD5 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacMD5 加密后的数据转十六进制 + */ + public static String encryptHmacMD5ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacMD5(data, key)); + } + + // = + + /** + * HmacSHA1 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA1 加密后的数据 + */ + public static byte[] encryptHmacSHA1(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacSHA1"); + } + + /** + * HmacSHA1 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA1 加密后的数据转十六进制 + */ + public static String encryptHmacSHA1ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacSHA1ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacSHA1 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA1 加密后的数据转十六进制 + */ + public static String encryptHmacSHA1ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacSHA1(data, key)); + } + + // = + + /** + * HmacSHA224 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA224 加密后的数据 + */ + public static byte[] encryptHmacSHA224(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacSHA224"); + } + + /** + * HmacSHA224 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA224 加密后的数据转十六进制 + */ + public static String encryptHmacSHA224ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacSHA224ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacSHA224 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA224 加密后的数据转十六进制 + */ + public static String encryptHmacSHA224ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacSHA224(data, key)); + } + + // = + + /** + * HmacSHA256 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA256 加密后的数据 + */ + public static byte[] encryptHmacSHA256(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacSHA256"); + } + + /** + * HmacSHA256 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA256 加密后的数据转十六进制 + */ + public static String encryptHmacSHA256ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacSHA256ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacSHA256 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA256 加密后的数据转十六进制 + */ + public static String encryptHmacSHA256ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacSHA256(data, key)); + } + + // = + + /** + * HmacSHA384 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA384 加密后的数据 + */ + public static byte[] encryptHmacSHA384(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacSHA384"); + } + + /** + * HmacSHA384 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA384 加密后的数据转十六进制 + */ + public static String encryptHmacSHA384ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacSHA384ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacSHA384 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA384 加密后的数据转十六进制 + */ + public static String encryptHmacSHA384ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacSHA384(data, key)); + } + + // = + + /** + * HmacSHA512 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA512 加密后的数据 + */ + public static byte[] encryptHmacSHA512(final byte[] data, final byte[] key) { + return hmacTemplate(data, key, "HmacSHA512"); + } + + /** + * HmacSHA512 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA512 加密后的数据转十六进制 + */ + public static String encryptHmacSHA512ToHexString(final String data, final String key) { + if (data == null || data.length() == 0 || key == null || key.length() == 0) return null; + return encryptHmacSHA512ToHexString(data.getBytes(), key.getBytes()); + } + + /** + * HmacSHA512 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return HmacSHA512 加密后的数据转十六进制 + */ + public static String encryptHmacSHA512ToHexString(final byte[] data, final byte[] key) { + return ConvertUtils.toHexString(encryptHmacSHA512(data, key)); + } + + /** + * Hmac 加密模版方法 + * @param data 待加密数据 + * @param key 密钥 + * @param algorithm 算法 + * @return 指定加密算法和密钥, 加密后的数据 + */ + public static byte[] hmacTemplate(final byte[] data, final byte[] key, final String algorithm) { + if (data == null || data.length == 0 || key == null || key.length == 0) return null; + try { + SecretKeySpec secretKey = new SecretKeySpec(key, algorithm); + Mac mac = Mac.getInstance(algorithm); + mac.init(secretKey); + return mac.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "hmacTemplate"); + return null; + } + } + + // = + + /** + * DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return DES 加密后的数据 + */ + public static byte[] encryptDES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "DES", transformation, iv, true); + } + + /** + * DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return DES 加密后的数据转 Base64 + */ + public static byte[] encryptDESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return base64Encode(encryptDES(data, key, transformation, iv)); + } + + /** + * DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return DES 加密后的数据转十六进制 + */ + public static String encryptDESToHexString(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return ConvertUtils.toHexString(encryptDES(data, key, transformation, iv)); + } + + // = + + /** + * DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return DES 解密后的数据 + */ + public static byte[] decryptDES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "DES", transformation, iv, false); + } + + /** + * DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return Base64 解码后, 在进行 DES 解密后的数据 + */ + public static byte[] decryptDESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return decryptDES(base64Decode(data), key, transformation, iv); + } + + /** + * DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 十六进制转换后, 在进行 DES 解密后的数据 + */ + public static byte[] decryptDESToHexString(final String data, final byte[] key, final String transformation, final byte[] iv) { + return decryptDES(ConvertUtils.decodeHex(data), key, transformation, iv); + } + + // = + + /** + * 3DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 3DES 加密后的数据 + */ + public static byte[] encrypt3DES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "DESede", transformation, iv, true); + } + + /** + * 3DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 3DES 加密后的数据转 Base64 + */ + public static byte[] encrypt3DESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return base64Encode(encrypt3DES(data, key, transformation, iv)); + } + + /** + * 3DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 3DES 加密后的数据转十六进制 + */ + public static String encrypt3DESToHexString(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return ConvertUtils.toHexString(encrypt3DES(data, key, transformation, iv)); + } + + // = + + /** + * 3DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 3DES 解密后的数据 + */ + public static byte[] decrypt3DES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "DESede", transformation, iv, false); + } + + /** + * 3DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return Base64 解码后, 在进行 3DES 解密后的数据 + */ + public static byte[] decrypt3DESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return decrypt3DES(base64Decode(data), key, transformation, iv); + } + + /** + * 3DES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 十六进制转换后, 在进行 3DES 解密后的数据 + */ + public static byte[] decrypt3DESToHexString(final String data, final byte[] key, final String transformation, final byte[] iv) { + return decrypt3DES(ConvertUtils.decodeHex(data), key, transformation, iv); + } + + // = + + /** + * AES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return AES 加密后的数据 + */ + public static byte[] encryptAES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "AES", transformation, iv, true); + } + + /** + * AES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return AES 加密后的数据转 Base64 + */ + public static byte[] encryptAESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return base64Encode(encryptAES(data, key, transformation, iv)); + } + + /** + * AES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return AES 加密后的数据转十六进制 + */ + public static String encryptAESToHexString(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return ConvertUtils.toHexString(encryptAES(data, key, transformation, iv)); + } + + // = + + /** + * AES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return AES 解密后的数据 + */ + public static byte[] decryptAES(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return symmetricTemplate(data, key, "AES", transformation, iv, false); + } + + /** + * AES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return Base64 解码后, 在进行 AES 解密后的数据 + */ + public static byte[] decryptAESToBase64(final byte[] data, final byte[] key, final String transformation, final byte[] iv) { + return decryptAES(base64Decode(data), key, transformation, iv); + } + + /** + * AES 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @return 十六进制转换后, 在进行 AES 解密后的数据 + */ + public static byte[] decryptAESToHexString(final String data, final byte[] key, final String transformation, final byte[] iv) { + return decryptAES(ConvertUtils.decodeHex(data), key, transformation, iv); + } + + /** + * 对称加密模版方法 + * @param data 待加解密数据 + * @param key 密钥 + * @param algorithm 算法 + * @param transformation {@link Cipher#getInstance} transformation + * @param iv 算法参数 {@link AlgorithmParameterSpec} + * @param isEncrypt 是否加密处理 + * @return 指定加密算法, 加解密后的数据 + */ + public static byte[] symmetricTemplate(final byte[] data, final byte[] key, final String algorithm, + final String transformation, final byte[] iv, final boolean isEncrypt) { + if (data == null || data.length == 0 || key == null || key.length == 0) return null; + try { + SecretKey secretKey; + if ("DES".equals(algorithm)) { + DESKeySpec desKey = new DESKeySpec(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); + secretKey = keyFactory.generateSecret(desKey); + } else { + secretKey = new SecretKeySpec(key, algorithm); + } + Cipher cipher = Cipher.getInstance(transformation); + if (iv == null || iv.length == 0) { + cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey); + } else { + AlgorithmParameterSpec params = new IvParameterSpec(iv); + cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, params); + } + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "symmetricTemplate"); + return null; + } + } + + // = + + /** + * RSA 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return RSA 加密后的数据 + */ + public static byte[] encryptRSA(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation) { + return rsaTemplate(data, key, isPublicKey, transformation, true); + } + + /** + * RSA 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return RSA 加密后的数据转 Base64 + */ + public static byte[] encryptRSAToBase64(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation) { + return base64Encode(encryptRSA(data, key, isPublicKey, transformation)); + } + + /** + * RSA 加密 + * @param data 待加密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return RSA 加密后的数据转十六进制 + */ + public static String encryptRSAToHexString(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation) { + return ConvertUtils.toHexString(encryptRSA(data, key, isPublicKey, transformation)); + } + + // = + + /** + * RSA 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return RSA 解密后的数据 + */ + public static byte[] decryptRSA(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation) { + return rsaTemplate(data, key, isPublicKey, transformation, false); + } + + /** + * RSA 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return Base64 解码后, 在进行 RSA 解密后的数据 + */ + public static byte[] decryptRSAToBase64(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation) { + return decryptRSA(base64Decode(data), key, isPublicKey, transformation); + } + + /** + * RSA 解密 + * @param data 待解密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @return 十六进制转换后, 在进行 RSA 解密后的数据 + */ + public static byte[] decryptRSAToHexString(final String data, final byte[] key, final boolean isPublicKey, final String transformation) { + return decryptRSA(ConvertUtils.decodeHex(data), key, isPublicKey, transformation); + } + + /** + * RSA 加解密模版方法 + * @param data 待加解密数据 + * @param key 密钥 + * @param isPublicKey {@code true} {@link X509EncodedKeySpec}, {@code false} {@link PKCS8EncodedKeySpec} + * @param transformation {@link Cipher#getInstance} transformation + * @param isEncrypt 是否加密处理 + * @return 指定加密算法, 加解密后的数据 + */ + public static byte[] rsaTemplate(final byte[] data, final byte[] key, final boolean isPublicKey, final String transformation, final boolean isEncrypt) { + if (data == null || key == null) return null; + try { + int dataLength = data.length; + int keyLength = key.length; + if (dataLength == 0 || keyLength == 0) return null; + + Key rsaKey; + if (isPublicKey) { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key); + rsaKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); + } else { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); + rsaKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); + } + if (rsaKey == null) return null; + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, rsaKey); + int maxLen = isEncrypt ? 117 : 128; + int count = dataLength / maxLen; + if (count > 0) { + byte[] ret = new byte[0]; + byte[] buffer = new byte[maxLen]; + int index = 0; + for (int i = 0; i < count; i++) { + System.arraycopy(data, index, buffer, 0, maxLen); + ret = ArrayUtils.arraycopy(ret, cipher.doFinal(buffer)); + index += maxLen; + } + if (index != dataLength) { + int restLen = dataLength - index; + buffer = new byte[restLen]; + System.arraycopy(data, index, buffer, 0, restLen); + ret = ArrayUtils.arraycopy(ret, cipher.doFinal(buffer)); + } + return ret; + } else { + return cipher.doFinal(data); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "rsaTemplate"); + } + return null; + } + + // ============ + // = 私有方法 = + // ============ + + /** + * Base64 编码 + * @param input 待编码数据 + * @return Base64 编码后的 byte[] + */ + private static byte[] base64Encode(final byte[] input) { + if (input == null) return null; + return Base64.encode(input, Base64.NO_WRAP); + } + + /** + * Base64 解码 + * @param input 待解码数据 + * @return Base64 解码后的 byte[] + */ + private static byte[] base64Decode(final byte[] input) { + if (input == null) return null; + return Base64.decode(input, Base64.NO_WRAP); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/EscapeUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/EscapeUtils.java new file mode 100644 index 0000000..1ec4514 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/EscapeUtils.java @@ -0,0 +1,141 @@ +package com.example.baseframe.utils.encrypt; + +/** + * detail: 字符串 ( 编解码 ) 工具类 + * @author Ttt + */ +public final class EscapeUtils { + + private EscapeUtils() { + } + + /** + * 编码 + * @param data 待编码数据 + * @return 编码后的字符串 + */ + public static String escape(final String data) { + if (data == null) return null; + StringBuilder builder = new StringBuilder(); + for (int i = 0, len = data.length(); i < len; i++) { + int ch = data.charAt(i); + if ('A' <= ch && ch <= 'Z') { + builder.append((char) ch); + } else if ('a' <= ch && ch <= 'z') { + builder.append((char) ch); + } else if ('0' <= ch && ch <= '9') { + builder.append((char) ch); + } else if (ch == '-' || ch == '_' || ch == '.' || ch == '!' + || ch == '~' || ch == '*' || ch == '\'' || ch == '(' + || ch == ')') { + builder.append((char) ch); + } else if (ch <= 0x007F) { + builder.append('%'); + builder.append(HEX[ch]); + } else { + builder.append('%'); + builder.append('u'); + builder.append(HEX[(ch >>> 8)]); + builder.append(HEX[(0x00FF & ch)]); + } + } + return builder.toString(); + } + + /** + * 解码 - 本方法不论参数 data 是否经过 escape() 编码, 均能获取正确的 ( 解码 ) 结果 + * @param data 待解码数据 + * @return 解码后的字符串 + */ + public static String unescape(final String data) { + if (data == null) return null; + StringBuilder builder = new StringBuilder(); + int i = 0; + int len = data.length(); + while (i < len) { + int ch = data.charAt(i); + if ('A' <= ch && ch <= 'Z') { + builder.append((char) ch); + } else if ('a' <= ch && ch <= 'z') { + builder.append((char) ch); + } else if ('0' <= ch && ch <= '9') { + builder.append((char) ch); + } else if (ch == '-' || ch == '_' || ch == '.' || ch == '!' + || ch == '~' || ch == '*' || ch == '\'' || ch == '(' + || ch == ')') { + builder.append((char) ch); + } else if (ch == '%') { + int cint = 0; + if ('u' != data.charAt(i + 1)) { + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 1)]; + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 2)]; + i += 2; + } else { + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 2)]; + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 3)]; + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 4)]; + cint = (cint << 4) | BYTE_VALUES[data.charAt(i + 5)]; + i += 5; + } + builder.append((char) cint); + } else { + builder.append((char) ch); + } + i++; + } + return builder.toString(); + } + + // = + + // 十六进制 0-255 + private static final String[] HEX = {"00", "01", "02", "03", "04", "05", + "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", "10", + "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", + "1C", "1D", "1E", "1F", "20", "21", "22", "23", "24", "25", "26", + "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F", "30", "31", + "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", + "3D", "3E", "3F", "40", "41", "42", "43", "44", "45", "46", "47", + "48", "49", "4A", "4B", "4C", "4D", "4E", "4F", "50", "51", "52", + "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", + "5E", "5F", "60", "61", "62", "63", "64", "65", "66", "67", "68", + "69", "6A", "6B", "6C", "6D", "6E", "6F", "70", "71", "72", "73", + "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", + "7F", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", + "8A", "8B", "8C", "8D", "8E", "8F", "90", "91", "92", "93", "94", + "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F", + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", + "AB", "AC", "AD", "AE", "AF", "B0", "B1", "B2", "B3", "B4", "B5", + "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF", "C0", + "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", + "CC", "CD", "CE", "CF", "D0", "D1", "D2", "D3", "D4", "D5", "D6", + "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF", "E0", "E1", + "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", + "ED", "EE", "EF", "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", + "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF"}; + + private static final byte[] BYTE_VALUES = {0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F}; +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/MD5Utils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/MD5Utils.java new file mode 100644 index 0000000..ab15fde --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/MD5Utils.java @@ -0,0 +1,158 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.ConvertUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.security.DigestInputStream; +import java.security.MessageDigest; + +/** + * detail: MD5 加密工具类 + * @author Ttt + *
+ *     Message Digest 消息摘要算法
+ * 
+ */ +public final class MD5Utils { + + private MD5Utils() { + } + + // 日志 TAG + private static final String TAG = MD5Utils.class.getSimpleName(); + + /** + * 加密内容 (32 位小写 MD5) + * @param data 待加密数据 + * @return MD5 加密后的字符串 + */ + public static String md5(final String data) { + if (data == null) return null; + try { + return md5(data.getBytes()); + } catch (Exception e) { + ALog.eTag(TAG, e, "md5"); + } + return null; + } + + /** + * 加密内容 (32 位小写 MD5) + * @param data 待加密数据 + * @return MD5 加密后的字符串 + */ + public static String md5(final byte[] data) { + if (data == null) return null; + try { + // 获取 MD5 摘要算法的 MessageDigest 对象 + MessageDigest digest = MessageDigest.getInstance("MD5"); + // 使用指定的字节更新摘要 + digest.update(data); + // 获取密文 + return ConvertUtils.toHexString(digest.digest(), true); + } catch (Exception e) { + ALog.eTag(TAG, e, "md5"); + } + return null; + } + + /** + * 加密内容 (32 位大写 MD5) + * @param data 待加密数据 + * @return MD5 加密后的字符串 + */ + public static String md5Upper(final String data) { + if (data == null) return null; + try { + return md5Upper(data.getBytes()); + } catch (Exception e) { + ALog.eTag(TAG, e, "md5Upper"); + } + return null; + } + + /** + * 加密内容 (32 位大写 MD5) + * @param data 待加密数据 + * @return MD5 加密后的字符串 + */ + public static String md5Upper(final byte[] data) { + if (data == null) return null; + try { + // 获取 MD5 摘要算法的 MessageDigest 对象 + MessageDigest digest = MessageDigest.getInstance("MD5"); + // 使用指定的字节更新摘要 + digest.update(data); + // 获取密文 + return ConvertUtils.toHexString(digest.digest(), false); + } catch (Exception e) { + ALog.eTag(TAG, e, "md5Upper"); + } + return null; + } + + // = + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值 + */ + public static byte[] getFileMD5(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return getFileMD5(file); + } + + /** + * 获取文件 MD5 值 + * @param filePath 文件路径 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String getFileMD5ToHexString(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return getFileMD5ToHexString(file); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值转十六进制字符串 + */ + public static String getFileMD5ToHexString(final File file) { + return ConvertUtils.toHexString(getFileMD5(file)); + } + + /** + * 获取文件 MD5 值 + * @param file 文件 + * @return 文件 MD5 值 byte[] + */ + public static byte[] getFileMD5(final File file) { + if (file == null) return null; + DigestInputStream dis = null; + try { + FileInputStream fis = new FileInputStream(file); + MessageDigest digest = MessageDigest.getInstance("MD5"); + dis = new DigestInputStream(fis, digest); + byte[] buffer = new byte[256 * 1024]; + while (true) { + if (!(dis.read(buffer) > 0)) break; + } + digest = dis.getMessageDigest(); + return digest.digest(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileMD5"); + return null; + } finally { + if (dis != null) { + try { + dis.close(); + } catch (Exception e) { + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/SHAUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/SHAUtils.java new file mode 100644 index 0000000..0ba77ac --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/SHAUtils.java @@ -0,0 +1,164 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; +import com.example.baseframe.utils.CommUtils; +import com.example.baseframe.utils.ConvertUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; + + +/** + * detail: SHA 加密工具类 + * @author Ttt + */ +public final class SHAUtils { + + private SHAUtils() { + } + + // 日志 TAG + private static final String TAG = SHAUtils.class.getSimpleName(); + + /** + * 加密内容 SHA1 + * @param data 待加密数据 + * @return SHA1 加密后的字符串 + */ + public static String sha1(final String data) { + return shaHex(data, "SHA-1"); + } + + /** + * 加密内容 SHA224 + * @param data 待加密数据 + * @return SHA224 加密后的字符串 + */ + public static String sha224(final String data) { + return shaHex(data, "SHA-224"); + } + + /** + * 加密内容 SHA256 + * @param data 待加密数据 + * @return SHA256 加密后的字符串 + */ + public static String sha256(final String data) { + return shaHex(data, "SHA-256"); + } + + /** + * 加密内容 SHA384 + * @param data 待加密数据 + * @return SHA384 加密后的字符串 + */ + public static String sha384(final String data) { + return shaHex(data, "SHA-384"); + } + + /** + * 加密内容 SHA512 + * @param data 待加密数据 + * @return SHA512 加密后的字符串 + */ + public static String sha512(final String data) { + return shaHex(data, "SHA-512"); + } + + // = + + /** + * 获取文件 SHA1 值 + * @param filePath 文件路径 + * @return 文件 SHA1 字符串信息 + */ + public static String getFileSHA1(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return getFileSHA(file, "SHA-1"); + } + + /** + * 获取文件 SHA1 值 + * @param file 文件 + * @return 文件 SHA1 字符串信息 + */ + public static String getFileSHA1(final File file) { + return getFileSHA(file, "SHA-1"); + } + + /** + * 获取文件 SHA256 值 + * @param filePath 文件路径 + * @return 文件 SHA256 字符串信息 + */ + public static String getFileSHA256(final String filePath) { + File file = CommUtils.isEmpty(filePath) ? null : new File(filePath); + return getFileSHA(file, "SHA-256"); + } + + /** + * 获取文件 SHA256 值 + * @param file 文件 + * @return 文件 SHA256 字符串信息 + */ + public static String getFileSHA256(final File file) { + return getFileSHA(file, "SHA-256"); + } + + // = + + /** + * 加密内容 SHA 模板 + * @param data 待加密数据 + * @param algorithm 算法 + * @return SHA 算法加密后的字符串 + */ + public static String shaHex(final String data, final String algorithm) { + if (data == null || algorithm == null) return null; + try { + byte[] bytes = data.getBytes(); + // 获取 SHA-1 摘要算法的 MessageDigest 对象 + MessageDigest digest = MessageDigest.getInstance(algorithm); + // 使用指定的字节更新摘要 + digest.update(bytes); + // 获取密文 + return ConvertUtils.toHexString(digest.digest(), true); + } catch (Exception e) { + ALog.eTag(TAG, e, "shaHex"); + } + return null; + } + + /** + * 获取文件 SHA 值 + * @param file 文件 + * @param algorithm 算法 + * @return 文件指定 SHA 字符串信息 + */ + public static String getFileSHA(final File file, final String algorithm) { + if (file == null || algorithm == null) return null; + InputStream is = null; + try { + is = new FileInputStream(file); + byte[] buffer = new byte[1024]; + MessageDigest digest = MessageDigest.getInstance(algorithm); + int numRead = 0; + while ((numRead = is.read(buffer)) > 0) { + digest.update(buffer, 0, numRead); + } + return ConvertUtils.toHexString(digest.digest(), true); + } catch (Exception e) { + ALog.eTag(TAG, e, "getFileSHA"); + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception e) { + } + } + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/TripleDESUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/TripleDESUtils.java new file mode 100644 index 0000000..7a68bd2 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/TripleDESUtils.java @@ -0,0 +1,79 @@ +package com.example.baseframe.utils.encrypt; + +import com.blankj.ALog; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + + +/** + * detail: 3DES 对称加密工具类 + * @author Ttt + *
+ *     Triple DES、DESede 进行了三重 DES 加密的算法 ( 对称加密算法 )
+ * 
+ */ +public final class TripleDESUtils { + + private TripleDESUtils() { + } + + // 日志 TAG + private static final String TAG = TripleDESUtils.class.getSimpleName(); + + /** + * 生成密钥 + * @return 密钥 byte[] + */ + public static byte[] initKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance("DESede"); + keyGen.init(168); // 112 168 + SecretKey secretKey = keyGen.generateKey(); + return secretKey.getEncoded(); + } catch (Exception e) { + ALog.eTag(TAG, e, "initKey"); + } + return null; + } + + /** + * 3DES 加密 + * @param data 待加密数据 + * @param key 密钥 + * @return 加密后的 byte[] + */ + public static byte[] encrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "DESede"); + Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "encrypt"); + } + return null; + } + + /** + * 3DES 解密 + * @param data 待加密数据 + * @param key 密钥 + * @return 解密后的 byte[] + */ + public static byte[] decrypt(final byte[] data, final byte[] key) { + if (data == null || key == null) return null; + try { + SecretKey secretKey = new SecretKeySpec(key, "DESede"); + Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return cipher.doFinal(data); + } catch (Exception e) { + ALog.eTag(TAG, e, "decrypt"); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/encrypt/XorUtils.java b/app/src/main/java/com/example/baseframe/utils/encrypt/XorUtils.java new file mode 100644 index 0000000..02999da --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/encrypt/XorUtils.java @@ -0,0 +1,77 @@ +package com.example.baseframe.utils.encrypt; + +/** + * detail: 异或 ( 加密 ) 工具类 + * @author Ttt + *
+ *     位运算可以实现很多高级、高效的运算
+ *     可用于 IM 二进制数据包加密
+ *     1. 能够实现加密
+ *     2. 采用异或加密算法不会改变二进制数据的长度这对二进制数据包封包起到不小的好处
+ *     也可用于记事本等场景
+ *     

+ * 参考链接 + * @see + *
+ */ +public final class XorUtils { + + private XorUtils() { + } + + /** + * 加解密 ( 固定 Key 方式 ) 这种方式 加解密 方法共用 + *
+     *     加密: byte[] bytes = encryptAsFix("123".getBytes());
+     *     解密: String str = new String(encryptAsFix(bytes));
+     * 
+ * @param data 待加解密数据 + * @return 加解密后的数据 byte[] + */ + public static byte[] encryptAsFix(final byte[] data) { + if (data == null) return null; + int len = data.length; + if (len == 0) return null; + int key = 0x12; + for (int i = 0; i < len; i++) { + data[i] ^= key; + } + return data; + } + + // = + + /** + * 加密 ( 非固定 Key 方式 ) + * @param data 待加密数据 + * @return 加密后的数据 byte[] + */ + public static byte[] encrypt(final byte[] data) { + if (data == null) return null; + int len = data.length; + if (len == 0) return null; + int key = 0x12; + for (int i = 0; i < len; i++) { + data[i] = (byte) (data[i] ^ key); + key = data[i]; + } + return data; + } + + /** + * 解密 ( 非固定 Key 方式 ) + * @param data 待解密数据 + * @return 解密后的数据 byte[] + */ + public static byte[] decrypt(final byte[] data) { + if (data == null) return null; + int len = data.length; + if (len == 0) return null; + int key = 0x12; + for (int i = len - 1; i > 0; i--) { + data[i] = (byte) (data[i] ^ data[i - 1]); + } + data[0] = (byte) (data[0] ^ key); + return data; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/glide/GlideCircleTransform.java b/app/src/main/java/com/example/baseframe/utils/glide/GlideCircleTransform.java new file mode 100644 index 0000000..ed4d7bd --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/glide/GlideCircleTransform.java @@ -0,0 +1,50 @@ +package com.example.baseframe.utils.glide; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; + +import java.security.MessageDigest; + +/** + * Created by jingbin on 2016/12/22. + */ + +public class GlideCircleTransform extends BitmapTransformation { + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { + return circleCrop(pool, toTransform); + } + + private static Bitmap circleCrop(BitmapPool pool, Bitmap source) { + if (source == null) return null; + int size = Math.min(source.getWidth(), source.getHeight()); + int x = (source.getWidth() - size) / 2; + int y = (source.getHeight() - size) / 2; + // TODO this could be acquired from the pool too + Bitmap squared = Bitmap.createBitmap(source, x, y, size, size); + Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888); + if (result == null) { + result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + } + Canvas canvas = new Canvas(result); + Paint paint = new Paint(); + paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); + paint.setAntiAlias(true); + float r = size / 2f; + canvas.drawCircle(r, r, r, paint); + return result; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/glide/GlideRoundTransform.java b/app/src/main/java/com/example/baseframe/utils/glide/GlideRoundTransform.java new file mode 100644 index 0000000..ad1f2ba --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/glide/GlideRoundTransform.java @@ -0,0 +1,71 @@ +package com.example.baseframe.utils.glide; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; + +import java.security.MessageDigest; + +public class GlideRoundTransform extends CenterCrop { + + private float radius; + public GlideRoundTransform(Context context) { + this(context,10); + } + public GlideRoundTransform(Context context, int radius) { + this.radius = dip2px(context,radius); + //this.radius = radius; + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { + //glide4.0+ + Bitmap transform = super.transform(pool, toTransform, outWidth, outHeight); + return roundCrop(pool, transform); + //glide3.0 + //return roundCrop(pool, toTransform); + } + + private Bitmap roundCrop(BitmapPool pool, Bitmap source) { + if (source == null) return null; + + Bitmap result = pool.get(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888); + if (result == null) { + result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(result); + Paint paint = new Paint(); + paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); + paint.setAntiAlias(true); + RectF rectF = new RectF(0f, 0f, source.getWidth(), source.getHeight()); + canvas.drawRoundRect(rectF, radius, radius, paint); + return result; + } + + public String getId() { + return getClass().getName() + Math.round(radius); + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) { + + } + + public static int pxTodp(Context context, float px) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (px / scale + 0.5f); + } + + public static int dip2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + +} diff --git a/app/src/main/java/com/example/baseframe/utils/glide/GlideUtil.java b/app/src/main/java/com/example/baseframe/utils/glide/GlideUtil.java new file mode 100644 index 0000000..1f85dd7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/glide/GlideUtil.java @@ -0,0 +1,195 @@ +package com.example.baseframe.utils.glide; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import androidx.annotation.DrawableRes; +import androidx.databinding.BindingAdapter; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.Transformation; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.example.baseframe.R; + +import static com.example.baseframe.utils.DensityUtil.dip2px; + + +/** + * @author yzh + * @date 2019/04/29 + */ + +public class GlideUtil { + + +// DiskCacheStrategy.NONE: 表示不缓存任何内容。 +// DiskCacheStrategy.DATA: 表示只缓存原始图片。 +// DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。 +// DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。 +// DiskCacheStrategy.AUTOMATIC: 表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。 + /** + * 将gif图转换为静态图 + */ + public static void displayasBitmap(String url, ImageView imageView) { + + Glide.with(imageView.getContext()) + .asBitmap() + .load(url) + .placeholder(R.drawable.shape_bg_loading) + .error(R.drawable.shape_bg_loading) +// .skipMemoryCache(true) //跳过内存缓存 + .diskCacheStrategy(DiskCacheStrategy.ALL) + .centerCrop() + .into(imageView); + } + + + /** + *一般加载 + * @param url + * @param type 加载失败默认图类型 + */ + @BindingAdapter({"displayImg","displayType"}) + public static void displayImg(ImageView imageView, String url,int type) { + Glide.with(imageView.getContext()) + .load(url) + .centerCrop() + .placeholder(getDefaultPic(type)) + .error(getDefaultPic(type)) + .into(imageView); + } + + /** + * 加载圆角图片(实现加载的图片和默认加载失败(占位图)都处理成指定圆角效果) + * @param url + * @param type 加载失败默认图类型 + * @param radius 圆角度数 + */ + @BindingAdapter(value = {"displayRound","displayType","radius"},requireAll = true) + public static void displayRoundImg(ImageView imageView, String url,int type,int radius) { + //这种加载成功才会显示圆角 +// Glide.with(imageView.getContext()) +// .load(url) +// .centerCrop() +// .apply(RequestOptions.bitmapTransform(new RoundedCorners(radius))) +// .transition(DrawableTransitionOptions.withCrossFade(500)) +// .placeholder(getDefaultPic(type)) +// .error(getDefaultPic(type)) +// .into(imageView); + + int placeholderId=getDefaultPic(type); + int errorId=getDefaultPic(type); + Transformation transform=new GlideRoundTransform(imageView.getContext(),radius); + //Transformation transform=new RoundedCorners(radius); + Glide.with(imageView.getContext()).load(url) + .apply(new RequestOptions() + .placeholder(placeholderId) + .error(errorId) + .centerCrop() + .transform(transform)) + // .transition(DrawableTransitionOptions.withCrossFade(500)) + .thumbnail(loadTransform(imageView.getContext(),placeholderId,transform)) + .thumbnail(loadTransform(imageView.getContext(),errorId,transform)) + .into(imageView); + + } + private static RequestBuilder loadTransform(Context context, @DrawableRes int placeholderId + , Transformation transform ) { + return Glide.with(context) + .load(placeholderId) + .apply(new RequestOptions().centerCrop() + .transform(transform)); + } + + + /** + * 加载圆形图,暂时用到显示头像(实现加载的图片和默认加载失败(占位图)都处理成指定圆形效果) + */ + @BindingAdapter(value = {"displayCircle","displayType"},requireAll = true) + public static void displayCircle(ImageView imageView, String url,int type) { + +// Glide.with(imageView.getContext()) +// .load(url) +// .centerCrop() +// .apply(RequestOptions.circleCropTransform()) +// .transition(DrawableTransitionOptions.withCrossFade(500)) +// .placeholder(getDefaultPic(type)) +// .error(getDefaultPic(type)) +// .into(imageView); + + int placeholderId=getDefaultPic(type); + int errorId=getDefaultPic(type); + + Transformation transform=new GlideCircleTransform(); + Glide.with(imageView.getContext()).load(url) + .apply(new RequestOptions() + .placeholder(placeholderId) + .error(errorId) + .centerCrop() + .transform(transform)) + // .transition(DrawableTransitionOptions.withCrossFade(500)) + .thumbnail(loadTransform(imageView.getContext(),placeholderId,transform)) + .thumbnail(loadTransform(imageView.getContext(),errorId,transform)) + .into(imageView); + } + + + + private static int getDefaultPic(int type) { + switch (type) { + case 0: + return R.drawable.shape_bg_loading; + case 4: + return R.drawable.no_banner; + default: + break; + } + return R.drawable.shape_bg_loading; + } + + /** + * 显示高斯模糊效果(电影详情页) + */ +// @BindingAdapter("android:displayGaussian") +// private static void displayGaussian(ImageView imageView,String url) { + // "23":模糊度;"4":图片缩放4倍后再进行模糊 +// Glide.with(imageView.getContext()) +// .load(url) +// .transition(DrawableTransitionOptions.withCrossFade()) +// .error(R.drawable.stackblur_default) +// .placeholder(R.drawable.stackblur_default) +// .transition(DrawableTransitionOptions.withCrossFade(500)) +// .transform(new BlurTransformation(50, 8)) +// .into(imageView); + // } + + + + + + + + + /** + * 加载固定宽高图片 + */ + @BindingAdapter({"android:imageUrl", "android:imageWidth", "android:imageHeight"}) + public static void imageUrl(ImageView imageView, String url, int imageWidthDp, int imageHeightDp) { + Glide.with(imageView.getContext()) + .load(url) + .override(dip2px(imageWidthDp),dip2px(imageHeightDp)) + .placeholder(getDefaultPic(4)) + .centerCrop() + .error(getDefaultPic(0)) + .into(imageView); + + // .apply(bitmapTransform(new CircleCrop())) +// .transform(new GlideCircleTransform()) +// .transform(new RoundedCorners(20)) +// .transform(new CenterCrop(), new RoundedCorners(20)) + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtil.java b/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtil.java new file mode 100644 index 0000000..47a5308 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtil.java @@ -0,0 +1,236 @@ +package com.example.baseframe.utils.permission; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.net.Uri; +import android.provider.Settings; + + +/** + * 检查权限的工具类 + * Created by bm on 17/2/7. + */ +public class PermissionsUtil { + + + //相机权限 + public static final String[] PERMISSION_CAMERA = new String[]{ + Manifest.permission.CAMERA + }; + + //录制视频 + public static final String[] PERMISSION_VIDEO = new String[]{ + Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO + }; + + //相机权限、写入权限 + public static final String[] PERMISSION_CAMERA_WRITE = new String[]{ + Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + + //获取手机状态权限 + public static final String[] PERMISSION_STATE = new String[]{ + Manifest.permission.READ_PHONE_STATE + }; + + //文件读取权限 + public static final String[] PERMISSION_FILE = new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE,// 写入权限 + Manifest.permission.READ_EXTERNAL_STORAGE //读取权限 + }; + + //手机通讯录权限 + public static final String[] PERMISSION_CONTACTS = new String[]{ + Manifest.permission.READ_CONTACTS,// 读取通讯录权限 + Manifest.permission.WRITE_CONTACTS, //编辑通讯录权限 + Manifest.permission.GET_ACCOUNTS + }; + + //获取位置权限 + public static final String[] PERMISSION_LOCATION = new String[]{ + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + }; + + //录音权限 + public static final String[] PERMISSION_MICROPHONE = new String[]{ + Manifest.permission.RECORD_AUDIO + }; + + //电话操作权限 + public static final String[] PERMISSION_PHONE = new String[]{ + // Manifest.permission.READ_PHONE_STATE,// 读取通讯录权限 + Manifest.permission.CALL_PHONE, //编辑通讯录权限 + // Manifest.permission.READ_CALL_LOG, + // Manifest.permission.WRITE_CALL_LOG, + // Manifest.permission.ADD_VOICEMAIL, + // Manifest.permission.USE_SIP, + // Manifest.permission.PROCESS_OUTGOING_CALLS + }; + + //获取传感器数据权限 + public static final String[] PERMISSION_SENSORS = new String[]{ + Manifest.permission.BODY_SENSORS + }; + + //短信操作权限 + public static final String[] PERMISSION_SMS = new String[]{ + Manifest.permission.SEND_SMS, + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS, + Manifest.permission.RECEIVE_WAP_PUSH, + Manifest.permission.RECEIVE_MMS, + }; + + + // 启动应用的设置 + public static void startAppSettings(Context context) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + context.getPackageName())); + context.startActivity(intent); + } + +// /** +// * 显示缺失权限提示 +// */ +// public static void showMissingPermissionDialog(final Context context, +// DialogInterface.OnClickListener cancelListener) { +// CommonAlertDialog.Builder builder = new CommonAlertDialog.Builder(context); +// builder.setTitle(R.string.help); +// builder.setMessage(R.string.string_help_text); +// builder.setPositiveButton(R.string.quit, cancelListener); +// builder.setNegativeButton(R.string.settings, new DialogInterface.OnClickListener() { +// @Override +// public void onClick(DialogInterface dialog, int which) { +// PermissionsUtil.startAppSettings(context); +// } +// }); +// builder.setCancelable(false); +// builder.show(); +// } +// +// // 判断含有全部的权限 +// public static boolean hasAllPermissionsGranted(@NonNull int[] grantResults) { +// for (int grantResult : grantResults) { +// if (grantResult != PackageManager.PERMISSION_GRANTED) { +// return false; +// } +// } +// return true; +// } +// +// // 判断权限集合 +// public static boolean lacksPermissions(Context context, String... permissions) { +// for (String permission : permissions) { +// if (lacksPermission(context, permission)) { +// return true; +// } +// } +// return false; +// } +// +// // 判断是否缺少权限 +// public static boolean lacksPermission(Context context, String permission) { +// return ContextCompat.checkSelfPermission(context, permission) != +// PackageManager.PERMISSION_GRANTED; +// } +// +// // 请求权限兼容低版本 +// public static void requestPermissions(Activity aty, String[] permissions, int requestCode) { +// ActivityCompat.requestPermissions(aty, permissions, requestCode); +// } +// +// // 检测权限配置 +// public static boolean checkPermissions(Activity aty, String[] permissions, int requestCode) { +// if (permissions != null && permissions.length > 0) { +// if (PermissionsUtil.lacksPermissions(aty, permissions)) { +// PermissionsUtil.requestPermissions(aty, permissions, requestCode); +// return false; +// } +// } +// return true; +// } +// +// //检测权限配置,不处理结果 +// public static boolean checkPermissionsUndo(Activity aty, String[] permissions) { +// return checkPermissions(aty, permissions, PERMISSION_REQUEST_CODE_UNDO); +// } +// +// //检测权限配置,强制打开 +// public static boolean checkPermissionsTodo(Activity aty, String[] permissions) { +// return checkPermissions(aty, permissions, PERMISSION_REQUEST_CODE_TODO); +// } +// +// /** +// * 检测权限配置 +// */ +// public static boolean checkPermissions(Fragment fragment, String[] permissions, int +// requestCode) { +// if (permissions != null && permissions.length > 0) { +// if (PermissionsUtil.lacksPermissions(fragment.getContext(), permissions)) { +// fragment.requestPermissions(permissions, requestCode); +// return false; +// } +// } +// return true; +// } +// +// public static boolean checkPermissionsFile(Activity aty, int requestCode) { +// return checkPermissions(aty, PERMISSION_FILE, requestCode); +// } +// +// public static boolean checkPermissionsFile(Activity aty) { +// return checkPermissionsUndo(aty, PERMISSION_FILE); +// } +// +// public static boolean checkPermissionsPhoneState(Activity aty, int requestCode) { +// return checkPermissions(aty, PERMISSION_STATE, requestCode); +// } +// +// public static boolean checkPermissionsPhoneState(Activity aty) { +// return checkPermissionsUndo(aty, PERMISSION_STATE); +// } +// +// public static boolean checkPermissionsCameraWrite(Activity aty, int requestCode) { +// return checkPermissions(aty, PERMISSION_CAMERA_WRITE, requestCode); +// } +// +// +// public static boolean checkPermissionsCamera(Activity aty, int requestCode) { +// return checkPermissions(aty, PERMISSION_CAMERA, requestCode); +// } +// +// public static boolean checkPermissionsVideo(Activity aty, int requestCode) { +// return checkPermissions(aty, PERMISSION_VIDEO, requestCode); +// } +// +// public static boolean checkPermissionsVideo(Activity aty) { +// return checkPermissionsUndo(aty, PERMISSION_VIDEO); +// } +// +// +// +// public static boolean checkPermissionsCamera(Activity aty) { +// return checkPermissionsUndo(aty, PERMISSION_CAMERA); +// } + + /** + * API22(含)之前用于判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的,22之后用动态权限 + * + * @return true 表示开启 + */ + public static boolean isOPenLocation(Context context) { + LocationManager locationManager + = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); + if (gps || network) { + return true; + } + return false; + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtils.java b/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtils.java new file mode 100644 index 0000000..aeab9c9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/permission/PermissionsUtils.java @@ -0,0 +1,154 @@ +package com.example.baseframe.utils.permission; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author yzh + * 权限工具类 + */ + +public class PermissionsUtils { + + + private final int mRequestCode = 100;//权限请求码 + public static boolean showSystemSetting = true; + + private PermissionsUtils() { + } + + private static PermissionsUtils permissionsUtils; + private IPermissionsResult mPermissionsResult; + + public static PermissionsUtils getInstance() { + if (permissionsUtils == null) { + permissionsUtils = new PermissionsUtils(); + } + return permissionsUtils; + } + + public void chekPermissions(Activity context, String[] permissions, @NonNull IPermissionsResult permissionsResult) { + mPermissionsResult = permissionsResult; + + if (Build.VERSION.SDK_INT < 23) {//6.0才用动态权限 + permissionsResult.passPermissons(); + return; + } + + //创建一个mPermissionList,逐个判断哪些权限未授予,未授予的权限存储到mPerrrmissionList中 + List mPermissionList = new ArrayList<>(); + //逐个判断你要的权限是否已经通过 + for (int i = 0; i < permissions.length; i++) { + if (ContextCompat.checkSelfPermission(context, permissions[i]) != PackageManager.PERMISSION_GRANTED) { + mPermissionList.add(permissions[i]);//添加还未授予的权限 + } + } + + //申请权限 + if (mPermissionList.size() > 0) {//有权限没有通过,需要申请 + ActivityCompat.requestPermissions(context, permissions, mRequestCode); + } else { + //说明权限都已经通过,可以做你想做的事情去 + permissionsResult.passPermissons(); + return; + } + + + } + + //请求权限后回调的方法 在BaseActivity 的onRequestPermissionsResult 在调用一下本方法 + //参数: requestCode 是我们自己定义的权限请求码 + //参数: permissions 是我们请求的权限名称数组 + //参数: grantResults 是我们在弹出页面后是否允许权限的标识数组,数组的长度对应的是权限名称数组的长度,数组的数据0表示允许权限,-1表示我们点击了禁止权限 + + public void onRequestPermissionsResult(Activity context, int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + boolean hasPermissionDismiss = false;//有权限没有通过 + if (mRequestCode == requestCode) { + for (int i = 0; i < grantResults.length; i++) { + if (grantResults[i] == -1) { + hasPermissionDismiss = true; + } + } + //如果有权限没有被允许 + if (hasPermissionDismiss) { + if (showSystemSetting) { + showSystemPermissionsSettingDialog(context);//跳转到系统设置权限页面,或者直接关闭页面,不让他继续访问 + } else { + mPermissionsResult.forbitPermissons(); + } + } else { + //全部权限通过,可以进行下一步操作。。。 + mPermissionsResult.passPermissons(); + } + } + + } + + + /** + * 不再提示权限时的展示对话框 + */ + AlertDialog mPermissionDialog; + + private void showSystemPermissionsSettingDialog(final Activity context) { + final String mPackName = context.getPackageName(); + if (mPermissionDialog == null) { + mPermissionDialog = new AlertDialog.Builder(context) + .setMessage("已禁用权限,请手动授予") + .setPositiveButton("设置", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + cancelPermissionDialog(); + + Uri packageURI = Uri.parse("package:" + mPackName); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI); + context.startActivity(intent); + context.finish(); + } + }) + .setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //关闭页面或者做其他操作 + cancelPermissionDialog(); + //mContext.finish(); + mPermissionsResult.forbitPermissons(); + } + }) + .create(); + } + mPermissionDialog.show(); + } + + //关闭对话框 + private void cancelPermissionDialog() { + if (mPermissionDialog != null) { + mPermissionDialog.cancel(); + mPermissionDialog = null; + } + + } + + + public interface IPermissionsResult { + void passPermissons(); + + void forbitPermissons(); + } + + +} diff --git a/app/src/main/java/com/example/baseframe/utils/scroll/GestureHelper.java b/app/src/main/java/com/example/baseframe/utils/scroll/GestureHelper.java new file mode 100644 index 0000000..cef1c49 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/scroll/GestureHelper.java @@ -0,0 +1,208 @@ +package com.example.baseframe.utils.scroll; + +import android.content.Context; +import android.util.TypedValue; +import android.view.MotionEvent; + +/** + * 手势辅助器 + */ +public class GestureHelper { + + /** + * 无手势,还不能确定手势 + */ + public static final int GESTURE_NONE = 0; + /** + * 手势:按住 + */ + public static final int GESTURE_PRESSED = 1; + /** + * 手势:点击 + */ + public static final int GESTURE_CLICK = 2; + /** + * 手势:长按 + */ + public static final int GESTURE_LONG_CLICK = 3; + /** + * 手势:左滑 + */ + public static final int GESTURE_LEFT = 4; + /** + * 手势:上滑 + */ + public static final int GESTURE_UP = 5; + /** + * 手势:右滑 + */ + public static final int GESTURE_RIGHT = 6; + /** + * 手势:下滑 + */ + public static final int GESTURE_DOWN = 7; + + /** + * 默认的点大小,单位:dip + */ + public static final float DEFAULT_FONT_SIZE_DP = 5; + /** + * 默认的长按时间 + */ + public static final int DEFAULT_LONG_CLICK_TIME = 800; + + private float pointSize; // 点的大小 + private int longClickTime; // 长按判定时间 + private float xyScale; + + private int gesture = GESTURE_NONE; // 手势 + private long downTime; + private float downX = 0f; + private float downY = 0f; + private float preX = 0f; + private float preY = 0f; + + /** + * 创建一个手势帮助器 + * + * @param pointSize 点的大小,超出此大小的滑动手势会被判定为非点击手势 + * @param longClickTime 长按点击时间,超过或等于此时间的按住手势算长按点击事件 + * @param xyScale X轴与Y轴比例,影响方向手势的判定,默认是1; + * 越小,手势判定越偏重于水平方向; + * 越大,手势判定偏重于垂直方向; + * 1,不偏重任何方向; + * 如果是专注于水平方向,可以将此值设置小于1的数, + * 如果是专注于垂直方向,可以将此值设置大于1的数; + * 如果是垂直与水平同等重要,将此值设置成1 + */ + public GestureHelper(float pointSize, int longClickTime, float xyScale) { + if (pointSize <= 0) { + throw new IllegalArgumentException("Illegal:pointSize <= 0"); + } + if (longClickTime <= 0) { + throw new IllegalArgumentException("Illegal:longClickTime <= 0"); + } + if (xyScale == 0) { + throw new IllegalArgumentException("Illegal:xyScale equals 0"); + } + this.pointSize = pointSize; + this.longClickTime = longClickTime; + this.xyScale = xyScale; + } + + /** + * 创建默认的手势辅助器 + * + * @param context 上下文对象 + * @return 手势器 + */ + public static GestureHelper createDefault(Context context) { + float pointSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + DEFAULT_FONT_SIZE_DP, context.getResources().getDisplayMetrics()); + return new GestureHelper(pointSize, DEFAULT_LONG_CLICK_TIME, 1f); + } + + /** + * 触发触摸滑动事件 + * + * @param event 事件 + */ + public void onTouchEvent(MotionEvent event) { +// System.out.println("onTouchEvent:action=" + event.getAction()); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: // 按下 + touchDown(event); + break; + case MotionEvent.ACTION_MOVE: // 移动 + touchMove(event); + break; + case MotionEvent.ACTION_CANCEL: // 取消 + case MotionEvent.ACTION_UP: // 抬起 + touchFinish(event); + break; + } +// System.out.println("onTouchEvent:" + gesture); + } + + /** + * 获取手势 + * + * @return 手势 + */ + public int getGesture() { + return gesture; + } + + /** + * 判定是否为水平滑动手势 + * + * @return true,水平滑动手势 + */ + public boolean isHorizontalGesture() { + return gesture == GESTURE_LEFT || gesture == GESTURE_RIGHT; + } + + /** + * 判定是否为垂直滑动手势 + * + * @return true,垂直滑动手势 + */ + public boolean isVerticalGesture() { + return gesture == GESTURE_UP || gesture == GESTURE_DOWN; + } + + private void touchDown(MotionEvent event) { + downTime = System.currentTimeMillis(); + downX = preX = event.getRawX(); + downY = preY = event.getRawY(); + gesture = GESTURE_PRESSED; + } + + private void touchMove(MotionEvent event) { + float rangeX = event.getRawX() - downX; + float rangeY = event.getRawY() - downY; +// System.out.println(String.format("touchMove:rangeX=%f,rangeY=%f,pointSize=%f", +// rangeX, rangeY, pointSize)); + if (gesture == GESTURE_NONE || gesture == GESTURE_PRESSED) { // 未确定手势或正在长按 + if (Math.abs(rangeX) > pointSize || Math.abs(rangeY) > pointSize) { + // 超出点的范围,不算点击、按住手势,应该是滑动手势 + float ox = event.getRawX() - preX; + float oy = event.getRawY() - preY; + if (Math.abs(ox) > xyScale * Math.abs(oy)) { + // 水平方向滑动 + if (ox < 0) { + gesture = GESTURE_LEFT; + } else { + gesture = GESTURE_RIGHT; + } + } else { + // 垂直方向滑动 + if (oy < 0) { + gesture = GESTURE_UP; + } else { + gesture = GESTURE_DOWN; + } + } + } else { + gesture = GESTURE_PRESSED; // 按住手势 + } + } + if (gesture == GESTURE_PRESSED) { // 按住中 + if (System.currentTimeMillis() - downTime >= longClickTime) { // 按住超过长按时间,算长按时间 + gesture = GESTURE_LONG_CLICK; + } + } + preX = event.getRawX(); + preY = event.getRawY(); + } + + private void touchFinish(MotionEvent event) { + if (gesture == GESTURE_PRESSED) { // 按住到释放,应该算点击手势 + if (System.currentTimeMillis() - downTime >= longClickTime) { // 按住超过长按时间,算长按时间 + gesture = GESTURE_LONG_CLICK; + } else { + gesture = GESTURE_CLICK; + } + } + } +} diff --git a/app/src/main/java/com/example/baseframe/utils/scroll/ScrollHelper.java b/app/src/main/java/com/example/baseframe/utils/scroll/ScrollHelper.java new file mode 100644 index 0000000..8ad2165 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/scroll/ScrollHelper.java @@ -0,0 +1,220 @@ +package com.example.baseframe.utils.scroll; + + +import android.content.Context; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewGroup; + +/** + * 滑动辅助器,可以直接使用{@link ViewScrollHelper ViewScrollHelper} + */ +public abstract class ScrollHelper { + + private GestureHelper gestureHelper; + private VelocityTracker velocityTracker; + + private float startTouchX; + private float startTouchY; + private int startScrollX; + private int startScrollY; + + public ScrollHelper(GestureHelper gestureHelper) { + this.gestureHelper = gestureHelper; + this.velocityTracker = VelocityTracker.obtain(); + } + + public ScrollHelper(Context context) { + this(GestureHelper.createDefault(context)); + } + + /** + * 触发触摸事件 + * + * @param event 事件 + */ + public void onTouchEvent(MotionEvent event) { + gestureHelper.onTouchEvent(event); + velocityTracker.addMovement(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + setStartPosition(event.getX(), event.getY()); + break; + } + + case MotionEvent.ACTION_MOVE: { + if (canScroll()) { + float rangeX = event.getX() - startTouchX; + float rangeY = event.getY() - startTouchY; + int dstX = (int) (startScrollX - rangeX); + int dstY = (int) (startScrollY - rangeY); + if (dstX < getMinHorizontallyScroll()) { + dstX = 0; + startTouchX = event.getX(); + startScrollX = dstX; + } else if (dstX > getMaxHorizontallyScroll()) { + dstX = getViewHorizontallyScrollSize(); + startTouchX = event.getX(); + startScrollX = dstX; + } + if (dstY < getMinVerticallyScroll()) { + dstY = 0; + startTouchY = event.getY(); + startScrollY = dstY; + } else if (dstY > getMaxVerticallyScroll()) { + dstY = getViewVerticallyScrollSize(); + startTouchY = event.getY(); + startScrollY = dstY; + } + viewScrollTo(dstX, dstY); + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + velocityTracker.computeCurrentVelocity(1000); + if (canScroll()) { + float xv = velocityTracker.getXVelocity(); + float yv = velocityTracker.getYVelocity(); + viewFling(xv, yv); + } + break; + } + } + } + + /** + * 获取手势辅助器 + * + * @return 手势辅助器 + */ + public GestureHelper getGestureHelper() { + return gestureHelper; + } + + /** + * 设置起始位置,一般是ACTION_DOWN的时候执行,如果有特殊要求,可以在外部主动调用,更改起始位置 + * + * @param x 位置X + * @param y 位置Y + */ + public void setStartPosition(float x, float y) { + startTouchX = x; + startTouchY = y; + startScrollX = getViewScrollX(); + startScrollY = getViewScrollY(); + } + + /** + * 判断是否可以滑动 + * + * @return 是否可以滑动 + */ + protected boolean canScroll() { + return gestureHelper.isVerticalGesture() || gestureHelper.isHorizontalGesture(); + } + + /** + * 获取水平方向最小的滑动位置 + * + * @return 水平方向最小的滑动位置 + */ + public int getMinHorizontallyScroll() { + return 0; + } + + /** + * 获取水平方向最大的滑动位置 + * + * @return 水平方向最大的滑动位置 + */ + public int getMaxHorizontallyScroll() { + return getViewHorizontallyScrollSize(); + } + + /** + * 获取垂直方向最小的滑动位置 + * + * @return 垂直方向最小的滑动位置 + */ + public int getMinVerticallyScroll() { + return 0; + } + + /** + * 获取垂直方向最大的滑动位置 + * + * @return 垂直方向最大的滑动位置 + */ + public int getMaxVerticallyScroll() { + return getViewVerticallyScrollSize(); + } + + /** + * 回收此对象 + */ + public void recycle() { + if (null != velocityTracker) { + velocityTracker.recycle(); + velocityTracker = null; + } + if (null != gestureHelper) { + gestureHelper = null; + } + } + + /** + * 获取视图滑动位置X + * + * @return 视图滑动位置Y + */ + protected abstract int getViewScrollX(); + + /** + * 获取视图滑动位置Y + * + * @return 视图滑动位置Y + */ + protected abstract int getViewScrollY(); + + /** + * 获取视图水平方向可以滑动的范围,一般在此方法返回 + * {@link ViewGroup#computeHorizontalScrollRange() ViewGroup.computeHorizontalScrollRange} 减去 + * {@link ViewGroup#computeHorizontalScrollExtent() ViewGroup.computeHorizontalScrollExtent} 的差 + *
result = range-extent + * + * @return 水平方向可以滑动的范围 + */ + protected abstract int getViewHorizontallyScrollSize(); + + /** + * 获取视图垂直方向可以滑动的范围,一般在此方法返回 + * {@link ViewGroup#computeVerticalScrollRange() ViewGroup.computeVerticalScrollRange} 减去 + * {@link ViewGroup#computeVerticalScrollExtent() ViewGroup.computeVerticalScrollExtent} 的差 + *
result = range-extent + * + * @return 垂直方向可以滑动的范围 + */ + protected abstract int getViewVerticallyScrollSize(); + + /** + * 将视图滑动至指定位置,一般调用{@link android.view.View#scrollTo(int, int) View.scrollTo}方法即可 + * + * @param x 位置X + * @param y 位置Y + */ + protected abstract void viewScrollTo(int x, int y); + + /** + * 当触摸抬起时,执行此方法,一般在此方法内执行 + * {@link android.widget.Scroller#fling(int, int, int, int, int, int, int, int) Scroller.fling} + * 方法,需要注意的是,速度应该取参数的相反值,因为参数的速度表示的是触摸滑动的速度,刚好与滑动 + * 的速度方向相反。 + * + * @param xv 水平触摸滑动的速度 + * @param yv 垂直触摸滑动的速度 + */ + protected abstract void viewFling(float xv, float yv); +} + diff --git a/app/src/main/java/com/example/baseframe/utils/scroll/ViewScrollHelper.java b/app/src/main/java/com/example/baseframe/utils/scroll/ViewScrollHelper.java new file mode 100644 index 0000000..c5311e1 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/scroll/ViewScrollHelper.java @@ -0,0 +1,42 @@ +package com.example.baseframe.utils.scroll; + +import android.view.View; + +/** + * 视图滑动辅助器 + * Created by yzh on 2018/12/18 10:13. + */ +public abstract class ViewScrollHelper extends ScrollHelper { + + private View view; + + public ViewScrollHelper(View view, GestureHelper gestureHelper) { + super(gestureHelper); + this.view = view; + } + + public ViewScrollHelper(View view) { + super(view.getContext()); + this.view = view; + } + + @Override + protected int getViewScrollX() { + return view.getScrollX(); + } + + @Override + protected int getViewScrollY() { + return view.getScrollY(); + } + + @Override + protected void viewScrollTo(int x, int y) { + view.scrollTo(x, y); + } + + protected View getView() { + return view; + } +} + diff --git a/app/src/main/java/com/example/baseframe/utils/wifi/WifiHotUtils.java b/app/src/main/java/com/example/baseframe/utils/wifi/WifiHotUtils.java new file mode 100644 index 0000000..0604365 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/wifi/WifiHotUtils.java @@ -0,0 +1,620 @@ +package com.example.baseframe.utils.wifi; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Intent; +import android.net.DhcpInfo; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.RequiresApi; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; +import com.example.baseframe.utils.CloseUtils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.lang.reflect.Method; + + +/** + * detail: Wifi 热点工具类 + * @author Ttt + *
+ *     所需权限
+ *     
+ *     
+ *     
+ *     
+ *     
+ *     
+ * 
+ */ +public final class WifiHotUtils { + + // 日志 TAG + private static final String TAG = WifiHotUtils.class.getSimpleName(); + // WifiManager 对象 + private WifiManager mWifiManager; + // Wifi 热点配置 + private WifiConfiguration mAPWifiConfig; + + /** + * 构造函数 + */ + public WifiHotUtils() { + // 初始化 WifiManager 对象 + mWifiManager = AppUtils.getWifiManager(); + } + + // ============= + // = Wifi 操作 = + // ============= + + /** + * 创建 Wifi 热点配置 ( 支持 无密码 /WPA2 PSK) + * @param ssid Wifi ssid + * @return {@link WifiConfiguration} 热点配置 + */ + public static WifiConfiguration createWifiConfigToAp(final String ssid) { + return createWifiConfigToAp(ssid, null); + } + + /** + * 创建 Wifi 热点配置 ( 支持 无密码 /WPA2 PSK) + * @param ssid Wifi ssid + * @param pwd 密码 ( 需要大于等于 8 位 ) + * @return {@link WifiConfiguration} + */ + public static WifiConfiguration createWifiConfigToAp(final String ssid, final String pwd) { + try { + // 创建一个新的网络配置 + WifiConfiguration wifiConfig = new WifiConfiguration(); + wifiConfig.allowedAuthAlgorithms.clear(); + wifiConfig.allowedGroupCiphers.clear(); + wifiConfig.allowedKeyManagement.clear(); + wifiConfig.allowedPairwiseCiphers.clear(); + wifiConfig.allowedProtocols.clear(); + wifiConfig.priority = 0; + // 设置连接的 SSID + wifiConfig.SSID = ssid; + // 判断密码 + if (TextUtils.isEmpty(pwd)) { + wifiConfig.hiddenSSID = true; + wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + } else { + wifiConfig.preSharedKey = pwd; + wifiConfig.hiddenSSID = true; + wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); +// wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.WPA); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); + wifiConfig.status = WifiConfiguration.Status.ENABLED; + } + return wifiConfig; + } catch (Exception e) { + ALog.eTag(TAG, e, "createWifiConfigToAp"); + } + return null; + } + + /** + * 开启 Wifi 热点 + *
+     *     android 8.0 及以上必须要有定位权限
+     *     android 7.0 及以下需要 WRITE_SETTINGS 权限
+     * 
+ * @param wifiConfig Wifi 配置 + * @return {@code true} success, {@code false} fail + */ + public boolean stratWifiAp(final WifiConfiguration wifiConfig) { + this.mAPWifiConfig = wifiConfig; + // 大于 8.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + // 关闭热点 + if (mReservation != null) { + mReservation.close(); + } + // 清空信息 + mAPWifiSSID = mAPWifiPwd = null; + // Android 8.0 是基于应用开启的, 必须使用固定生成的配置进行创建, 无法进行控制 (APP 关闭后, 热点自动关闭 ) + // 必须有定位权限 + mWifiManager.startLocalOnlyHotspot(new WifiManager.LocalOnlyHotspotCallback() { + @Override + public void onStarted(WifiManager.LocalOnlyHotspotReservation reservation) { + super.onStarted(reservation); + // 保存信息 + mReservation = reservation; + // 获取配置信息 + WifiConfiguration wifiConfiguration = reservation.getWifiConfiguration(); + mAPWifiSSID = wifiConfiguration.SSID; + mAPWifiPwd = wifiConfiguration.preSharedKey; + // 打印信息 + ALog.dTag(TAG, "Android 8.0 onStarted wifiAp ssid: " + mAPWifiSSID + ", pwd: " + mAPWifiPwd); + // 触发回调 + if (mWifiAPListener != null) { + mWifiAPListener.onStarted(wifiConfiguration); + } + } + + @Override + public void onStopped() { + super.onStopped(); + // 打印信息 + ALog.dTag(TAG, "Android 8.0 onStopped wifiAp"); + // 触发回调 + if (mWifiAPListener != null) { + mWifiAPListener.onStopped(); + } + } + + @Override + public void onFailed(int reason) { + super.onFailed(reason); + // 打印信息 + ALog.eTag(TAG, "Android 8.0 onFailed wifiAp, reason: " + reason); + // 触发回调 + if (mWifiAPListener != null) { + mWifiAPListener.onFailed(reason); + } + } + }, null); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "stratWifiAp"); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { // android 7.1 系统以上不支持自动开启热点, 需要手动开启热点 + try { + // 先设置 Wifi 热点信息, 这样跳转前保存热点信息, 开启热点则是对应设置的信息 + boolean setResult = setWifiApConfiguration(wifiConfig); + // 打印日志 + ALog.dTag(TAG, "设置 Wifi 热点信息是否成功: " + setResult); + // 跳转到便携式热点设置页面 + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.TetherSettings")); + AppUtils.startActivity(intent); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "stratWifiAp"); + } + } else { + try { + // 需要 android.permission.WRITE_SETTINGS 权限 + // 获取设置 Wifi 热点方法 + Method method = mWifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class); + // 开启 Wifi 热点 + method.invoke(mWifiManager, wifiConfig, true); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "stratWifiAp"); + } + } + return false; + } + + /** + * 关闭 Wifi 热点 + * @return {@code true} success, {@code false} fail + */ + public boolean closeWifiAp() { + // 大于 8.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 关闭热点 + if (mReservation != null) { + mReservation.close(); + } + // 清空信息 + mAPWifiSSID = mAPWifiPwd = null; + return true; + } + try { + // 获取设置 Wifi 热点方法 + Method method = mWifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class); + // 创建一个新的网络配置 + WifiConfiguration wifiConfig = new WifiConfiguration(); + wifiConfig.allowedAuthAlgorithms.clear(); + wifiConfig.allowedGroupCiphers.clear(); + wifiConfig.allowedKeyManagement.clear(); + wifiConfig.allowedPairwiseCiphers.clear(); + wifiConfig.allowedProtocols.clear(); + wifiConfig.priority = 0; + // 设置 Wifi ssid + wifiConfig.SSID = "CloseWifiAp"; // formatSSID(ssid,true); + // 设置 Wifi 密码 + wifiConfig.preSharedKey = "CloseWifiAp"; + // 设置 Wifi 属性 + wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.RSN); + wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.WPA); + wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); + // 开启 Wifi 热点 + method.invoke(mWifiManager, wifiConfig, false); + return true; + } catch (Exception e) { + ALog.eTag(TAG, e, "closeWifiAp"); + } + return false; + } + + // ================ + // = 手机热点功能 = + // ================ + + /** + * Wifi 热点正在关闭 - WifiManager.WIFI_AP_STATE_DISABLING + */ + public static final int WIFI_AP_STATE_DISABLING = 10; + /** + * Wifi 热点已关闭 - WifiManager.WIFI_AP_STATE_DISABLED + */ + public static final int WIFI_AP_STATE_DISABLED = 11; + /** + * Wifi 热点正在打开 - WifiManager.WIFI_AP_STATE_ENABLING + */ + public static final int WIFI_AP_STATE_ENABLING = 12; + /** + * Wifi 热点已打开 - WifiManager.WIFI_AP_STATE_ENABLED + */ + public static final int WIFI_AP_STATE_ENABLED = 13; + /** + * Wifi 热点状态未知 - WifiManager.WIFI_AP_STATE_FAILED + */ + public static final int WIFI_AP_STATE_FAILED = 14; + + /** + * 获取 Wifi 热点状态 + * @return Wifi 热点状态 + */ + public int getWifiApState() { + try { + // 反射获取方法 + Method method = mWifiManager.getClass().getMethod("getWifiApState"); + // 调用方法, 获取状态 + int wifiApState = (Integer) method.invoke(mWifiManager); + // 打印状态 + ALog.dTag(TAG, "WifiApState: " + wifiApState); + return wifiApState; + } catch (Exception e) { + ALog.eTag(TAG, e, "getWifiApState"); + } + return WIFI_AP_STATE_FAILED; + } + + /** + * 获取 Wifi 热点配置信息 + * @return {@link WifiConfiguration} 热点配置 + */ + public WifiConfiguration getWifiApConfiguration() { + try { + // 获取 Wifi 热点方法 + Method method = mWifiManager.getClass().getMethod("getWifiApConfiguration"); + // 获取配置 + WifiConfiguration wifiApConfig = (WifiConfiguration) method.invoke(mWifiManager); + // 返回配置信息 + return wifiApConfig; + } catch (Exception e) { + ALog.eTag(TAG, e, "getWifiApConfiguration"); + } + return null; + } + + /** + * 设置 Wifi 热点配置信息 + * @param apWifiConfig Wifi 热点配置 + * @return {@code true} success, {@code false} fail + */ + public boolean setWifiApConfiguration(final WifiConfiguration apWifiConfig) { + try { + // 获取设置 Wifi 热点方法 + Method method = mWifiManager.getClass().getMethod("setWifiApConfiguration", WifiConfiguration.class); + // 开启 Wifi 热点 + return (boolean) method.invoke(mWifiManager, apWifiConfig); + } catch (Exception e) { + ALog.eTag(TAG, e, "setWifiApConfiguration"); + } + return false; + } + + // = + + /** + * 判断是否打开 Wifi 热点 + * @return {@code true} yes, {@code false} no + */ + public boolean isOpenWifiAp() { + // 判断是否开启热点 ( 默认未打开 ) + boolean isOpen = false; + // 获取当前 Wifi 热点状态 + int wifiApState = getWifiApState(); + switch (wifiApState) { + case WIFI_AP_STATE_DISABLING: // Wifi 热点正在关闭 + break; + case WIFI_AP_STATE_DISABLED: // Wifi 热点已关闭 + break; + case WIFI_AP_STATE_ENABLING: // Wifi 热点正在打开 + break; + case WIFI_AP_STATE_ENABLED: // Wifi 热点已打开 + isOpen = true; + break; + case WIFI_AP_STATE_FAILED: // Wifi 热点状态未知 + break; + } + return isOpen; + } + + /** + * 关闭 Wifi 热点 ( 判断当前状态 ) + * @param isExecute 是否执行关闭 + * @return {@code true} success, {@code false} fail + */ + public boolean closeWifiApCheck(final boolean isExecute) { + // 判断是否开启热点 ( 默认是 ) + boolean isOpen = true; + // 判断是否执行关闭 + boolean isExecuteClose = isExecute; + // 获取当前 Wifi 热点状态 + int wifiApState = getWifiApState(); + switch (wifiApState) { + case WIFI_AP_STATE_DISABLING: // Wifi 热点正在关闭 + isExecuteClose = false; + break; + case WIFI_AP_STATE_DISABLED: // Wifi 热点已关闭 + isOpen = false; + break; + case WIFI_AP_STATE_ENABLING: // Wifi 热点正在打开 + break; + case WIFI_AP_STATE_ENABLED: // Wifi 热点已打开 + break; + case WIFI_AP_STATE_FAILED: // Wifi 热点状态未知 + break; + } + // 如果属于开启, 则进行关闭 + if (isOpen && isExecuteClose) { + closeWifiAp(); + } + return isOpen; + } + + /** + * 是否有设备连接热点 + * @return {@code true} yes, {@code false} no + */ + public boolean isConnectHot() { + try { + BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); + String line; + while ((line = br.readLine()) != null) { + String[] splitted = line.split(" +"); + if (splitted != null && splitted.length >= 4) { + String ipAddress = splitted[0]; // IP 地址 + // 防止地址为 null, 并且需要以. 拆分存在 4 个长度 255.255.255.255 + if (ipAddress != null && ipAddress.split("\\.").length >= 3) { + return true; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "isConnectHot"); + } + return false; + } + + /** + * 获取热点主机 IP 地址 + * @return 热点主机 IP 地址 + */ + @SuppressLint("MissingPermission") + public String getHotspotServiceIp() { + try { + // 获取网关信息 + DhcpInfo dhcpInfo = mWifiManager.getDhcpInfo(); + // 获取服务器地址 + return intToString(dhcpInfo.serverAddress); + } catch (Exception e) { + ALog.eTag(TAG, e, "getHotspotServiceIp"); + } + return null; + } + + /** + * 获取连接上的子网关热点 IP ( 一个 ) + * @return 连接上的子网关热点 IP + */ + public String getHotspotAllotIp() { + try { + BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); + String line; + while ((line = br.readLine()) != null) { + String[] splitted = line.split(" +"); + if (splitted != null && splitted.length >= 4) { + String ipAddress = splitted[0]; // IP 地址 + // 防止地址为 null, 并且需要以. 拆分存在 4 个长度 255.255.255.255 + if (ipAddress != null && ipAddress.split("\\.").length >= 3) { + return ipAddress; + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "getHotspotAllotIp"); + } + return null; + } + + /** + * 获取连接的热点信息 + * @return 连接的热点信息 + */ + public String getConnectHotspotMsg() { + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader("/proc/net/arp")); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + builder.append(line); + } + return builder.toString(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getHotspotAllotIp"); + } finally { + CloseUtils.closeIOQuietly(br); + } + return null; + } + +// /** +// * 获取连接的热点信息 +// * @return 连接的热点信息 +// */ +// private String getConnectHotspotMsg() { +// try { +// BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); +// String line; +// while ((line = br.readLine()) != null) { +// String[] splitted = line.split(" +"); +// if (splitted != null && splitted.length >= 4) { +// String ip = splitted[0]; // IP 地址 +// String mac = splitted[3]; // Mac 地址 +// } +// } +// } catch (Exception e) { +// ALog.eTag(TAG, e, "getConnectHotspotMsg"); +// } +// return null; +// } + + /** + * 获取热点拼接后的 IP 网关掩码 + * @param defaultGateway 默认网关掩码 + * @param ipAddress IP 地址 + * @return 网关掩码 + */ + public String getHotspotSplitIpMask(final String defaultGateway, final String ipAddress) { + // 网关掩码 + String hsMask = defaultGateway; + // 获取网关掩码 + if (ipAddress != null) { + try { + int length = ipAddress.lastIndexOf("."); + // 进行裁剪 + hsMask = ipAddress.substring(0, length) + ".255"; + } catch (Exception e) { + ALog.eTag(TAG, e, "getHotspotSplitIpMask"); + } + } + return hsMask; + } + + /** + * 转换 IP 地址 + * @param data 待转换的数据 + * @return 转换后的 IP 地址 + */ + private String intToString(final int data) { + StringBuilder builder = new StringBuilder(); + int b = (data >> 0) & 0xff; + builder.append(b + "."); + b = (data >> 8) & 0xff; + builder.append(b + "."); + b = (data >> 16) & 0xff; + builder.append(b + "."); + b = (data >> 24) & 0xff; + builder.append(b); + return builder.toString(); + } + + // ==================== + // = Android 8.0 相关 = + // ==================== + + // Wifi ssid + private String mAPWifiSSID; + // Wifi 密码 + private String mAPWifiPwd; + // Wifi 热点对象 + private WifiManager.LocalOnlyHotspotReservation mReservation; + + /** + * 获取 Wifi 热点名 + * @return Wifi ssid + */ + public String getApWifiSSID() { + // 大于 8.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return mAPWifiSSID; + } else { + if (mAPWifiConfig != null) { + return mAPWifiConfig.SSID; + } + } + return null; + } + + /** + * 获取 Wifi 热点密码 + * @return Wifi 热点密码 + */ + public String getApWifiPwd() { + // 大于 8.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return mAPWifiPwd; + } else { + if (mAPWifiConfig != null) { + return mAPWifiConfig.preSharedKey; + } + } + return null; + } + + // = + + // Wifi 热点监听 + private OnWifiAPListener mWifiAPListener; + + /** + * 设置 Wifi 热点监听事件 + * @param wifiAPListener {@link OnWifiAPListener} + * @return {@link WifiHotUtils} + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public WifiHotUtils setOnWifiAPListener(final OnWifiAPListener wifiAPListener) { + this.mWifiAPListener = wifiAPListener; + return this; + } + + /** + * detail: Android Wifi 热点监听 + * @author Ttt + */ + public interface OnWifiAPListener { + + /** + * 开启热点回调 + * @param wifiConfig 热点配置 + */ + void onStarted(WifiConfiguration wifiConfig); + + /** + * 关闭热点回调 + */ + void onStopped(); + + /** + * 失败回调 + * @param reason 失败原因 ( 错误码 ) + */ + void onFailed(int reason); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/wifi/WifiUtils.java b/app/src/main/java/com/example/baseframe/utils/wifi/WifiUtils.java new file mode 100644 index 0000000..fe83fc7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/wifi/WifiUtils.java @@ -0,0 +1,1060 @@ +package com.example.baseframe.utils.wifi; + +import android.annotation.SuppressLint; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.KeyMgmt; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.RequiresPermission; + +import com.blankj.ALog; +import com.example.baseframe.utils.AppUtils; +import com.example.baseframe.utils.ConvertUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + + +/** + * detail: Wifi 工具类 + * @author Ttt + *
+ *     所需权限
+ *     
+ *     
+ *     
+ * 
+ */ +public final class WifiUtils { + + // 日志 TAG + private static final String TAG = WifiUtils.class.getSimpleName(); + // WifiManager 对象 + private WifiManager mWifiManager; + + // ======== + // = 常量 = + // ======== + + // 没有密码 + public static final int NOPWD = 0; + // wep 加密方式 + public static final int WEP = 1; + // wpa 加密方式 + public static final int WPA = 2; + + /** + * 构造函数 + */ + public WifiUtils() { + // 初始化 WifiManager 对象 + mWifiManager = AppUtils.getWifiManager(); + } + + // =========================== + // = Wifi 开关、连接状态获取 = + // =========================== + + /** + * 判断是否打开 Wifi + * @return {@code true} yes, {@code false} no + */ + public boolean isOpenWifi() { + return mWifiManager.isWifiEnabled(); + } + + /** + * 打开 Wifi + * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public boolean openWifi() { + // 如果没有打开 Wifi, 才进行打开 + if (!isOpenWifi()) { + try { + return mWifiManager.setWifiEnabled(true); + } catch (Exception e) { + ALog.eTag(TAG, e, "openWifi"); + } + } + return false; + } + + /** + * 关闭 Wifi + * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public boolean closeWifi() { + // 如果已经打开了 Wifi, 才进行关闭 + if (isOpenWifi()) { + try { + return mWifiManager.setWifiEnabled(false); + } catch (Exception e) { + ALog.eTag(TAG, e, "closeWifi"); + } + } + return false; + } + + /** + * 自动切换 Wifi 开关状态 + *
+     *     如果打开了, 则关闭
+     *     如果关闭了, 则打开
+     * 
+ * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public boolean toggleWifiEnabled() { + try { + return mWifiManager.setWifiEnabled(!isOpenWifi()); + } catch (Exception e) { + ALog.eTag(TAG, e, "toggleWifiEnabled"); + } + return false; + } + + /** + * 获取当前 Wifi 连接状态 + * @return Wifi 连接状态 + */ + @SuppressLint("MissingPermission") + public int getWifiState() { + // WifiManager.WIFI_STATE_ENABLED: // 已打开 + // WifiManager.WIFI_STATE_ENABLING: // 正在打开 + // WifiManager.WIFI_STATE_DISABLED: // 已关闭 + // WifiManager.WIFI_STATE_DISABLING: // 正在关闭 + // WifiManager.WIFI_STATE_UNKNOWN: // 未知 + try { + return mWifiManager.getWifiState(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getWifiState"); + } + return WifiManager.WIFI_STATE_UNKNOWN; + } + + // ============ + // = get 操作 = + // ============ + + /** + * 开始扫描 Wifi + * @return {@code true} 操作成功, {@code false} 操作失败 + */ + @SuppressLint("MissingPermission") + public boolean startScan() { + try { + return mWifiManager.startScan(); + } catch (Exception e) { + ALog.eTag(TAG, e, "startScan"); + } + return false; + } + + /** + * 获取已配置 ( 连接过 ) 的 Wifi 配置 + * @return {@link List} 已配置 ( 连接过 ) 的 Wifi 配置 + */ + @SuppressLint("MissingPermission") + public List getConfiguration() { + try { + return mWifiManager.getConfiguredNetworks(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getConfiguration"); + } + return null; + } + + /** + * 获取附近的 Wifi 列表 + * @return {@link List} 附近的 Wifi 列表 + */ + @SuppressLint("MissingPermission") + public List getWifiList() { + try { + return mWifiManager.getScanResults(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getWifiList"); + } + return null; + } + + /** + * 获取连接的 WifiInfo + * @return {@link WifiInfo} + */ + @SuppressLint("MissingPermission") + public WifiInfo getWifiInfo() { + try { + return mWifiManager.getConnectionInfo(); + } catch (Exception e) { + ALog.eTag(TAG, e, "getWifiInfo"); + } + return null; + } + + /** + * 获取 MAC 地址 + * @param wifiInfo {@link WifiInfo} + * @return MAC 地址 + */ + public static String getMacAddress(final WifiInfo wifiInfo) { + if (wifiInfo == null) return null; + return wifiInfo.getMacAddress(); + } + + /** + * 获取连接的 BSSID + * @param wifiInfo {@link WifiInfo} + * @return BSSID + */ + public static String getBSSID(final WifiInfo wifiInfo) { + if (wifiInfo == null) return null; + return wifiInfo.getBSSID(); + } + + /** + * 获取 IP 地址 + * @param wifiInfo {@link WifiInfo} + * @return IP 地址 + */ + public static int getIPAddress(final WifiInfo wifiInfo) { + if (wifiInfo == null) return -1; + return wifiInfo.getIpAddress(); + } + + /** + * 获取连接的 Network Id + * @param wifiInfo {@link WifiInfo} + * @return Network Id + */ + public static int getNetworkId(final WifiInfo wifiInfo) { + if (wifiInfo == null) return -1; + return wifiInfo.getNetworkId(); + } + + /** + * 获取 Wifi SSID + * @param wifiInfo {@link WifiInfo} + * @return Wifi SSID + */ + public static String getSSID(final WifiInfo wifiInfo) { + if (wifiInfo == null) return null; + try { + // 获取 SSID, 并进行处理 + return formatSSID(wifiInfo.getSSID(), false); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSSID"); + } + return null; + } + + /** + * 获取当前连接的 Wifi SSID + * @return Wifi SSID + */ + @SuppressLint("MissingPermission") + public static String getSSID() { + try { + // 获取当前连接的 Wifi + WifiInfo wifiInfo = AppUtils.getWifiManager().getConnectionInfo(); + // 获取 Wifi SSID + return formatSSID(wifiInfo.getSSID(), false); + } catch (Exception e) { + ALog.eTag(TAG, e, "getSSID"); + } + return null; + } + + // = + + /** + * 判断是否存在 \"ssid\", 存在则裁剪返回 + * @param ssid 待处理的 SSID + * @return 处理后的 SSID + */ + public static String formatSSID(final String ssid) { + if (ssid == null) return null; + // 自动去掉 "" + if (ssid != null && ssid.startsWith("\"") && ssid.endsWith("\"")) { + try { + // 裁剪连接的 ssid, 并返回 + return ssid.substring(1, ssid.length() - 1); + } catch (Exception e) { + ALog.eTag(TAG, e, "formatSSID"); + } + } + return ssid; + } + + /** + * 格式化处理 SSID + * @param ssid 待处理的 SSID + * @param isAppend {@code true} 添加引号, {@code false} 删除引号 + * @return 处理后的 SSID + */ + public static String formatSSID(final String ssid, final boolean isAppend) { + if (ssid == null) return null; + if (isAppend) { + return "\"" + ssid + "\""; + } else { + return formatSSID(ssid); + } + } + + /** + * 获取处理后的密码 + * @param pwd 待处理的密码 + * @param isJudge 是否需要判断 + * @return 处理后的密码 + */ + public static String getPassword(final String pwd, final boolean isJudge) { + if (pwd == null) return null; + if (isJudge && isHexWepKey(pwd)) { + return pwd; + } else { + return "\"" + pwd + "\""; + } + } + + /** + * 判断是否 wep 加密 + * @param wepKey 加密类型 + * @return {@code true} yes, {@code false} no + */ + public static boolean isHexWepKey(final String wepKey) { + if (wepKey == null) return false; + // WEP-40, WEP-104, and some vendors using 256-bit WEP (WEP-232?) + int len = wepKey.length(); + if (len != 10 && len != 26 && len != 58) { + return false; + } + return ConvertUtils.isHex(wepKey); + } + + // ============ + // = 快捷操作 = + // ============ + + /** + * 获取加密类型 + * @param typeStr 加密类型 + * @return 加密类型 {@link WifiUtils#NOPWD}、{@link WifiUtils#WPA}、{@link WifiUtils#WEP} + */ + public static int getWifiType(final String typeStr) { + if (typeStr == null) return NOPWD; + if (typeStr.contains("WPA")) { + return WPA; + } else if (typeStr.contains("WEP")) { + return WEP; + } + return NOPWD; + } + + /** + * 获取加密类型 + * @param typeInt 加密类型 + * @return 加密类型 {@link WifiUtils#NOPWD}、{@link WifiUtils#WPA}、{@link WifiUtils#WEP} + */ + public static int getWifiTypeInt(final String typeInt) { + if (typeInt == null) return NOPWD; + if (typeInt.equals("2")) { + return WPA; + } else if (typeInt.equals("1")) { + return WEP; + } + return NOPWD; + } + + /** + * 获取加密类型 + * @param type 加密类型 + * @return 加密类型 + */ + public static String getWifiType(final int type) { + switch (type) { + case WPA: + return "2"; + case WEP: + return "1"; + case NOPWD: + return "0"; + } + return "0"; + } + + /** + * 获取加密类型 + * @param type 加密类型 + * @return 加密类型 + */ + public static String getWifiTypeStr(final int type) { + switch (type) { + case WPA: + return "WPA"; + case WEP: + return "WEP"; + default: + return ""; + } + } + + /** + * 判断是否连接为 null - unknown ssid + * @param ssid Wifi ssid + * @return {@code true} yes, {@code false} no + */ + public static boolean isConnNull(final String ssid) { + if (ssid == null) return true; + return ssid.indexOf("unknown") != -1; // + } + + /** + * 获取连接的 Wifi 热点 SSID + * @return Wifi 热点 SSID + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public static String isConnectAphot() { + try { + // 连接管理 + ConnectivityManager cManager = AppUtils.getConnectivityManager(); + // 版本兼容处理 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + // 连接状态 + NetworkInfo.State nState = cManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState(); + if ((nState == NetworkInfo.State.CONNECTED)) { + // 获取连接的 ssid + return getSSID(); + } + } else { + // 获取当前活跃的网络 ( 连接的网络信息 ) + Network network = cManager.getActiveNetwork(); + if (network != null) { + NetworkCapabilities networkCapabilities = cManager.getNetworkCapabilities(network); + // 判断是否连接 Wifi + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + // 获取连接的 ssid + return getSSID(); + } + } + } + } catch (Exception e) { + ALog.eTag(TAG, e, "isConnectAphot"); + } + return null; + } + + // ================= + // = Wifi 配置操作 = + // ================= + + // 默认没有密码 + public static final int SECURITY_NONE = 0; + // WEP 加密方式 + public static final int SECURITY_WEP = 1; + // PSK 加密方式 + public static final int SECURITY_PSK = 2; + // EAP 加密方式 + public static final int SECURITY_EAP = 3; + + /** + * 获取 Wifi 加密类型 + * @param wifiConfig Wifi 配置信息 + * @return Wifi 加密类型 + */ + public static int getSecurity(final WifiConfiguration wifiConfig) { + if (wifiConfig == null) return SECURITY_NONE; + if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) { + return SECURITY_PSK; + } + if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA_EAP) + || wifiConfig.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) { + return SECURITY_EAP; + } + return (wifiConfig.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE; + } + + /** + * 判断 Wifi 加密类型, 是否为加密类型 + * @param wifiConfig Wifi 配置信息 + * @return {@code true} yes, {@code false} no + */ + public static boolean isExistsPwd(final WifiConfiguration wifiConfig) { + if (wifiConfig == null) return false; + int wifiSecurity = getSecurity(wifiConfig); + // 判断是否加密 + return (wifiSecurity != SECURITY_NONE); + } + + /** + * 获取指定的 ssid 网络配置 ( 需连接保存过, 才存在 ) + * @param ssid Wifi ssid + * @return {@link WifiConfiguration} + */ + public WifiConfiguration isExists(final String ssid) { + if (ssid == null) return null; + // 获取 Wifi 连接过的配置信息 + List listWifiConfigs = getConfiguration(); + // 防止为 null + if (listWifiConfigs == null) return null; + // 遍历判断是否存在 + for (int i = 0, len = listWifiConfigs.size(); i < len; i++) { + WifiConfiguration wifiConfig = listWifiConfigs.get(i); + if (wifiConfig != null) { + if (wifiConfig.SSID.equals("\"" + ssid + "\"")) { + return wifiConfig; + } + } + } + return null; + } + + /** + * 获取指定的 network id 网络配置 ( 需连接保存过, 才存在 ) + * @param networkId network id + * @return {@link WifiConfiguration} + */ + public WifiConfiguration isExists(final int networkId) { + // 获取 Wifi 连接过的配置信息 + List listWifiConfigs = getConfiguration(); + // 防止为 null + if (listWifiConfigs == null) return null; + // 遍历判断是否存在 + for (int i = 0, len = listWifiConfigs.size(); i < len; i++) { + WifiConfiguration wConfig = listWifiConfigs.get(i); + if (wConfig != null) { + if (wConfig.networkId == networkId) { + return wConfig; + } + } + } + return null; + } + + // ============ + // = 配置操作 = + // ============ + + /** + * 删除指定的 Wifi(SSID) 配置信息 + * @param ssid Wifi ssid + * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public static boolean delWifiConfig(final String ssid) { + if (ssid == null) return false; + try { + // 初始化 WifiManager 对象 + WifiManager wifiManager = AppUtils.getWifiManager(); + // 获取 Wifi 连接过的配置信息 + List listWifiConfigs = wifiManager.getConfiguredNetworks(); + // 防止为 null + if (listWifiConfigs != null) { + // 遍历判断是否存在 + for (int i = 0, len = listWifiConfigs.size(); i < len; i++) { + WifiConfiguration wConfig = listWifiConfigs.get(i); + if (wConfig != null) { + if (wConfig.SSID.equals("\"" + ssid + "\"")) { + // 删除操作 + wifiManager.removeNetwork(wConfig.networkId); + } + } + } + // 保存操作 + return wifiManager.saveConfiguration(); + } + } catch (Exception e) { + ALog.eTag(TAG, e, "delWifiConfig"); + } + return false; + } + + // = + + /** + * 快速连接 Wifi ( 不使用静态 IP 方式 ) + * @param ssid Wifi ssid + * @param pwd Wifi 密码 + * @param type Wifi 加密类型 + * @return {@link WifiConfiguration} + */ + public WifiConfiguration quickConnWifi(final String ssid, final String pwd, final int type) { + return quickConnWifi(ssid, pwd, type, false, null); + } + + /** + * 快速连接 Wifi + * @param ssid Wifi ssid + * @param pwd Wifi 密码 + * @param type Wifi 加密类型 + * @param isStatic 是否使用静态 IP 连接 + * @param ip 静态 IP 地址 + * @return {@link WifiConfiguration} + */ + @SuppressLint("MissingPermission") + public WifiConfiguration quickConnWifi(final String ssid, final String pwd, final int type, final boolean isStatic, final String ip) { + // 步骤: + // 1. 创建 Wifi 静态 IP 连接配置 + // 2. 创建正常 Wifi 连接配置 + // 3. 查询准备连接的 Wifi SSID 是否存在配置文件, 准备进行删除 + // 4. 查询当前连接的 Wifi SSID 准备进行断开 + // 5. 同步进行断开, 删除操作, 并且进行保存 + // 6. 调用连接方法 + // 7. 返回连接的配置信息 + // = + try { + // 正常的 Wifi 连接配置 + WifiConfiguration connWifiConfig = null; + // 如果需要通过静态 IP 方式连接, 则进行设置 + if (isStatic && !TextUtils.isEmpty(ip)) { + // 创建 Wifi 静态 IP 连接配置 + WifiConfiguration staticWifiConfig = setStaticWifiConfig(createWifiConfig(ssid, pwd, type, true), ip); + // 如果静态 IP 方式, 配置失败, 则初始化正常连接的 Wifi 配置 + if (staticWifiConfig == null) { + // 创建正常的配置信息 + connWifiConfig = createWifiConfig(ssid, pwd, type, true); + // = + ALog.dTag(TAG, "属于正常方式连接 (DHCP)"); + } else { + // 设置静态信息 + connWifiConfig = staticWifiConfig; + // = + ALog.dTag(TAG, "属于静态 IP 方式连接"); + } + } else { + // 创建正常的配置信息 + connWifiConfig = createWifiConfig(ssid, pwd, type, true); + // = + ALog.dTag(TAG, "属于正常方式连接 (DHCP)"); + } + // 判断当前准备连接的 Wifi, 是否存在配置文件 + WifiConfiguration preWifiConfig = this.isExists(ssid); + // = + if (preWifiConfig != null) { + // 存在则删除 + boolean isRemove = mWifiManager.removeNetwork(preWifiConfig.networkId); + // 打印结果 + ALog.dTag(TAG, "删除旧的配置信息 - " + preWifiConfig.SSID + ", isRemove: " + isRemove); + // 保存配置 + mWifiManager.saveConfiguration(); + } + // = + // 连接网络 + int nId = mWifiManager.addNetwork(connWifiConfig); + if (nId != -1) { + try { + // 获取当前连接的 Wifi 对象 + WifiInfo wifiInfo = getWifiInfo(); + // 获取连接的 id + int networdId = wifiInfo.getNetworkId(); + // 禁用网络 + boolean isDisable = mWifiManager.disableNetwork(networdId); + // 断开之前的连接 + boolean isDisConnect = mWifiManager.disconnect(); + // 打印断开连接结果 + ALog.dTag(TAG, "isDisConnect: " + isDisConnect + ", isDisable: " + isDisable); + } catch (Exception e) { + ALog.eTag(TAG, e, "quickConnWifi - 关闭连接出错: " + nId); + } + // 开始连接 + boolean isResult = mWifiManager.enableNetwork(nId, true); + // = + if (!isResult) { + isResult = mWifiManager.enableNetwork(nId, true); + } + // 打印结果 + ALog.dTag(TAG, "addNetwork(enableNetwork) - result: " + isResult); + } else { + // 尝试不带引号 SSID 连接 + connWifiConfig.SSID = formatSSID(connWifiConfig.SSID, false); + // 连接网络 + nId = mWifiManager.addNetwork(connWifiConfig); + if (nId != -1) { + try { + // 获取当前连接的 Wifi 对象 + WifiInfo wifiInfo = getWifiInfo(); + // 获取连接的 id + int networdId = wifiInfo.getNetworkId(); + // 禁用网络 + boolean isDisable = mWifiManager.disableNetwork(networdId); + // 断开之前的连接 + boolean isDisConnect = mWifiManager.disconnect(); + // 打印断开连接结果 + ALog.dTag(TAG, "isDisConnect: " + isDisConnect + ", isDisable: " + isDisable); + } catch (Exception e) { + ALog.eTag(TAG, e, "quickConnWifi - 关闭连接出错: " + nId); + } + // 开始连接 + boolean isResult = mWifiManager.enableNetwork(nId, true); + // = + if (!isResult) { + isResult = mWifiManager.enableNetwork(nId, true); + } + // 打印结果 + ALog.dTag(TAG, "addNetwork(enableNetwork) - result: " + isResult); + } + } + // 保存 id + connWifiConfig.networkId = nId; + // 连接的 networkId + ALog.dTag(TAG, "连接的 SSID - networkId: " + nId); + // 返回连接的信息 + return connWifiConfig; + } catch (Exception e) { + ALog.eTag(TAG, "quickConnWifi", e); + } + return null; + } + + /** + * 创建 Wifi 配置信息 + * @param ssid Wifi ssid + * @param pwd Wifi 密码 + * @param type Wifi 加密类型 + * @param isHandler 是否处理双引号 + * @return {@link WifiConfiguration} + */ + public static WifiConfiguration createWifiConfig(final String ssid, final String pwd, final int type, final boolean isHandler) { + try { + // 创建一个新的网络配置 + WifiConfiguration wifiConfig = new WifiConfiguration(); + wifiConfig.allowedAuthAlgorithms.clear(); + wifiConfig.allowedGroupCiphers.clear(); + wifiConfig.allowedKeyManagement.clear(); + wifiConfig.allowedPairwiseCiphers.clear(); + wifiConfig.allowedProtocols.clear(); + wifiConfig.priority = 0; + // 设置连接的 SSID + if (isHandler) { + wifiConfig.SSID = formatSSID(ssid, true); + } else { + wifiConfig.SSID = ssid; + } + switch (type) { + case WifiUtils.NOPWD: // 不存在密码 + wifiConfig.hiddenSSID = true; + wifiConfig.allowedKeyManagement.set(KeyMgmt.NONE); + break; + case WifiUtils.WEP: // WEP 加密方式 + wifiConfig.hiddenSSID = true; + if (isHandler) { + if (isHexWepKey(pwd)) { + wifiConfig.wepKeys[0] = pwd; + } else { + wifiConfig.wepKeys[0] = "\"" + pwd + "\""; + } + } else { + wifiConfig.wepKeys[0] = pwd; + } + wifiConfig.allowedKeyManagement.set(KeyMgmt.NONE); + wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + // wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104); + wifiConfig.wepTxKeyIndex = 0; + break; + case WifiUtils.WPA: // WPA 加密方式 + if (isHandler) { + wifiConfig.preSharedKey = "\"" + pwd + "\""; + } else { + wifiConfig.preSharedKey = pwd; + } + wifiConfig.hiddenSSID = true; + wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + wifiConfig.allowedKeyManagement.set(KeyMgmt.WPA_PSK); +// wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.WPA); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + wifiConfig.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); + wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); + wifiConfig.status = WifiConfiguration.Status.ENABLED; + break; + } + return wifiConfig; + } catch (Exception e) { + ALog.eTag(TAG, e, "createWifiConfig"); + } + return null; + } + + // ============ + // = 连接操作 = + // ============ + + /** + * 移除 Wifi 配置信息 + * @param wifiConfig Wifi 配置信息 + * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public boolean removeWifiConfig(final WifiConfiguration wifiConfig) { + // 如果等于 null 则直接返回 + if (wifiConfig == null) return false; + try { + // 删除配置 + boolean isResult = mWifiManager.removeNetwork(wifiConfig.networkId); + // 保存操作 + mWifiManager.saveConfiguration(); + // 返回删除结果 + return isResult; + } catch (Exception e) { + ALog.eTag(TAG, "removeWifiConfig", e); + } + return false; + } + + /** + * 断开指定 networkId 的网络 + * @param networkId network id + * @return {@code true} success, {@code false} fail + */ + @SuppressLint("MissingPermission") + public boolean disconnectWifi(final int networkId) { + try { + mWifiManager.disableNetwork(networkId); + return mWifiManager.disconnect(); + } catch (Exception e) { + ALog.eTag(TAG, "disconnectWifi", e); + } + return false; + } + + // =========================== + // = 设置静态 IP、域名等信息 = + // =========================== + + /** + * 设置静态 Wifi 配置信息 + * @param wifiConfig Wifi 配置信息 + * @param ip 静态 IP + * @return {@link WifiConfiguration} + */ + private WifiConfiguration setStaticWifiConfig(final WifiConfiguration wifiConfig, final String ip) { + String gateway = null; + String dns; + if (ip != null) { + try { + InetAddress intetAddress = InetAddress.getByName(ip); + int intIp = inetAddressToInt(intetAddress); + dns = (intIp & 0xFF) + "." + ((intIp >> 8) & 0xFF) + "." + ((intIp >> 16) & 0xFF) + ".1"; + gateway = dns; + } catch (Exception e) { + ALog.eTag(TAG, e, "setStaticWifiConfig"); + return null; + } + } + // 暂时不需要设置 DNS, 所以 DNS 参数传入 null + return setStaticWifiConfig(wifiConfig, ip, gateway, null, 24); + } + + /** + * 设置静态 Wifi 配置信息 + * @param wifiConfig Wifi 配置信息 + * @param ip 静态 IP + * @param gateway 网关 + * @param dns DNS + * @param networkPrefixLength 网络前缀长度 + * @return {@link WifiConfiguration} + */ + private WifiConfiguration setStaticWifiConfig(final WifiConfiguration wifiConfig, final String ip, + final String gateway, final String dns, final int networkPrefixLength) { + try { + if (ip == null || gateway == null) return null; + // 设置 InetAddress + InetAddress intetAddress = InetAddress.getByName(ip); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) { // 旧的版本, 5.0 之前 + // 设置 IP 分配方式, 静态 IP + setEnumField(wifiConfig, "STATIC", "ipAssignment"); + // 设置不用代理 + setEnumField(wifiConfig, "NONE", "proxySettings"); + // 设置 IP 地址 + setIpAddress(intetAddress, networkPrefixLength, wifiConfig); + // 设置网关 + setGateway(InetAddress.getByName(gateway), wifiConfig); + if (dns != null) { // 判断是否需要设置域名 + // 设置 DNS + setDNS(InetAddress.getByName(dns), wifiConfig); + } + } else { // 5.0 新版本改变到其他地方 + Object object = getDeclaredField(wifiConfig, "mIpConfiguration"); + // 设置 IP 分配方式, 静态 IP + setEnumField(object, "STATIC", "ipAssignment"); + // 设置不用代理 + setEnumField(object, "NONE", "proxySettings"); + // 设置 IP 地址、网关、DNS + setStaticIpConfig(ip, gateway, dns, networkPrefixLength, object); + } + return wifiConfig; + } catch (Exception e) { + ALog.eTag(TAG, e, "setStaticWifiConfig"); + } + return null; + } + + /** + * 转换 IP 地址 + * @param inetAddr {@link InetAddress} + * @return IPv4 地址 + * @throws Exception 不属于 IPv4 地址 + */ + private int inetAddressToInt(final InetAddress inetAddr) throws Exception { + byte[] data = inetAddr.getAddress(); + if (data.length != 4) { + throw new IllegalArgumentException("Not an IPv4 address"); + } + return ((data[3] & 0xff) << 24) | ((data[2] & 0xff) << 16) | ((data[1] & 0xff) << 8) | (data[0] & 0xff); + } + + /** + * 设置 DNS + * @param dns DNS + * @param wifiConfig Wifi 配置信息 + * @throws Exception 设置失败, 抛出异常 + */ + private void setDNS(final InetAddress dns, final WifiConfiguration wifiConfig) throws Exception { + Object linkProperties = getField(wifiConfig, "linkProperties"); + if (linkProperties == null) + throw new NullPointerException(); + + List mDnses = (ArrayList) getDeclaredField(linkProperties, "mDnses"); + mDnses.clear(); // or add a new dns address, here I just want to replace DNS1 + mDnses.add(dns); + } + + /** + * 设置网关 + * @param gateway 网关 + * @param wifiConfig Wifi 配置信息 + * @throws Exception 设置失败, 抛出异常 + */ + private void setGateway(final InetAddress gateway, final WifiConfiguration wifiConfig) throws Exception { + Object linkProperties = getField(wifiConfig, "linkProperties"); + if (linkProperties == null) + throw new NullPointerException(); + + Class routeInfoClass = Class.forName("android.net.RouteInfo"); + Constructor routeInfoConstructor = routeInfoClass.getConstructor(InetAddress.class); + Object routeInfo = routeInfoConstructor.newInstance(gateway); + ArrayList mRoutes = (ArrayList) getDeclaredField(linkProperties, "mRoutes"); + mRoutes.clear(); + mRoutes.add(routeInfo); + } + + /** + * 设置 IP 地址 + * @param address IP 地址 + * @param prefixLength 网络前缀长度 + * @param wifiConfig Wifi 配置信息 + * @throws Exception 设置失败, 抛出异常 + */ + private void setIpAddress(final InetAddress address, final int prefixLength, final WifiConfiguration wifiConfig) throws Exception { + Object linkProperties = getField(wifiConfig, "linkProperties"); + if (linkProperties == null) + throw new NullPointerException(); + + Class laClass = Class.forName("android.net.LinkAddress"); + Constructor laConstructor = laClass.getConstructor(InetAddress.class, int.class); + Object linkAddress = laConstructor.newInstance(address, prefixLength); + ArrayList mLinkAddresses = (ArrayList) getDeclaredField(linkProperties, "mLinkAddresses"); + mLinkAddresses.clear(); + mLinkAddresses.add(linkAddress); + } + + /** + * 设置 IP 地址、网关、DNS (5.0 之后 ) + * @param ip 静态 IP + * @param gateway 网关 + * @param dns DNS + * @param prefixLength 网络前缀长度 + * @param object Wifi 配置信息 + * @throws Exception 设置失败, 抛出异常 + */ + private void setStaticIpConfig(final String ip, final String gateway, final String dns, final int prefixLength, final Object object) throws Exception { + // 从 WifiConfig 成员变量 mIpConfiguration 获取 staticIpConfiguration + // 获取 staticIpConfiguration 变量 + Object staticIpConfigClass = getField(object, "staticIpConfiguration"); + if (staticIpConfigClass == null) { + // 创建静态 IP 配置类 + staticIpConfigClass = Class.forName("android.net.StaticIpConfiguration").newInstance(); + } + // 初始化 LinkAddress 并设置 IP 地址 + Class laClass = Class.forName("android.net.LinkAddress"); + Constructor laConstructor = laClass.getConstructor(InetAddress.class, int.class); + Object linkAddress = laConstructor.newInstance(InetAddress.getByName(ip), prefixLength); + // 设置地址 IP 地址 ipAddress + setValueField(staticIpConfigClass, linkAddress, "ipAddress"); + // 设置网关 gateway + setValueField(staticIpConfigClass, InetAddress.getByName(gateway), "gateway"); + if (dns != null) { // 判断是否需要设置域名 + // 设置 DNS + List mDnses = (ArrayList) getDeclaredField(staticIpConfigClass, "dnsServers"); + mDnses.clear(); // or add a new dns address, here I just want to replace DNS1 + mDnses.add(InetAddress.getByName(dns)); + } + // 设置赋值 staticIpConfiguration 属性 + setValueField(object, staticIpConfigClass, "staticIpConfiguration"); + } + + /** + * 通过反射获取 + * @param object Object + * @param name 字段名 + * @return 对应的字段 + * @throws Exception 获取失败, 抛出异常 + */ + private Object getField(final Object object, final String name) throws Exception { + Field field = object.getClass().getField(name); + return field.get(object); + } + + /** + * 通过反射获取 + * @param object Object + * @param name 字段名 + * @return 对应的字段 + * @throws Exception 获取失败, 抛出异常 + */ + private Object getDeclaredField(final Object object, final String name) throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(object); + } + + /** + * 通过反射枚举类, 进行设置 + * @param object Object + * @param value 设置参数值 + * @param name 字段名 + * @throws Exception 设置失败, 抛出异常 + */ + private void setEnumField(final Object object, final String value, final String name) throws Exception { + Field field = object.getClass().getField(name); + field.set(object, Enum.valueOf((Class) field.getType(), value)); + } + + /** + * 通过反射, 进行设置 + * @param object Object + * @param val 设置参数值 + * @param name 字段名 + * @throws Exception 设置失败, 抛出异常 + */ + private void setValueField(final Object object, final Object val, final String name) throws Exception { + Field field = object.getClass().getField(name); + field.set(object, val); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/utils/wifi/WifiVo.java b/app/src/main/java/com/example/baseframe/utils/wifi/WifiVo.java new file mode 100644 index 0000000..42991ce --- /dev/null +++ b/app/src/main/java/com/example/baseframe/utils/wifi/WifiVo.java @@ -0,0 +1,148 @@ +package com.example.baseframe.utils.wifi; + +import android.net.wifi.ScanResult; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.Keep; + +import com.blankj.ALog; + +import java.util.ArrayList; +import java.util.List; + + +/** + * detail: Wifi 信息实体类 + * @author Ttt + */ +public class WifiVo implements Parcelable { + + // 日志 TAG + private static final String TAG = WifiVo.class.getSimpleName(); + + @Keep // Wifi ssid + public String wifiSSID = null; + @Keep // Wifi 密码 + public String wifiPwd = null; + @Keep // Wifi 加密类型 + public int wifiType = WifiUtils.NOPWD; + @Keep // Wifi 信号等级 + public int wifiLevel = 0; + + public WifiVo() { + } + + /** + * 获取 Wifi 信息 + * @param scanResult 扫描的 Wifi 信息 + * @return {@link WifiVo} + */ + public static WifiVo createWifiVo(final ScanResult scanResult) { + return createWifiVo(scanResult, false); + } + + /** + * 获取 Wifi 信息 + * @param scanResult 扫描的 Wifi 信息 + * @param isAppend {@code true} 添加引号, {@code false} 删除引号 + * @return {@link WifiVo} + */ + public static WifiVo createWifiVo(final ScanResult scanResult, final boolean isAppend) { + if (scanResult != null) { + try { + // 防止 Wifi 名长度为 0 + if (scanResult.SSID.length() == 0) { + return null; + } + WifiVo wifiVo = new WifiVo(); + // Wifi ssid + wifiVo.wifiSSID = WifiUtils.formatSSID(scanResult.SSID, isAppend); + // Wifi 加密类型 + wifiVo.wifiType = WifiUtils.getWifiType(scanResult.capabilities); + // Wifi 信号等级 + wifiVo.wifiLevel = scanResult.level; + return wifiVo; + } catch (Exception e) { + ALog.eTag(TAG, e, "createWifiVo"); + } + } + return null; + } + + /** + * 扫描 Wifi 信息 + * @param listScanResults 扫描返回的数据 + * @return {@link List} + */ + public static List scanWifiVos(final List listScanResults) { + List listWifiVos = new ArrayList<>(); + scanWifiVos(listWifiVos, listScanResults); + return listWifiVos; + } + + /** + * 扫描 Wifi 信息 + * @param listWifiVos 数据源 + * @param listScanResults 扫描返回的数据 + * @return {@code true} success, {@code false} fail + */ + public static boolean scanWifiVos(final List listWifiVos, final List listScanResults) { + if (listWifiVos == null || listScanResults == null) return false; + // 清空旧数据 + listWifiVos.clear(); + // 遍历 Wifi 列表数据 + for (int i = 0, len = listScanResults.size(); i < len; i++) { + // 如果出现异常、或者失败, 则无视当前的索引 Wifi 信息 + try { + // 获取当前索引的 Wifi 信息 + ScanResult scanResult = listScanResults.get(i); + // 防止 Wifi 名长度为 0 + if (scanResult.SSID.length() == 0) { + continue; + } + // 保存 Wifi 信息 + listWifiVos.add(createWifiVo(scanResult)); + } catch (Exception e) { + ALog.eTag(TAG, e, "scanWifiVos"); + } + } + return true; + } + + // ============== + // = Parcelable = + // ============== + + protected WifiVo(Parcel in) { + wifiSSID = in.readString(); + wifiPwd = in.readString(); + wifiType = in.readInt(); + wifiLevel = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public WifiVo createFromParcel(Parcel in) { + return new WifiVo(in); + } + + @Override + public WifiVo[] newArray(int size) { + return new WifiVo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeString(wifiSSID); + dest.writeString(wifiPwd); + dest.writeInt(wifiType); + dest.writeInt(wifiLevel); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/webview/BaseWebAcivity.java b/app/src/main/java/com/example/baseframe/webview/BaseWebAcivity.java new file mode 100644 index 0000000..8ee9751 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/BaseWebAcivity.java @@ -0,0 +1,423 @@ +package com.example.baseframe.webview; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; +import androidx.databinding.ViewDataBinding; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.base.BaseActivity; +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.utils.BarUtils; +import com.example.baseframe.webview.config.FullscreenHolder; +import com.example.baseframe.webview.config.IWebPageView; +import com.example.baseframe.webview.config.MyJavascriptInterface; +import com.example.baseframe.webview.config.MyWebChromeClient; +import com.example.baseframe.webview.config.MyWebViewClient; +import com.example.baseframe.webview.config.WebProgress; + + +/** + * Created by yzh on 2019/12/11 16:06. + */ +public abstract class BaseWebAcivity extends BaseActivity implements IWebPageView { + // 加载视频相关 + private MyWebChromeClient mWebChromeClient; + // 全屏时视频加载view + protected FrameLayout videoFullView; + protected WebView webView; + + // 进度条 + protected WebProgress mProgressBar; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + BarUtils.setNavBarColor(mContext,getColors(R.color.colorAccent)); + } + } + + + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + //设置相应的 设计图 dp 比率 + if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){ + //"横屏" + // DensityUtil.setDensity(getApplication(), this,960); + }else{ + // "竖屏" + // DensityUtil.setDensity(getApplication(), this,600); + } + } + + + + protected void back() { + //返回网页上一页 + if (webView.canGoBack() && !onPageError) { + webView.goBack(); + //退出网页 + } else { + handleFinish(); + } + } + + + protected void scrollChangeHeader(int scrolledY, View titleLayout) { + + if (scrolledY < 0) { + scrolledY = 0; + } + // 滑动多少距离后标题透明 + int slidingDistance = 500; + float alpha = Math.abs(scrolledY) * 1.0f / slidingDistance; + //ALog.i("scrolledY "+scrolledY+" "+alpha); + if (alpha <= 1f) { + titleLayout.setAlpha(1f - alpha); + } + + } + + + + @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface", "JavascriptInterface"}) + protected void initWebView() { + WebSettings ws = webView.getSettings(); + // 网页内容的宽度是否可大于WebView控件的宽度 + ws.setLoadWithOverviewMode(false); + // 保存表单数据 + ws.setSaveFormData(true); + // 是否应该支持使用其屏幕缩放控件和手势缩放 + ws.setSupportZoom(true); + ws.setBuiltInZoomControls(false); + ws.setDisplayZoomControls(false); + // 启动应用缓存 + ws.setAppCacheEnabled(true); + // 设置缓存模式 + ws.setCacheMode(WebSettings.LOAD_DEFAULT); + // setDefaultZoom api19被弃用 + // 设置此属性,可任意比例缩放。 + ws.setUseWideViewPort(true); + // 不缩放 + webView.setInitialScale(100); + // 告诉WebView启用JavaScript执行。默认的是false。 + ws.setJavaScriptEnabled(true); + // 页面加载好以后,再放开图片 + ws.setBlockNetworkImage(false); + // 使用localStorage则必须打开 + ws.setDomStorageEnabled(true); + // 排版适应屏幕 + ws.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); + // WebView是否新窗口打开(加了后可能打不开网页) +// ws.setSupportMultipleWindows(true); + + // webview从5.0开始默认不允许混合模式,https中不能加载http资源,需要设置开启。 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ws.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + /** 设置字体默认缩放大小(改变网页字体大小,setTextSize api14被弃用)*/ + ws.setTextZoom(100); + + mWebChromeClient = new MyWebChromeClient(this); + webView.setWebChromeClient(mWebChromeClient); + webView.setWebViewClient(new MyWebViewClient(this)); + webView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return handleLongImage(); + } + }); + + // 与js交互 + webView.addJavascriptInterface(new MyJavascriptInterface(this), "injectedObject"); + webView.addJavascriptInterface(this, "androidInjected"); + } + + int time=0; + @JavascriptInterface + public void reload(String s){ + ALog.v("reload==="+s); + if(time<3){ + runOnUiThread(() ->{ + webView.goBack();//先关闭加载的本地404页面,在刷新 + webView.postDelayed(() ->webView.reload(),1000); + }); + }else{ + ALog.w("java传递参数去调用 js方法"); + loadJs("javascript:callJsWithArgs('" + "请稍后再试哦~" + "')"); + } + + time++; + } + + + @Override + public void showWebView() { + webView.setVisibility(View.VISIBLE); + } + + @Override + public void hindWebView() { + webView.setVisibility(View.INVISIBLE); + } + + @Override + public void fullViewAddView(View view) { + FrameLayout decor = (FrameLayout) getWindow().getDecorView(); + videoFullView = new FullscreenHolder(this); + videoFullView.addView(view); + decor.addView(videoFullView); + } + + @Override + public void showVideoFullView() { + videoFullView.setVisibility(View.VISIBLE); + } + + @Override + public void hindVideoFullView() { + videoFullView.setVisibility(View.GONE); + } + + @Override + public void startProgress(int newProgress) { + mProgressBar.setWebProgress(newProgress); + } + + + + /** + * android与js交互: + * 前端注入js代码:不能加重复的节点,不然会覆盖 + * 前端调用js代码 + */ + @Override + public void onPageFinished(WebView view, String url) { + if (!WebTools.isNetworkConnected(this)) { + mProgressBar.hide(); + } + loadImageClickJS(); + loadTextClickJS(); + loadCallJS(); + } + + /** + * 处理是否唤起三方app + */ + @Override + public boolean isOpenThirdApp(String url) { + return WebTools.handleThirdApp(this, url); + } + + /** + * 网页是否加载失败了 + */ + private boolean onPageError; + + @Override + public void onReceivedError(int errorCode, String description) { + onPageError = true; + ALog.v("onReceivedError---"+onPageError); + } + + @Override + public void onPageStarted(String url) { + if(!WebTools.DEFAULT_ERROR.equals(url)){ + onPageError = false;//每次加载时初始化错误状态 + } + ALog.v("url: "+url); + } + + /** + * 前端注入JS: + * 这段js函数的功能就是,遍历所有的img节点,并添加onclick函数,函数的功能是在图片点击的时候调用本地java接口并传递url过去 + */ + public void loadImageClickJS() { + loadJs("javascript:(function(){" + + "var objs = document.getElementsByTagName(\"img\");" + + "for(var i=0;i节点,将节点里的属性传递过去(属性自定义,用于页面跳转) + */ + public void loadTextClickJS() { + loadJs("javascript:(function(){" + + "var objs =document.getElementsByTagName(\"li\");" + + "for(var i=0;i { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(jsString, null); + } else { + webView.loadUrl(jsString); + } + }); + + } + + + /** + * 长按事件处理 + */ + private boolean handleLongImage() { + final WebView.HitTestResult hitTestResult = webView.getHitTestResult(); + // 如果是图片类型或者是带有图片链接的类型 + if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE || + hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + // 弹出保存图片的对话框 +// new AlertDialog.Builder(WebViewActivity.this) +// .setItems(new String[]{"查看大图", "保存图片到相册"}, new DialogInterface.OnClickListener() { +// @Override +// public void onClick(DialogInterface dialog, int which) { +// String picUrl = hitTestResult.getExtra(); +// //获取图片 +// Log.e("picUrl","picUrl: "+ picUrl); +// switch (which) { +// case 0: +// break; +// case 1: +// break; +// default: +// break; +// } +// } +// }) +// .show(); + return true; + } + return false; + } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + //全屏播放退出全屏 + if (mWebChromeClient.inCustomView()) { + hideCustomView(); + return true; + } else { + finish(); + } + } + return false; + } + + +} diff --git a/app/src/main/java/com/example/baseframe/webview/WebTools.java b/app/src/main/java/com/example/baseframe/webview/WebTools.java new file mode 100644 index 0000000..c37d0bc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/WebTools.java @@ -0,0 +1,193 @@ +package com.example.baseframe.webview; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import com.example.baseframe.api.App; + + +public class WebTools { + + /** + * 网页加载失败时显示的本地默认网页 + */ + public static String DEFAULT_ERROR= "file:///android_asset/html/404.html"; + /** + * 判断网络是否连通 + */ + public static boolean isNetworkConnected(Context context) { + try { + if (context != null) { + @SuppressWarnings("static-access") + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } else { + /**如果context为空,就返回false,表示网络未连接*/ + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static boolean isWifiConnected(Context context) { + if (context != null) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && (info.getType() == ConnectivityManager.TYPE_WIFI); + } else { + /**如果context为null就表示为未连接*/ + return false; + } + } + + /** + * 将 Android5.0以下手机不能直接打开mp4后缀的链接 + * + * @param url 视频链接 + */ + public static String getVideoHtmlBody(String url) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + } + + /** + * 实现文本复制功能 + * + * @param content 复制的文本 + */ + public static void copy(String content) { + if (!TextUtils.isEmpty(content)) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { + ClipboardManager clipboard = (ClipboardManager) App.getInstance().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(content); + } else { + ClipboardManager clipboard = (ClipboardManager) App.getInstance().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(content, content); + clipboard.setPrimaryClip(clip); + } + } + } + + /** + * 使用浏览器打开链接 + */ + public static void openLink(Context context, String content) { + if (!TextUtils.isEmpty(content) && content.startsWith("http")) { + Uri issuesUrl = Uri.parse(content); + Intent intent = new Intent(Intent.ACTION_VIEW, issuesUrl); + context.startActivity(intent); + } + } + + /** + * 分享 + */ + public static void share(Context context, String extraText) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT,"分享"); + intent.putExtra(Intent.EXTRA_TEXT, extraText); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(Intent.createChooser(intent,"分享")); + } + + /** + * 通过包名找应用,不需要权限 + */ + public static boolean hasPackage(Context context, String packageName) { + if (null == context || TextUtils.isEmpty(packageName)) { + return false; + } + try { + context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_GIDS); + return true; + } catch (PackageManager.NameNotFoundException e) { + // 抛出找不到的异常,说明该程序已经被卸载 + return false; + } + } + + /** + * 处理三方链接 + * 网页里可能唤起其他的app + */ + public static boolean handleThirdApp(Activity activity, String backUrl) { + /**http开头直接跳过*/ + if (backUrl.startsWith("http")) { + // 可能有提示下载Apk文件 + if (backUrl.contains(".apk")) { + startActivity(activity, backUrl); + return true; + } + return false; + } + + boolean isJump = true; + /**屏蔽以下应用唤起App,可根据需求 添加或取消*/ + if ( + backUrl.startsWith("tbopen:")// 淘宝 +// || backUrl.startsWith("openapp.jdmobile:")// 京东 +// || backUrl.startsWith("jdmobile:")//京东 +// || backUrl.startsWith("alipay:")// 支付宝 +// || backUrl.startsWith("alipays:")//支付宝 + || backUrl.startsWith("zhihu:")// 知乎 + || backUrl.startsWith("vipshop:")// + || backUrl.startsWith("youku:")//优酷 + || backUrl.startsWith("uclink:")// UC + || backUrl.startsWith("ucbrowser:")// UC + || backUrl.startsWith("newsapp:")// + || backUrl.startsWith("sinaweibo:")// 新浪微博 + || backUrl.startsWith("suning:")// + || backUrl.startsWith("pinduoduo:")// 拼多多 + // || backUrl.startsWith("baiduboxapp:")// 百度 + || backUrl.startsWith("qtt:")// + ) { + isJump = false; + } + if (isJump) { + startActivity(activity, backUrl); + } + return isJump; + } + + private static void startActivity(Context context, String url) { + try { + + // 用于DeepLink测试 + if (url.startsWith("will://")) { + Uri uri = Uri.parse(url); + Log.e("---------scheme", uri.getScheme() + ";host: " + uri.getHost() + ";Id: " + uri.getPathSegments().get(0)); + } + + Intent intent = new Intent(); + intent.setAction("android.intent.action.VIEW"); + Uri uri = Uri.parse(url); + intent.setData(uri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/example/baseframe/webview/WebViewActivity.java b/app/src/main/java/com/example/baseframe/webview/WebViewActivity.java new file mode 100644 index 0000000..53daab2 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/WebViewActivity.java @@ -0,0 +1,223 @@ +package com.example.baseframe.webview; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; + +import com.blankj.ALog; +import com.example.baseframe.R; +import com.example.baseframe.api.App; + +import com.example.baseframe.base.BaseViewModel; +import com.example.baseframe.databinding.ActivityWebviewBinding; +import com.example.baseframe.utils.StatusBarUtil; + + +/** + * 公用展示的WebView的Activity + * Created by yzh on 2019/12/11. + */ + +public class WebViewActivity extends BaseWebAcivity { + + // 网页链接 + private String mUrl; + // 可滚动的title 使用简单 没有渐变效果,文字两旁有阴影 + private Toolbar mTitleToolBar; + private String mTitle; + + int type = 0; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //setContentView(R.layout.activity_webview); + } + + @Override + protected int getLayoutId() { + return R.layout.activity_webview; + } + + @Override + public void initViewObservable() { + } + + @Override + protected void initView() { + mUrl = getIntent().getStringExtra("mUrl"); + mTitle = getIntent().getStringExtra("mTitle"); + type = getIntent().getIntExtra("type", 0); + initTitle(); + initWebView(); + handleLoadUrl(); + getDataFromBrowser(getIntent()); + } + + + + private void handleLoadUrl() { + if (!TextUtils.isEmpty(mUrl) && mUrl.endsWith("mp4") && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + webView.loadData(WebTools.getVideoHtmlBody(mUrl), "text/html", "UTF-8"); + } else { + webView.loadUrl(mUrl); + } + } + + private void initTitle() { + StatusBarUtil.setColor(this, ContextCompat.getColor(this, R.color.colorAccent), 0); + mProgressBar = mBinding.pbProgress; + mProgressBar.setColor(ContextCompat.getColor(this, R.color.ui_blue)); + mProgressBar.show(); + webView =mBinding.webviewDetail; + initToolBar(); +// webView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> +// scrollChangeHeader(scrollY,titleLayout) +// ); + } + + + + private void initToolBar() { + mTitleToolBar = mBinding.commonToolbar; + setSupportActionBar(mTitleToolBar); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + //去除默认Title显示 + actionBar.setDisplayShowTitleEnabled(false); + } + mTitleToolBar.setOverflowIcon(ContextCompat.getDrawable(this, R.drawable.actionbar_more)); + mBinding.commonTitle.postDelayed(() -> mBinding.commonTitle.setSelected(true), 1000); + setTitle(mTitle); + + mTitleToolBar.setNavigationOnClickListener(v -> back()); + } + + + + + + + + /** + * 使用singleTask启动模式的Activity在系统中只会存在一个实例。 + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + getDataFromBrowser(intent); + } + + + + @Override + public void setTitle(String mTitle) { + if (mBinding.commonTitle != null) + mBinding.commonTitle.setText(mTitle); + } + + @Override + protected void onPause() { + super.onPause(); + webView.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + webView.onResume(); + // 支付宝网页版在打开文章详情之后,无法点击按钮下一步 + webView.resumeTimers(); + // 设置为横屏 + if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + } + + @Override + protected void onDestroy() { + if (videoFullView != null) { + videoFullView.removeAllViews(); + videoFullView = null; + } + if (webView != null) { + ViewGroup parent = (ViewGroup) webView.getParent(); + if (parent != null) { + parent.removeView(webView); + } + webView.removeAllViews(); + webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null); + webView.stopLoading(); + webView.setWebChromeClient(null); + webView.setWebViewClient(null); + webView.destroy(); + webView = null; + } + super.onDestroy(); + } + + + /** + * 打开网页: + * + * @param mUrl 要加载的网页url + * @param mTitle 标题 + */ + public static void loadUrl(String mUrl, String mTitle) { + Intent intent = new Intent(App.getInstance(), WebViewActivity.class); + intent.putExtra("mUrl", mUrl); + intent.putExtra("mTitle", mTitle == null ? "加载中..." : mTitle); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + App.getInstance().startActivity(intent); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_webview, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) {// 返回键 + handleFinish(); + + } else if (itemId == R.id.actionbar_share) {// 分享到 + String shareText = webView.getTitle() + webView.getUrl(); + WebTools.share(WebViewActivity.this, shareText); + + } else if (itemId == R.id.actionbar_cope) {// 复制链接 + WebTools.copy(webView.getUrl()); + Toast.makeText(this, "复制成功", Toast.LENGTH_LONG).show(); + + } else if (itemId == R.id.actionbar_open) {// 打开链接 + WebTools.openLink(WebViewActivity.this, webView.getUrl()); + + } else if (itemId == R.id.actionbar_webview_refresh) {// 刷新页面 + webView.reload(); + } + return super.onOptionsItemSelected(item); + } + + + @Override + public void startProgress(int newProgress) { + super.startProgress(newProgress); + ALog.i("newProgress:"+newProgress); + + + } +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/FullscreenHolder.java b/app/src/main/java/com/example/baseframe/webview/config/FullscreenHolder.java new file mode 100644 index 0000000..315c8c9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/FullscreenHolder.java @@ -0,0 +1,22 @@ +package com.example.baseframe.webview.config; + +import android.content.Context; +import android.view.MotionEvent; +import android.widget.FrameLayout; + +/** + * Created by jingbin on 2016/11/17. + */ + +public class FullscreenHolder extends FrameLayout { + + public FullscreenHolder(Context ctx) { + super(ctx); + setBackgroundColor(ctx.getResources().getColor(android.R.color.black)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return true; + } +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/IWebPageView.java b/app/src/main/java/com/example/baseframe/webview/config/IWebPageView.java new file mode 100644 index 0000000..56b8d59 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/IWebPageView.java @@ -0,0 +1,87 @@ +package com.example.baseframe.webview.config; + +import android.content.Intent; +import android.view.View; +import android.webkit.WebView; +import android.widget.FrameLayout; + +/** + * Created by jingbin on 2019/07/27. + */ +public interface IWebPageView { + + /** + * 显示webview + */ + void showWebView(); + + /** + * 隐藏webview + */ + void hindWebView(); + + /** + * 进度条变化时调用 + * + * @param newProgress 进度0-100 + */ + void startProgress(int newProgress); + + /** + * 添加视频全屏view + */ + void fullViewAddView(View view); + + /** + * 显示全屏view + */ + void showVideoFullView(); + + /** + * 隐藏全屏view + */ + void hindVideoFullView(); + + /** + * 设置横竖屏 + */ + void setRequestedOrientation(int screenOrientationPortrait); + + /** + * 得到全屏view + */ + FrameLayout getVideoFullView(); + + /** + * 加载视频进度条 + */ + View getVideoLoadingProgressView(); + + /** + * 返回标题处理 + */ + void onReceivedTitle(WebView view, String title); + + /** + * 上传图片打开文件夹 + */ + void startFileChooserForResult(Intent intent, int requestCode); + + /** + * 页面加载结束,添加js监听等 + */ + void onPageFinished(WebView view, String url); + + /** + * 是否处理打开三方app + * @param url + */ + boolean isOpenThirdApp(String url); + + /** + * 网页加载失败 + */ + void onReceivedError(int errorCode, String description); + + void onPageStarted(String url); +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/MyJavascriptInterface.java b/app/src/main/java/com/example/baseframe/webview/config/MyJavascriptInterface.java new file mode 100644 index 0000000..c28b130 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/MyJavascriptInterface.java @@ -0,0 +1,64 @@ +package com.example.baseframe.webview.config; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.JavascriptInterface; + +/** + * Created by yzh + * js通信接口 + */ +public class MyJavascriptInterface { + private Context context; + + public MyJavascriptInterface(Context context) { + this.context = context; + } + + /** + * 前端代码嵌入js: + * imageClick 名应和js函数方法名一致 + * + * @param src 图片的链接 + */ + @JavascriptInterface + public void imageClick(String src) { + Log.e("imageClick", "----点击了图片"); + Log.e("src", src); + } + + /** + * 前端代码嵌入js + * 遍历
  • 节点 + * + * @param type
  • 节点下type属性的值 + * @param item_pk item_pk属性的值 + */ + @JavascriptInterface + public void textClick(String type, String item_pk) { + if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(item_pk)) { + Log.e("textClick", "----点击了文字"); + Log.e("type", type); + Log.e("item_pk", item_pk); + } + } + + /** + * 网页使用的js,方法无参数 + */ + @JavascriptInterface + public void startFunction() { + Log.e("startFunction", "----无参"); + } + + /** + * 网页使用的js,方法有参数,且参数名为data + * + * @param data 网页js里的参数名 + */ + @JavascriptInterface + public void startFunction(String data) { + Log.e("startFunction", "----有参" + data); + } +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/MyWebChromeClient.java b/app/src/main/java/com/example/baseframe/webview/config/MyWebChromeClient.java new file mode 100644 index 0000000..df3041e --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/MyWebChromeClient.java @@ -0,0 +1,187 @@ +package com.example.baseframe.webview.config; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.net.Uri; +import android.os.Build; +import android.view.View; +import android.webkit.PermissionRequest; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; + +import androidx.annotation.RequiresApi; + +import static android.app.Activity.RESULT_OK; + + +/** + * Created by jingbin on 2019/07/27. + * - 播放网络视频配置 + * - 上传图片(兼容) + */ +public class MyWebChromeClient extends WebChromeClient { + + private ValueCallback mUploadMessage; + private ValueCallback mUploadMessageForAndroid5; + public static int FILECHOOSER_RESULTCODE = 1; + public static int FILECHOOSER_RESULTCODE_FOR_ANDROID_5 = 2; + + private View mXProgressVideo; + private IWebPageView mIWebPageView; + private View mXCustomView; + private CustomViewCallback mXCustomViewCallback; + + public MyWebChromeClient(IWebPageView mIWebPageView) { + this.mIWebPageView = mIWebPageView; + } + + /** + * 播放网络视频时全屏会被调用的方法 + */ + @Override + public void onShowCustomView(View view, CustomViewCallback callback) { + mIWebPageView.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + mIWebPageView.hindWebView(); + // 如果一个视图已经存在,那么立刻终止并新建一个 + if (mXCustomView != null) { + callback.onCustomViewHidden(); + return; + } + + mIWebPageView.fullViewAddView(view); + mXCustomView = view; + mXCustomViewCallback = callback; + mIWebPageView.showVideoFullView(); + } + + /** + * 视频播放退出全屏会被调用的 + */ + @Override + public void onHideCustomView() { + // 不是全屏播放状态 + if (mXCustomView == null) { + return; + } + mIWebPageView.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + mXCustomView.setVisibility(View.GONE); + if (mIWebPageView.getVideoFullView() != null) { + mIWebPageView.getVideoFullView().removeView(mXCustomView); + } + mXCustomView = null; + mIWebPageView.hindVideoFullView(); + mXCustomViewCallback.onCustomViewHidden(); + mIWebPageView.showWebView(); + } + + /** + * 视频加载时loading + */ + @Override + public View getVideoLoadingProgressView() { + if (mXProgressVideo == null) { + mXProgressVideo = mIWebPageView.getVideoLoadingProgressView(); + } + return mXProgressVideo; + } + + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + mIWebPageView.startProgress(newProgress); + } + + /** + * 判断是否是全屏 + */ + public boolean inCustomView() { + return (mXCustomView != null); + } + + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + // 设置title + mIWebPageView.onReceivedTitle(view, title); + } + + //扩展浏览器上传文件 + //3.0++版本 + public void openFileChooser(ValueCallback uploadMsg, String acceptType) { + openFileChooserImpl(uploadMsg); + } + + //3.0--版本 + public void openFileChooser(ValueCallback uploadMsg) { + openFileChooserImpl(uploadMsg); + } + + public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) { + openFileChooserImpl(uploadMsg); + } + + // For Android > 5.0 + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback uploadMsg, FileChooserParams fileChooserParams) { + openFileChooserImplForAndroid5(uploadMsg); + return true; + } + + private void openFileChooserImpl(ValueCallback uploadMsg) { + mUploadMessage = uploadMsg; + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("image/*"); + mIWebPageView.startFileChooserForResult(Intent.createChooser(intent, "文件选择"), FILECHOOSER_RESULTCODE); + } + + private void openFileChooserImplForAndroid5(ValueCallback uploadMsg) { + mUploadMessageForAndroid5 = uploadMsg; + Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); + contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); + contentSelectionIntent.setType("image/*"); + + Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); + chooserIntent.putExtra(Intent.EXTRA_TITLE, "图片选择"); + + mIWebPageView.startFileChooserForResult(chooserIntent, FILECHOOSER_RESULTCODE_FOR_ANDROID_5); + } + + /** + * 5.0以下 上传图片成功后的回调 + */ + public void mUploadMessage(Intent intent, int resultCode) { + if (null == mUploadMessage) { + return; + } + Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData(); + mUploadMessage.onReceiveValue(result); + mUploadMessage = null; + } + + /** + * 5.0以上 上传图片成功后的回调 + */ + public void mUploadMessageForAndroid5(Intent intent, int resultCode) { + if (null == mUploadMessageForAndroid5) { + return; + } + Uri result = (intent == null || resultCode != RESULT_OK) ? null : intent.getData(); + if (result != null) { + mUploadMessageForAndroid5.onReceiveValue(new Uri[]{result}); + } else { + mUploadMessageForAndroid5.onReceiveValue(new Uri[]{}); + } + mUploadMessageForAndroid5 = null; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onPermissionRequest(PermissionRequest request) { + super.onPermissionRequest(request); + request.grant(request.getResources()); + } +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/MyWebViewClient.java b/app/src/main/java/com/example/baseframe/webview/config/MyWebViewClient.java new file mode 100644 index 0000000..44a91f6 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/MyWebViewClient.java @@ -0,0 +1,118 @@ +package com.example.baseframe.webview.config; + +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.SslErrorHandler; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.blankj.ALog; +import com.example.baseframe.webview.WebTools; + + +/** + * Created by jingbin on 2016/11/17. + * 监听网页链接: + * - 根据标识:打电话、发短信、发邮件 + * - 进度条的显示 + * - 添加javascript监听 + * - 唤起京东,支付宝,微信原生App + */ +public class MyWebViewClient extends WebViewClient { + + private IWebPageView mIWebPageView; + + public MyWebViewClient(IWebPageView mIWebPageView) { + this.mIWebPageView = mIWebPageView; + } + + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + mIWebPageView.onPageStarted(url); + + WebSettings webSettings = view.getSettings(); + Log.v("MyWebViewClient","onPageStarted()-返回 userAgent: "+webSettings.getUserAgentString()); + } + + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Log.e("jing", "----url:" + url); + if (TextUtils.isEmpty(url)) { + return false; + } + return mIWebPageView.isOpenThirdApp(url); + } + + + @Override + public void onPageFinished(WebView view, String url) { + // html加载完成之后,添加监听图片的点击js函数 + mIWebPageView.onPageFinished(view, url); + super.onPageFinished(view, url); + } + + //这个方法在 android 6.0一下会回调这个 + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + ALog.v(errorCode+"---onReceivedError---"+description); + //有些网页回调了些错误,但依旧能打开网址:如net::ERR_CONNECTION_REFUSED -6 是不需要显示本地的错误页的 + + if (errorCode !=-6) { + //用javascript隐藏系统定义的404页面信息 +// String data = "Page NO FOUND!"; +// view.loadUrl("javascript:document.body.innerHTML=\"" + data + "\""); + + view.loadUrl(WebTools.DEFAULT_ERROR);//加载自定义错误页面html(注意会影响回退栈,失败了返回需要直接finish) + mIWebPageView.onReceivedError(errorCode, description); + } + } + + //这个方法在 android 6.0以上会回调这个 + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + //屏蔽系统默认的错误页面 + super.onReceivedError(view, request, error); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int errorCode=error.getErrorCode(); + ALog.v(errorCode+"---onReceivedError---"+error.getDescription()); + if (errorCode == 500||errorCode == 404||errorCode == -2) { + view.loadUrl(WebTools.DEFAULT_ERROR);//显示本地失败的html(注意会影响回退栈,失败了返回需要直接finish) + mIWebPageView.onReceivedError(errorCode, error.getDescription().toString()); + } + } + + } + + + // SSL Error. Failed to validate the certificate chain,error: java.security.cert.CertPathValidatorExcept + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + ALog.e(error.toString()); +// if(isDbug){ +// //测试环境默认信任所有htpps的证书 + handler.proceed(); +// }else{ +// super.onReceivedSslError(view, handler, error); +// } + } + + // 视频全屏播放按返回页面被放大的问题 + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + super.onScaleChanged(view, oldScale, newScale); + if (newScale - oldScale > 7) { + view.setInitialScale((int) (oldScale / newScale * 100)); //异常放大,缩回去。 + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/webview/config/WebProgress.java b/app/src/main/java/com/example/baseframe/webview/config/WebProgress.java new file mode 100644 index 0000000..970c13f --- /dev/null +++ b/app/src/main/java/com/example/baseframe/webview/config/WebProgress.java @@ -0,0 +1,359 @@ +package com.example.baseframe.webview.config; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; + +/** + * WebView进度条,原作者: cenxiaozhong,在此基础上修改优化: + * 1. progress同时返回两次100时进度条出现两次 + * 2. 当一条进度没跑完,又点击其他链接开始第二次进度时,第二次进度不出现 + * 3. 修改消失动画时长,使其消失时看到可以进度跑完 + * + * @author jingbin + * Link to https://github.com/youlookwhat/WebProgress + */ +public class WebProgress extends FrameLayout { + + /** + * 默认匀速动画最大的时长 + */ + public static final int MAX_UNIFORM_SPEED_DURATION = 8 * 1000; + /** + * 默认加速后减速动画最大时长 + */ + public static final int MAX_DECELERATE_SPEED_DURATION = 450; + /** + * 95f-100f时,透明度1f-0f时长 + */ + public static final int DO_END_ALPHA_DURATION = 630; + /** + * 95f - 100f动画时长 + */ + public static final int DO_END_PROGRESS_DURATION = 500; + /** + * 当前匀速动画最大的时长 + */ + private static int CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION; + /** + * 当前加速后减速动画最大时长 + */ + private static int CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION; + /** + * 默认的高度(dp) + */ + public static int WEB_PROGRESS_DEFAULT_HEIGHT = 3; + /** + * 进度条颜色默认 + */ + public static String WEB_PROGRESS_COLOR = "#2483D9"; + /** + * 进度条颜色 + */ + private int mColor; + /** + * 进度条的画笔 + */ + private Paint mPaint; + /** + * 进度条动画 + */ + private Animator mAnimator; + /** + * 控件的宽度 + */ + private int mTargetWidth = 0; + /** + * 控件的高度 + */ + private int mTargetHeight; + /** + * 标志当前进度条的状态 + */ + private int TAG = 0; + /** + * 第一次过来进度show,后面就是setProgress + */ + private boolean isShow = false; + public static final int UN_START = 0; + public static final int STARTED = 1; + public static final int FINISH = 2; + private float mCurrentProgress = 0F; + + public WebProgress(Context context) { + this(context, null); + } + + public WebProgress(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public WebProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + mPaint = new Paint(); + mColor = Color.parseColor(WEB_PROGRESS_COLOR); + mPaint.setAntiAlias(true); + mPaint.setColor(mColor); + mPaint.setDither(true); + mPaint.setStrokeCap(Paint.Cap.SQUARE); + + mTargetWidth = context.getResources().getDisplayMetrics().widthPixels; + mTargetHeight = dip2px(WEB_PROGRESS_DEFAULT_HEIGHT); + } + + /** + * 设置单色进度条 + */ + public void setColor(int color) { + this.mColor = color; + mPaint.setColor(color); + } + + public void setColor(String color) { + this.setColor(Color.parseColor(color)); + } + + public void setColor(int startColor, int endColor) { + LinearGradient linearGradient = new LinearGradient(0, 0, mTargetWidth, mTargetHeight, startColor, endColor, Shader.TileMode.CLAMP); + mPaint.setShader(linearGradient); + } + + /** + * 设置渐变色进度条 + * + * @param startColor 开始颜色 + * @param endColor 结束颜色 + */ + public void setColor(String startColor, String endColor) { + this.setColor(Color.parseColor(startColor), Color.parseColor(endColor)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int wMode = MeasureSpec.getMode(widthMeasureSpec); + int w = MeasureSpec.getSize(widthMeasureSpec); + + int hMode = MeasureSpec.getMode(heightMeasureSpec); + int h = MeasureSpec.getSize(heightMeasureSpec); + + if (wMode == MeasureSpec.AT_MOST) { + w = w <= getContext().getResources().getDisplayMetrics().widthPixels ? w : getContext().getResources().getDisplayMetrics().widthPixels; + } + if (hMode == MeasureSpec.AT_MOST) { + h = mTargetHeight; + } + this.setMeasuredDimension(w, h); + } + + @Override + protected void onDraw(Canvas canvas) { + + } + + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.drawRect(0, 0, mCurrentProgress / 100 * Float.valueOf(this.getWidth()), this.getHeight(), mPaint); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + this.mTargetWidth = getMeasuredWidth(); + int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels; + if (mTargetWidth >= screenWidth) { + CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION; + CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION; + } else { + //取比值 + float rate = this.mTargetWidth / Float.valueOf(screenWidth); + CURRENT_MAX_UNIFORM_SPEED_DURATION = (int) (MAX_UNIFORM_SPEED_DURATION * rate); + CURRENT_MAX_DECELERATE_SPEED_DURATION = (int) (MAX_DECELERATE_SPEED_DURATION * rate); + } + } + + private void setFinish() { + isShow = false; + TAG = FINISH; + } + + private void startAnim(boolean isFinished) { + + float v = isFinished ? 100 : 95; + + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + } + mCurrentProgress = mCurrentProgress == 0f ? 0.00000001f : mCurrentProgress; + + if (!isFinished) { + ValueAnimator mAnimator = ValueAnimator.ofFloat(mCurrentProgress, v); + float residue = 1f - mCurrentProgress / 100 - 0.05f; + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.setDuration((long) (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION)); + mAnimator.addUpdateListener(mAnimatorUpdateListener); + mAnimator.start(); + this.mAnimator = mAnimator; + } else { + + ValueAnimator segment95Animator = null; + if (mCurrentProgress < 95f) { + segment95Animator = ValueAnimator.ofFloat(mCurrentProgress, 95); + float residue = 1f - mCurrentProgress / 100f - 0.05f; + segment95Animator.setInterpolator(new LinearInterpolator()); + segment95Animator.setDuration((long) (residue * CURRENT_MAX_DECELERATE_SPEED_DURATION)); + segment95Animator.setInterpolator(new DecelerateInterpolator()); + segment95Animator.addUpdateListener(mAnimatorUpdateListener); + } + + ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); + mObjectAnimator.setDuration(DO_END_ALPHA_DURATION); + ValueAnimator mValueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f); + mValueAnimatorEnd.setDuration(DO_END_PROGRESS_DURATION); + mValueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener); + + AnimatorSet mAnimatorSet = new AnimatorSet(); + mAnimatorSet.playTogether(mObjectAnimator, mValueAnimatorEnd); + + if (segment95Animator != null) { + AnimatorSet mAnimatorSet1 = new AnimatorSet(); + mAnimatorSet1.play(mAnimatorSet).after(segment95Animator); + mAnimatorSet = mAnimatorSet1; + } + mAnimatorSet.addListener(mAnimatorListenerAdapter); + mAnimatorSet.start(); + mAnimator = mAnimatorSet; + } + + TAG = STARTED; + } + + private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = (float) animation.getAnimatedValue(); + WebProgress.this.mCurrentProgress = t; + WebProgress.this.invalidate(); + } + }; + + private AnimatorListenerAdapter mAnimatorListenerAdapter = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + doEnd(); + } + }; + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + /** + * animator cause leak , if not cancel; + */ + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + mAnimator = null; + } + } + + private void doEnd() { + if (TAG == FINISH && mCurrentProgress == 100f) { + setVisibility(GONE); + mCurrentProgress = 0f; + this.setAlpha(1f); + } + TAG = UN_START; + } + + public void reset() { + mCurrentProgress = 0; + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + } + } + + public void setProgress(int newProgress) { + setProgress(Float.valueOf(newProgress)); + } + + + public LayoutParams offerLayoutParams() { + return new LayoutParams(mTargetWidth, mTargetHeight); + } + + private int dip2px(float dpValue) { + final float scale = getContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public void setProgress(float progress) { + // fix 同时返回两个 100,产生两次进度条的问题; + if (TAG == UN_START && progress == 100f) { + setVisibility(View.GONE); + return; + } + + if (getVisibility() == View.GONE) { + setVisibility(View.VISIBLE); + } + if (progress < 95f) { + return; + } + if (TAG != FINISH) { + startAnim(true); + } + } + + /** + * 显示进度条 + */ + public void show() { + isShow = true; + setVisibility(View.VISIBLE); + mCurrentProgress = 0f; + startAnim(false); + } + + /** + * 进度完成后消失 + */ + public void hide() { + setWebProgress(100); + } + + /** + * 为单独处理WebView进度条 + */ + public void setWebProgress(int newProgress) { + if (newProgress >= 0 && newProgress < 95) { + if (!isShow) { + show(); + } else { + setProgress(newProgress); + } + } else { + setProgress(newProgress); + setFinish(); + } + } +} + diff --git a/app/src/main/java/com/example/baseframe/weight/AudioWaveView.java b/app/src/main/java/com/example/baseframe/weight/AudioWaveView.java new file mode 100644 index 0000000..b54042a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/AudioWaveView.java @@ -0,0 +1,111 @@ +package com.example.baseframe.weight; + +/** + * @anthor yzh + * @time 2019/12/2 9:52 + */ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Random; + +public class AudioWaveView extends View { + private Paint paint; + private RectF rectF1; + private RectF rectF2; + private RectF rectF3; + private RectF rectF4; + private RectF rectF5; + private int viewWidth; + private int viewHeight; + /** 每个条的宽度 */ + private int rectWidth; + /** 条数 */ + private int columnCount = 5; + /** 条间距 */ + private final int space = 6; + /** 条随机高度 */ + private int randomHeight; + private Random random; + @SuppressLint("HandlerLeak") + private Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + invalidate(); + } + }; + + public AudioWaveView(Context context) { + super(context); + init(); + } + + public AudioWaveView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + viewWidth = MeasureSpec.getSize(widthMeasureSpec); + viewHeight = MeasureSpec.getSize(heightMeasureSpec); + + rectWidth = (viewWidth - space * (columnCount - 1)) / columnCount; + } + + private void init() { + paint = new Paint(); + paint.setColor(Color.LTGRAY); + paint.setStyle(Paint.Style.FILL); + random = new Random(); + + initRect(); + } + + private void initRect() { + rectF1 = new RectF(); + rectF2 = new RectF(); + rectF3 = new RectF(); + rectF4 = new RectF(); + rectF5 = new RectF(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int left = rectWidth + space; + + //画每个条之前高度都重新随机生成 + randomHeight = random.nextInt(viewHeight); + rectF1.set(left * 0, randomHeight, left * 0 + rectWidth, viewHeight); + randomHeight = random.nextInt(viewHeight); + rectF2.set(left * 1, randomHeight, left * 1 + rectWidth, viewHeight); + randomHeight = random.nextInt(viewHeight); + rectF3.set(left * 2, randomHeight, left * 2 + rectWidth, viewHeight); + randomHeight = random.nextInt(viewHeight); + rectF4.set(left * 3, randomHeight, left * 3 + rectWidth, viewHeight); + randomHeight = random.nextInt(viewHeight); + rectF5.set(left * 4, randomHeight, left * 4 + rectWidth, viewHeight); + + canvas.drawRect(rectF1, paint); + canvas.drawRect(rectF2, paint); + canvas.drawRect(rectF3, paint); + canvas.drawRect(rectF4, paint); + canvas.drawRect(rectF5, paint); + + handler.sendEmptyMessageDelayed(0, 200); //每间隔200毫秒发送消息刷新 + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/CountDownView.java b/app/src/main/java/com/example/baseframe/weight/CountDownView.java new file mode 100644 index 0000000..eddd1e7 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/CountDownView.java @@ -0,0 +1,150 @@ +package com.example.baseframe.weight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import com.example.baseframe.R; +import com.example.baseframe.utils.DensityUtil; + + +/** + * 环形倒计时view + */ +public class CountDownView extends View { + //圆轮颜色 + private int mRingColor; + //圆轮宽度 + private int mRingWidth; + //圆轮进度值文本大小 + private int mRingProgessTextSize; + //宽度 + private int mWidth; + //高度 + private int mHeight; + private Paint mPaint; + //圆环的矩形区域 + private RectF mRectF; + // + private int mProgessTextColor; + private int mCountdownTime; + private float mCurrentProgress; + private OnCountDownFinishListener mListener; + + public CountDownView(Context context) { + this(context, null); + } + + public CountDownView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CountDownView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CountDownView); + mRingColor = a.getColor(R.styleable.CountDownView_ringColor, context.getResources().getColor(R.color.colorAccent)); + //mRingWidth = a.getFloat(R.styleable.CountDownView_ringWidth, 40); + mRingWidth= a.getDimensionPixelSize(R.styleable.CountDownView_ringWidth, 30); + mRingProgessTextSize = a.getDimensionPixelSize(R.styleable.CountDownView_progressTextSize, DensityUtil.sp2px(20)); + mProgessTextColor = a.getColor(R.styleable.CountDownView_progressTextColor, context.getResources().getColor(R.color.colorAccent)); + mCountdownTime = a.getInteger(R.styleable.CountDownView_countdownTime, 60); + a.recycle(); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setAntiAlias(true); + this.setWillNotDraw(false); + } + + public void setCountdownTime(int mCountdownTime) { + this.mCountdownTime = mCountdownTime; + } + + @SuppressLint("DrawAllocation") + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mWidth = getMeasuredWidth(); + mHeight = getMeasuredHeight(); + mRectF = new RectF(0 + mRingWidth / 2, 0 + mRingWidth / 2, + mWidth - mRingWidth / 2, mHeight - mRingWidth / 2); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + /** + *圆环 + */ + //颜色 + mPaint.setColor(mRingColor); + //空心 + mPaint.setStyle(Paint.Style.STROKE); + //宽度 + mPaint.setStrokeWidth(mRingWidth); + canvas.drawArc(mRectF, -90, mCurrentProgress - 360, false, mPaint); + //绘制文本 + Paint textPaint = new Paint(); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Paint.Align.CENTER); + String text = mCountdownTime - (int) (mCurrentProgress / 360f * mCountdownTime) + "s"; + textPaint.setTextSize(mRingProgessTextSize); + textPaint.setColor(mProgessTextColor); + + //文字居中显示 + Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt(); + int baseline = (int) ((mRectF.bottom + mRectF.top - fontMetrics.bottom - fontMetrics.top) / 2); + canvas.drawText(text, mRectF.centerX(), baseline, textPaint); + } + + private ValueAnimator getValA(long countdownTime) { + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 100); + valueAnimator.setDuration(countdownTime); + valueAnimator.setInterpolator(new LinearInterpolator()); + valueAnimator.setRepeatCount(0); + return valueAnimator; + } + /** + * 开始倒计时 + */ + public void startCountDown() { + setClickable(false); + ValueAnimator valueAnimator = getValA(mCountdownTime * 1000); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float i = Float.valueOf(String.valueOf(animation.getAnimatedValue())); + mCurrentProgress = (int) (360 * (i / 100f)); + invalidate(); + } + }); + valueAnimator.start(); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + //倒计时结束回调 + if (mListener != null) { + mListener.countDownFinished(); + } + setClickable(true); + } + + }); + } + public void setAddCountDownListener(OnCountDownFinishListener mListener) { + this.mListener = mListener; + } + public interface OnCountDownFinishListener { + void countDownFinished(); + } + + +} diff --git a/app/src/main/java/com/example/baseframe/weight/IProgressBar.java b/app/src/main/java/com/example/baseframe/weight/IProgressBar.java new file mode 100644 index 0000000..3a53d98 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/IProgressBar.java @@ -0,0 +1,351 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import androidx.core.view.ViewCompat; + +import com.example.baseframe.R; +import com.example.baseframe.utils.DensityUtil; + + +/** + * 一个进度条控件,通过颜色变化显示进度,支持环形和矩形两种形式,主要特性如下: + *
      + *
    1. 支持在进度条中以文字形式显示进度,支持修改文字的颜色和大小。
    2. + *
    3. 可以通过 xml 属性修改进度背景色,当前进度颜色,进度条尺寸。
    4. + *
    5. 支持限制进度的最大值。
    6. + *
    + * + * @author cginechen + * @date 2015-07-29 + */ +public class IProgressBar extends View { + + public final static int TYPE_RECT = 0; + public final static int TYPE_CIRCLE = 1; + public final static int TYPE_ROUND_RECT = 2; + public final static int TOTAL_DURATION = 1000; + public final static int DEFAULT_PROGRESS_COLOR = Color.BLUE; + public final static int DEFAULT_BACKGROUND_COLOR = Color.GRAY; + public final static int DEFAULT_TEXT_SIZE = 20; + public final static int DEFAULT_TEXT_COLOR = Color.BLACK; + private final static int PENDING_VALUE_NOT_SET = -1; + /*circle_progress member*/ + public static int DEFAULT_STROKE_WIDTH = DensityUtil.dip2px(40); + IProgressBarTextGenerator mIProgressBarTextGenerator; + /*rect_progress member*/ + RectF mBgRect; + RectF mProgressRect; + /*common member*/ + private int mWidth; + private int mHeight; + private int mType; + private int mProgressColor; + private int mBackgroundColor; + private int mMaxValue; + private int mValue; + private int mPendingValue; + private long mAnimationStartTime; + private int mAnimationDistance; + private int mAnimationDuration; + private int mTextSize; + private int mTextColor; + private boolean mRoundCap; + private Paint mBackgroundPaint = new Paint(); + private Paint mPaint = new Paint(); + private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private RectF mArcOval = new RectF(); + private String mText = ""; + private int mStrokeWidth; + private int mCircleRadius; + private Point mCenterPoint; + + + public IProgressBar(Context context) { + super(context); + setup(context, null); + } + + public IProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + setup(context, attrs); + } + + public IProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(context, attrs); + } + + public void setup(Context context, AttributeSet attrs) { + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.IProgressBar); + mType = array.getInt(R.styleable.IProgressBar_qmui_type, TYPE_RECT); + mProgressColor = array.getColor(R.styleable.IProgressBar_qmui_progress_color, DEFAULT_PROGRESS_COLOR); + mBackgroundColor = array.getColor(R.styleable.IProgressBar_qmui_background_color, DEFAULT_BACKGROUND_COLOR); + + mMaxValue = array.getInt(R.styleable.IProgressBar_qmui_max_value, 100); + mValue = array.getInt(R.styleable.IProgressBar_qmui_value, 0); + + mRoundCap = array.getBoolean(R.styleable.IProgressBar_qmui_stroke_round_cap, false); + + mTextSize = DEFAULT_TEXT_SIZE; + if (array.hasValue(R.styleable.IProgressBar_android_textSize)) { + mTextSize = array.getDimensionPixelSize(R.styleable.IProgressBar_android_textSize, DEFAULT_TEXT_SIZE); + } + mTextColor = DEFAULT_TEXT_COLOR; + if (array.hasValue(R.styleable.IProgressBar_android_textColor)) { + mTextColor = array.getColor(R.styleable.IProgressBar_android_textColor, DEFAULT_TEXT_COLOR); + } + + if (mType == TYPE_CIRCLE) { + mStrokeWidth = array.getDimensionPixelSize(R.styleable.IProgressBar_qmui_stroke_width, DEFAULT_STROKE_WIDTH); + } + array.recycle(); + configPaint(mTextColor, mTextSize, mRoundCap); + + setProgress(mValue); + } + + private void configShape() { + if (mType == TYPE_RECT || mType == TYPE_ROUND_RECT) { + mBgRect = new RectF(getPaddingLeft(), getPaddingTop(), mWidth + getPaddingLeft(), mHeight + getPaddingTop()); + mProgressRect = new RectF(); + } else { + mCircleRadius = (Math.min(mWidth, mHeight) - mStrokeWidth) / 2; + mCenterPoint = new Point(mWidth / 2, mHeight / 2); + } + } + + private void configPaint(int textColor, int textSize, boolean isRoundCap) { + mPaint.setColor(mProgressColor); + mBackgroundPaint.setColor(mBackgroundColor); + if (mType == TYPE_RECT || mType == TYPE_ROUND_RECT) { + mPaint.setStyle(Paint.Style.FILL); + mBackgroundPaint.setStyle(Paint.Style.FILL); + } else { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mStrokeWidth); + mPaint.setAntiAlias(true); + if (isRoundCap) { + mPaint.setStrokeCap(Paint.Cap.ROUND); + } + mBackgroundPaint.setStyle(Paint.Style.STROKE); + mBackgroundPaint.setStrokeWidth(mStrokeWidth); + mBackgroundPaint.setAntiAlias(true); + } + mTextPaint.setColor(textColor); + mTextPaint.setTextSize(textSize); + mTextPaint.setTextAlign(Paint.Align.CENTER); + } + + public void setType(int type) { + mType = type; + configPaint(mTextColor, mTextSize, mRoundCap); + invalidate(); + } + + public void setBarColor(int backgroundColor, int progressColor) { + mBackgroundColor = backgroundColor; + mProgressColor = progressColor; + mBackgroundPaint.setColor(mBackgroundColor); + mPaint.setColor(mProgressColor); + invalidate(); + } + + /** + * 设置进度文案的文字大小 + * + * @see #setTextColor(int) + * @see #setIProgressBarTextGenerator(IProgressBarTextGenerator) + */ + public void setTextSize(int textSize) { + mTextPaint.setTextSize(textSize); + invalidate(); + } + + /** + * 设置进度文案的文字颜色 + * + * @see #setTextSize(int) + * @see #setIProgressBarTextGenerator(IProgressBarTextGenerator) + */ + public void setTextColor(int textColor) { + mTextPaint.setColor(textColor); + invalidate(); + } + + /** + * 设置环形进度条的两端是否有圆形的线帽,类型为{@link #TYPE_CIRCLE}时生效 + */ + public void setStrokeRoundCap(boolean isRoundCap) { + mPaint.setStrokeCap(isRoundCap ? Paint.Cap.ROUND : Paint.Cap.BUTT); + invalidate(); + } + + /** + * 通过 {@link IProgressBarTextGenerator} 设置进度文案 + */ + public void setIProgressBarTextGenerator(IProgressBarTextGenerator IProgressBarTextGenerator) { + mIProgressBarTextGenerator = IProgressBarTextGenerator; + } + + public IProgressBarTextGenerator getIProgressBarTextGenerator() { + return mIProgressBarTextGenerator; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mPendingValue != PENDING_VALUE_NOT_SET) { + long elapsed = System.currentTimeMillis() - mAnimationStartTime; + if (elapsed >= mAnimationDuration) { + mValue = mPendingValue; + mPendingValue = PENDING_VALUE_NOT_SET; + } else { + mValue = (int) (mPendingValue - (1f - ((float) elapsed / mAnimationDuration)) * mAnimationDistance); + ViewCompat.postInvalidateOnAnimation(this); + } + } + + if (mIProgressBarTextGenerator != null) { + mText = mIProgressBarTextGenerator.generateText(this, mValue, mMaxValue); + } + if(((mType == TYPE_RECT || mType == TYPE_ROUND_RECT) && mBgRect == null) || + (mType == TYPE_CIRCLE && mCenterPoint == null)){ + // npe protect, sometimes measure may not be called by parent. + configShape(); + } + if (mType == TYPE_RECT) { + drawRect(canvas); + } else if (mType == TYPE_ROUND_RECT) { + drawRoundRect(canvas); + } else { + drawCircle(canvas); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + mHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + configShape(); + setMeasuredDimension(mWidth, mHeight); + } + + private void drawRect(Canvas canvas) { + canvas.drawRect(mBgRect, mBackgroundPaint); + mProgressRect.set(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + parseValueToWidth(), getPaddingTop() + mHeight); + canvas.drawRect(mProgressRect, mPaint); + if (mText != null && mText.length() > 0) { + Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); + float baseline = mBgRect.top + (mBgRect.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; + canvas.drawText(mText, mBgRect.centerX(), baseline, mTextPaint); + } + } + + private void drawRoundRect(Canvas canvas) { + float round = mHeight / 2f; + canvas.drawRoundRect(mBgRect, round, round, mBackgroundPaint); + mProgressRect.set(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + parseValueToWidth(), getPaddingTop() + mHeight); + canvas.drawRoundRect(mProgressRect, round, round, mPaint); + if (mText != null && mText.length() > 0) { + Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); + float baseline = mBgRect.top + (mBgRect.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; + canvas.drawText(mText, mBgRect.centerX(), baseline, mTextPaint); + } + } + + private void drawCircle(Canvas canvas) { + canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCircleRadius, mBackgroundPaint); + mArcOval.left = mCenterPoint.x - mCircleRadius; + mArcOval.right = mCenterPoint.x + mCircleRadius; + mArcOval.top = mCenterPoint.y - mCircleRadius; + mArcOval.bottom = mCenterPoint.y + mCircleRadius; + if (mValue > 0) { + canvas.drawArc(mArcOval, 270, 360f * mValue / mMaxValue, false, mPaint); + } + if (mText != null && mText.length() > 0) { + Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); + float baseline = mArcOval.top + (mArcOval.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; + canvas.drawText(mText, mCenterPoint.x, baseline, mTextPaint); + } + } + + private int parseValueToWidth() { + return mWidth * mValue / mMaxValue; + } + + public int getProgress() { + return mValue; + } + + public void setProgress(int progress) { + setProgress(progress, true); + } + + public void setProgress(int progress, boolean animated) { + if (progress > mMaxValue || progress < 0) { + return; + } + + if ((mPendingValue == PENDING_VALUE_NOT_SET && mValue == progress) || + (mPendingValue != PENDING_VALUE_NOT_SET && mPendingValue == progress)) { + return; + } + + if (!animated) { + mPendingValue = PENDING_VALUE_NOT_SET; + mValue = progress; + invalidate(); + } else { + mAnimationDuration = Math.abs((int) (TOTAL_DURATION * (mValue - progress) / (float) mMaxValue)); + mAnimationStartTime = System.currentTimeMillis(); + mAnimationDistance = progress - mValue; + mPendingValue = progress; + invalidate(); + } + } + + public int getMaxValue() { + return mMaxValue; + } + + public void setMaxValue(int maxValue) { + mMaxValue = maxValue; + } + + public interface IProgressBarTextGenerator { + /** + * 设置进度文案, {@link IProgressBar} 会在进度更新时调用该方法获取要显示的文案 + * + * @param value 当前进度值 + * @param maxValue 最大进度值 + * @return 进度文案 + */ + String generateText(IProgressBar progressBar, int value, int maxValue); + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/JumpingBeans.java b/app/src/main/java/com/example/baseframe/weight/JumpingBeans.java new file mode 100644 index 0000000..55edc81 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/JumpingBeans.java @@ -0,0 +1,381 @@ +/* + * Copyright 2014 Frakbot (Sebastiano Poggi and Francesco Pontillo) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight; + +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/** + * Provides "jumping beans" functionality for a TextView. + *

    + * Remember to call the {@link #stopJumping()} method once you're done + * using the JumpingBeans (that is, when you detach the TextView from + * the view tree, you hide it, or the parent Activity/Fragment goes in + * the paused status). This will allow to release the animations and + * free up memory and CPU that would be otherwise wasted. + *

    + * Please note that you: + *

      + *
    • Must not try to change a jumping beans text in a textview before calling + * {@link #stopJumping()} as to avoid unnecessary invalidation calls; + * the JumpingBeans class cannot know when this happens and will keep + * animating the textview (well, try to, anyway), wasting resources
    • + *
    • Must not try to use a jumping beans text in another view; it will not + * animate. Just create another jumping beans animation for each new + * view
    • + *
    • Must not use more than one JumpingBeans instance on a single TextView, as + * the first cleanup operation called on any of these JumpingBeans will also cleanup + * all other JumpingBeans' stuff. This is most likely not what you want to happen in + * some cases.
    • + *
    • Should not use JumpingBeans on large chunks of text. Ideally this should + * be done on small views with just a few words. We've strived to make it as inexpensive + * as possible to use JumpingBeans but invalidating and possibly relayouting a large + * TextView can be pretty expensive.
    • + *
    + */ +public final class JumpingBeans { + + /** + * The default fraction of the whole animation time spent actually animating. + * The rest of the range will be spent in "resting" state. + * This the "duty cycle" of the jumping animation. + */ + public static final float DEFAULT_ANIMATION_DUTY_CYCLE = 0.65f; + + /** + * The default duration of a whole jumping animation loop, in milliseconds. + */ + public static final int DEFAULT_LOOP_DURATION = 1300; // ms + + public static final String ELLIPSIS_GLYPH = "…"; + public static final String THREE_DOTS_ELLIPSIS = "..."; + public static final int THREE_DOTS_ELLIPSIS_LENGTH = 3; + + private final JumpingBeansSpan[] jumpingBeans; + private final WeakReference textView; + + private JumpingBeans(JumpingBeansSpan[] beans, TextView textView) { + this.jumpingBeans = beans; + this.textView = new WeakReference<>(textView); + } + + /** + * Create an instance of the {@link net.frakbot.jumpingbeans.JumpingBeans.Builder} + * applied to the provided {@code TextView}. + * + * @param textView The TextView to apply the JumpingBeans to + * @return the {@link net.frakbot.jumpingbeans.JumpingBeans.Builder} + */ + public static Builder with( TextView textView) { + return new Builder(textView); + } + + /** + * Stops the jumping animation and frees up the animations. + */ + public void stopJumping() { + for (JumpingBeansSpan bean : jumpingBeans) { + if (bean != null) { + bean.teardown(); + } + } + + cleanupSpansFrom(textView.get()); + } + + private static void cleanupSpansFrom(TextView tv) { + if (tv != null) { + CharSequence text = tv.getText(); + if (text instanceof Spanned) { + CharSequence cleanText = removeJumpingBeansSpansFrom((Spanned) text); + tv.setText(cleanText); + } + } + } + + private static CharSequence removeJumpingBeansSpansFrom(Spanned text) { + SpannableStringBuilder sbb = new SpannableStringBuilder(text.toString()); + Object[] spans = text.getSpans(0, text.length(), Object.class); + for (Object span : spans) { + if (!(span instanceof JumpingBeansSpan)) { + sbb.setSpan(span, text.getSpanStart(span), + text.getSpanEnd(span), text.getSpanFlags(span)); + } + } + return sbb; + } + + private static CharSequence appendThreeDotsEllipsisTo(TextView textView) { + CharSequence text = getTextSafe(textView); + if (text.length() > 0 && endsWithEllipsisGlyph(text)) { + text = text.subSequence(0, text.length() - 1); + } + + if (!endsWithThreeEllipsisDots(text)) { + text = new SpannableStringBuilder(text).append(THREE_DOTS_ELLIPSIS); // Preserve spans in original text + } + return text; + } + + private static CharSequence getTextSafe(TextView textView) { + return !TextUtils.isEmpty(textView.getText()) ? textView.getText() : ""; + } + + private static boolean endsWithEllipsisGlyph(CharSequence text) { + return TextUtils.equals(text.subSequence(text.length() - 1, text.length()), ELLIPSIS_GLYPH); + } + + private static boolean endsWithThreeEllipsisDots( CharSequence text) { + if (text.length() < THREE_DOTS_ELLIPSIS_LENGTH) { + // TODO we should try to normalize "invalid" ellipsis (e.g., ".." or "....") + return false; + } + return TextUtils.equals(text.subSequence(text.length() - THREE_DOTS_ELLIPSIS_LENGTH, text.length()), THREE_DOTS_ELLIPSIS); + } + + private static CharSequence ensureTextCanJump(int startPos, int endPos, CharSequence text) { + if (text == null) { + throw new NullPointerException("The textView text must not be null"); + } + + if (endPos < startPos) { + throw new IllegalArgumentException("The start position must be smaller than the end position"); + } + + if (startPos < 0) { + throw new IndexOutOfBoundsException("The start position must be non-negative"); + } + + if (endPos > text.length()) { + throw new IndexOutOfBoundsException("The end position must be smaller than the text length"); + } + return text; + } + + /** + * Builder class for {@link net.frakbot.jumpingbeans.JumpingBeans} objects. + *

    + * Provides a way to set the fields of a {@link com.example.yzh.view.JumpingBeans} and generate + * the desired jumping beans effect. With this builder you can easily append + * a Hangouts-style trio of jumping suspension points to any TextView, or + * apply the effect to any other subset of a TextView's text. + *

    + *

    Example: + *

    + *

    +     * JumpingBeans jumpingBeans = JumpingBeans.with(myTextView)
    +     *     .appendJumpingDots()
    +     *     .setLoopDuration(1500)
    +     *     .build();
    +     * 
    + */ + public static class Builder { + + private int startPos, endPos; + private float animRange = DEFAULT_ANIMATION_DUTY_CYCLE; + private int loopDuration = DEFAULT_LOOP_DURATION; + private int waveCharDelay = -1; + private CharSequence text; + private TextView textView; + private boolean wave; + + /*package*/ Builder(TextView textView) { + this.textView = textView; + } + + /** + * Appends three jumping dots to the end of a TextView text. + *

    + * This implies that the animation will by default be a wave. + *

    + * If the TextView has no text, the resulting TextView text will + * consist of the three dots only. + *

    + * The TextView text is cached to the current value at + * this time and set again in the {@link #build()} method, so any + * change to the TextView text done in the meantime will be lost. + * This means that you should do all changes to the TextView text + * before you begin using this builder. + *

    + * Call the {@link #build()} method once you're done to get the + * resulting {@link net.frakbot.jumpingbeans.JumpingBeans}. + * + * @see #setIsWave(boolean) + */ + public Builder appendJumpingDots() { + CharSequence text = appendThreeDotsEllipsisTo(textView); + + this.text = text; + this.wave = true; + this.startPos = text.length() - THREE_DOTS_ELLIPSIS_LENGTH; + this.endPos = text.length(); + + return this; + } + + /** + * Appends three jumping dots to the end of a TextView text. + *

    + * This implies that the animation will by default be a wave. + *

    + * If the TextView has no text, the resulting TextView text will + * consist of the three dots only. + *

    + * The TextView text is cached to the current value at + * this time and set again in the {@link #build()} method, so any + * change to the TextView text done in the meantime will be lost. + * This means that you should do all changes to the TextView text + * before you begin using this builder. + *

    + * Call the {@link #build()} method once you're done to get the + * resulting {@link net.frakbot.jumpingbeans.JumpingBeans}. + * + * @param startPos The position of the first character to animate + * @param endPos The position after the one the animated range ends at + * (just like in {@link String#substring(int)}) + * @see #setIsWave(boolean) + */ + public Builder makeTextJump(int startPos, int endPos) { + CharSequence text = textView.getText(); + ensureTextCanJump(startPos, endPos, text); + + this.text = text; + this.wave = true; + this.startPos = startPos; + this.endPos = endPos; + + return this; + } + + /** + * Sets the fraction of the animation loop time spent actually animating. + * The rest of the time will be spent "resting". + * The default value is + * {@link net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_ANIMATION_DUTY_CYCLE}. + * + * @param animatedRange The fraction of the animation loop time spent + * actually animating the characters + */ + public Builder setAnimatedDutyCycle(float animatedRange) { + if (animatedRange <= 0f || animatedRange > 1f) { + throw new IllegalArgumentException("The animated range must be in the (0, 1] range"); + } + this.animRange = animatedRange; + return this; + } + + /** + * Sets the jumping loop duration. The default value is + * {@link net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_LOOP_DURATION}. + * + * @param loopDuration The jumping animation loop duration, in milliseconds + */ + public Builder setLoopDuration(int loopDuration) { + if (loopDuration < 1) { + throw new IllegalArgumentException("The loop duration must be bigger than zero"); + } + this.loopDuration = loopDuration; + return this; + } + + /** + * Sets the delay for starting the animation of every single dot over the + * start of the previous one, in milliseconds. The default value is + * the loop length divided by three times the number of character animated + * by this instance of JumpingBeans. + *

    + * Only has a meaning when the animation is a wave. + * + * @param waveCharOffset The start delay for the animation of every single + * character over the previous one, in milliseconds + * @see #setIsWave(boolean) + */ + public Builder setWavePerCharDelay(int waveCharOffset) { + if (waveCharOffset < 0) { + throw new IllegalArgumentException("The wave char offset must be non-negative"); + } + this.waveCharDelay = waveCharOffset; + return this; + } + + /** + * Sets a flag that determines if the characters will jump in a wave + * (i.e., with a delay between each other) or all at the same + * time. + * + * @param wave If true, the animation is going to be a wave; if + * false, all characters will jump ay the same time + * @see #setWavePerCharDelay(int) + */ + public Builder setIsWave(boolean wave) { + this.wave = wave; + return this; + } + + /** + * Combine all of the options that have been set and return a new + * {@link net.frakbot.jumpingbeans.JumpingBeans} instance. + *

    + * Remember to call the {@link #stopJumping()} method once you're done + * using the JumpingBeans (that is, when you detach the TextView from + * the view tree, you hide it, or the parent Activity/Fragment goes in + * the paused status). This will allow to release the animations and + * free up memory and CPU that would be otherwise wasted. + */ + public JumpingBeans build() { + SpannableStringBuilder sbb = new SpannableStringBuilder(text); + JumpingBeansSpan[] spans; + if (wave) { + spans = getJumpingBeansSpans(sbb); + } else { + spans = buildSingleSpan(sbb); + } + + textView.setText(sbb); + return new JumpingBeans(spans, textView); + } + + private JumpingBeansSpan[] getJumpingBeansSpans(SpannableStringBuilder sbb) { + JumpingBeansSpan[] spans; + if (waveCharDelay == -1) { + waveCharDelay = loopDuration / (3 * (endPos - startPos)); + } + + spans = new JumpingBeansSpan[endPos - startPos]; + for (int pos = startPos; pos < endPos; pos++) { + JumpingBeansSpan jumpingBean = + new JumpingBeansSpan(textView, loopDuration, pos - startPos, waveCharDelay, animRange); + sbb.setSpan(jumpingBean, pos, pos + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spans[pos - startPos] = jumpingBean; + } + return spans; + } + + private JumpingBeansSpan[] buildSingleSpan(SpannableStringBuilder sbb) { + JumpingBeansSpan[] spans; + spans = new JumpingBeansSpan[]{new JumpingBeansSpan(textView, loopDuration, 0, 0, animRange)}; + sbb.setSpan(spans[0], startPos, endPos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return spans; + } + + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/JumpingBeansSpan.java b/app/src/main/java/com/example/baseframe/weight/JumpingBeansSpan.java new file mode 100644 index 0000000..adcee52 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/JumpingBeansSpan.java @@ -0,0 +1,147 @@ +/* + * Copyright 2014 Frakbot (Sebastiano Poggi and Francesco Pontillo) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; +import android.text.TextPaint; +import android.text.style.SuperscriptSpan; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/*package*/ @TargetApi(Build.VERSION_CODES.HONEYCOMB) +final class JumpingBeansSpan extends SuperscriptSpan implements ValueAnimator.AnimatorUpdateListener { + + private final WeakReference textView; + private final int delay; + private final int loopDuration; + private final float animatedRange; + private int shift; + private ValueAnimator jumpAnimator; + + public JumpingBeansSpan(TextView textView, int loopDuration, int position, int waveCharOffset, + float animatedRange) { + this.textView = new WeakReference<>(textView); + this.delay = waveCharOffset * position; + this.loopDuration = loopDuration; + this.animatedRange = animatedRange; + } + + @Override + public void updateMeasureState( TextPaint tp) { + initIfNecessary(tp.ascent()); + tp.baselineShift = shift; + } + + @Override + public void updateDrawState( TextPaint tp) { + initIfNecessary(tp.ascent()); + tp.baselineShift = shift; + } + + private void initIfNecessary(float ascent) { + if (jumpAnimator != null) { + return; + } + + this.shift = 0; + int maxShift = (int) ascent / 2; + jumpAnimator = ValueAnimator.ofInt(0, maxShift); + jumpAnimator + .setDuration(loopDuration) + .setStartDelay(delay); + jumpAnimator.setInterpolator(new JumpInterpolator(animatedRange)); + jumpAnimator.setRepeatCount(ValueAnimator.INFINITE); + jumpAnimator.setRepeatMode(ValueAnimator.RESTART); + jumpAnimator.addUpdateListener(this); + jumpAnimator.start(); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // No need for synchronization as this always run on main thread anyway + TextView v = textView.get(); + if (v != null) { + updateAnimationFor(animation, v); + } else { + cleanupAndComplainAboutUserBeingAFool(); + } + } + + private void updateAnimationFor(ValueAnimator animation, TextView v) { + if (isAttachedToHierarchy(v)) { + shift = (int) animation.getAnimatedValue(); + v.invalidate(); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static boolean isAttachedToHierarchy(View v) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return v.isAttachedToWindow(); + } + return v.getParent() != null; // Best-effort fallback + } + + private void cleanupAndComplainAboutUserBeingAFool() { + // The textview has been destroyed and teardown() hasn't been called + teardown(); + Log.w("JumpingBeans", "!!! Remember to call JumpingBeans.stopJumping() when appropriate !!!"); + } + + /*package*/ void teardown() { + if (jumpAnimator != null) { + jumpAnimator.cancel(); + jumpAnimator.removeAllListeners(); + } + if (textView.get() != null) { + textView.clear(); + } + } + + /** + * A tweaked {@link android.view.animation.AccelerateDecelerateInterpolator} + * that covers the full range in a fraction of its input range, and holds on + * the final value on the rest of the input range. By default, this fraction + * is 65% of the full range. + * + * @see net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_ANIMATION_DUTY_CYCLE + */ + private static class JumpInterpolator implements TimeInterpolator { + + private final float animRange; + + public JumpInterpolator(float animatedRange) { + animRange = Math.abs(animatedRange); + } + + @Override + public float getInterpolation(float input) { + // We want to map the [0, PI] sine range onto [0, animRange] + double radians = (input / animRange) * Math.PI; + double interpolatedValue = Math.max(0f, Math.sin(radians)); + return (float) interpolatedValue; + } + + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/LineWaveVoiceView.java b/app/src/main/java/com/example/baseframe/weight/LineWaveVoiceView.java new file mode 100644 index 0000000..e62c044 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/LineWaveVoiceView.java @@ -0,0 +1,175 @@ +package com.example.baseframe.weight; + + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import androidx.core.content.ContextCompat; + +import com.example.baseframe.R; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @anthor yzh + * 语音录制的动画效果 + * 基于 https://www.jianshu.com/p/6dd10a5adca8 修改 + */ +public class LineWaveVoiceView extends View { + private static final String DEFAULT_TEXT = " 请录音 "; + private static final int LINE_WIDTH = 9;//默认矩形波纹的宽度 + private Paint paint = new Paint(); + private Runnable task; + private ExecutorService executorService = Executors.newCachedThreadPool(); + private RectF rectRight = new RectF();//右边波纹矩形的数据,10个矩形复用一个rectF + private RectF rectLeft = new RectF();//左边波纹矩形的数据 + private String text = DEFAULT_TEXT; + private int updateSpeed; + private int lineColor; + private int textColor; + private float lineWidth; + private float textSize; + + + public LineWaveVoiceView(Context context) { + super(context); + } + + public LineWaveVoiceView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LineWaveVoiceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(attrs, context); + resetView(mWaveList, DEFAULT_WAVE_HEIGHT); + task = new LineJitterTask(); + } + + private void initView(AttributeSet attrs, Context context) { + //获取布局属性里的值 + TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.LineWaveVoiceView); + lineColor = mTypedArray.getColor(R.styleable.LineWaveVoiceView_voiceLineColor, ContextCompat.getColor(context,R.color.ui_blue)); + lineWidth = mTypedArray.getDimension(R.styleable.LineWaveVoiceView_voiceLineWidth, LINE_WIDTH); + textSize = mTypedArray.getDimension(R.styleable.LineWaveVoiceView_voiceTextSize, 42); + textColor = mTypedArray.getColor(R.styleable.LineWaveVoiceView_voiceTextColor, ContextCompat.getColor(context,R.color.ui_gray)); + updateSpeed = mTypedArray.getColor(R.styleable.LineWaveVoiceView_updateSpeed, UPDATE_INTERVAL_TIME); + mTypedArray.recycle(); + } + + +// .获取该View的实际宽高的一半,然后设置矩形的四边,熟悉Android的view的绘制都知道,view的宽为right - left, +// 高度为bottom - top。所以让right比left多一个lineWidth即可让矩形的宽为lineWidth, +// bottom比top多4lineWidth即可让高读为4lineWidth,并利用实际宽高的一半,把矩形绘制在view的中央。 + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if(mWaveList==null){ + return; + } + //获取实际宽高的一半 + int widthCentre = getWidth() / 2; + int heightCentre = getHeight() / 2; + paint.setStrokeWidth(0); + paint.setColor(textColor); + paint.setTextSize(textSize); + float textWidth = paint.measureText(text); + canvas.drawText(text, widthCentre - textWidth / 2, heightCentre - (paint.ascent() + paint.descent()) / 2, paint); + + //设置颜色 + paint.setColor(lineColor); + //填充内部 + paint.setStyle(Paint.Style.FILL); + //设置抗锯齿 + paint.setAntiAlias(true); + for (int i = 0; i < 9; i++) { + rectRight.left = widthCentre + textWidth / 2 + (1 + 2 * i) * lineWidth; + rectRight.top = heightCentre - lineWidth * mWaveList.get(i) / 2; + rectRight.right = widthCentre + textWidth / 2 + (2 + 2 * i) * lineWidth; + rectRight.bottom = heightCentre + lineWidth * mWaveList.get(i) / 2; + + //左边矩形 + rectLeft.left = widthCentre - textWidth / 2 - (2 + 2 * i) * lineWidth; + rectLeft.top = heightCentre - mWaveList.get(i) * lineWidth / 2; + rectLeft.right = widthCentre - textWidth / 2 - (1 + 2 * i) * lineWidth; + rectLeft.bottom = heightCentre + mWaveList.get(i) * lineWidth / 2; + + canvas.drawRoundRect(rectRight, 6, 6, paint); + canvas.drawRoundRect(rectLeft, 6, 6, paint); + } + } + + private static final int MIN_WAVE_HEIGHT = 2;//矩形线最小高 + // private static final int MAX_WAVE_HEIGHT = 10;//矩形线最大高 + private static final int MAX_WAVE_HEIGHT = 3;//矩形线最大高 + private static final int[] DEFAULT_WAVE_HEIGHT = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + private static final int UPDATE_INTERVAL_TIME = 100;//100ms更新一次 + private LinkedList mWaveList = new LinkedList<>(); + private float maxDb; + + private void resetView(List list, int[] array) { + list.clear(); + for (int anArray : array) { + list.add(anArray); + } + } + + private synchronized void refreshElement() { + Random random = new Random(); + maxDb = random.nextInt(5) + 1; + int waveH = MIN_WAVE_HEIGHT + Math.round(maxDb * (MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT)); + // ALog.i("waveH===="+waveH); + mWaveList.add(0, waveH); + mWaveList.removeLast(); + } + + public boolean isStart = false; + + private class LineJitterTask implements Runnable { + @Override + public void run() { + while (isStart) { + refreshElement(); + try { + Thread.sleep(updateSpeed); + } catch (Exception e) { + e.printStackTrace(); + } + postInvalidate(); + } + } + } + + public synchronized void startRecord() { + isStart = true; + executorService.execute(task); + } + + public synchronized void stopRecord() { + isStart = false; + mWaveList.clear(); + resetView(mWaveList, DEFAULT_WAVE_HEIGHT); + postInvalidate(); + } + + + public synchronized void setText(String text) { + this.text = text; + postInvalidate(); + } + + public void setUpdateSpeed(int updateSpeed) { + this.updateSpeed = updateSpeed; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/weight/LoadDialog.java b/app/src/main/java/com/example/baseframe/weight/LoadDialog.java new file mode 100644 index 0000000..b1f0f67 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/LoadDialog.java @@ -0,0 +1,97 @@ +package com.example.baseframe.weight; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import com.blankj.ALog; +import com.example.baseframe.R; + + +/** + * @Author: yzh + * @CreateDate: 2019/11/1 11:39 + */ +public class LoadDialog extends Dialog { + + private TextView tvMessage; + private String msg = ""; + private Boolean cancelAble = true; + private JumpingBeans jump; + private int cancelTime;//避免异常情况下Dialog + public LoadDialog(Context context, String msg, Boolean cancelAble) { + super(context, R.style.comm_load_dialog); + this.msg = msg; + this.cancelAble = cancelAble; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.comm_load_dialogs); + tvMessage = this.findViewById(R.id.tvLoadDialog_Message); + + if (TextUtils.isEmpty(msg)) { + tvMessage.setVisibility(View.GONE); + } else { + tvMessage.setVisibility(View.VISIBLE); + tvMessage.setText(msg); + if (msg.length() > 3) { + //if(msg.equals(CommentUtils.getString("R.string.content_loading"))){ + jump = JumpingBeans.with(tvMessage) + .makeTextJump(msg.length() - 3, msg.length()) + .setIsWave(true) + .setLoopDuration(1500) + .build(); + //} + } + } + + } + + @Override + public void dismiss() { + if(jump!=null){ + jump.stopJumping(); + } + cancelTime=0; + super.dismiss(); + } + @Override + public boolean onTouchEvent(MotionEvent event) { + + ALog.v("cancelTime "+cancelTime); + switch (event.getAction()){ + case MotionEvent.ACTION_UP: + cancelTime++; + if (cancelTime>9){ + return super.onTouchEvent(event); + } + break; + } + + + if (!cancelAble){ + return false; + } + + return super.onTouchEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!cancelAble) return false; + } + return super.onKeyDown(keyCode, event); + } + + +} diff --git a/app/src/main/java/com/example/baseframe/weight/NoScrollViewPager.java b/app/src/main/java/com/example/baseframe/weight/NoScrollViewPager.java new file mode 100644 index 0000000..3077cda --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/NoScrollViewPager.java @@ -0,0 +1,58 @@ +package com.example.baseframe.weight; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + +/** + * 禁止滑动的viewpager + * Created by yzh on 2016/2/25. + */ +public class NoScrollViewPager extends ViewPager { + private boolean noScroll = true; + + public NoScrollViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + // TODO Auto-generated constructor stub + } + + public NoScrollViewPager(Context context) { + super(context); + } + + public void setNoScroll(boolean noScroll) { + this.noScroll = noScroll; + } + + + @Override + public boolean onTouchEvent(MotionEvent arg0) { + /* return false;//super.onTouchEvent(arg0); */ + //触摸没有反应就可以了 + if (noScroll) + return false; + else + return super.onTouchEvent(arg0); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent arg0) { + if (noScroll) + return false; + else + return super.onInterceptTouchEvent(arg0); + } + + @Override + public void setCurrentItem(int item, boolean smoothScroll) { + super.setCurrentItem(item, smoothScroll); + } + + @Override + public void setCurrentItem(int item) { + super.setCurrentItem(item); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/weight/RoundImageView.java b/app/src/main/java/com/example/baseframe/weight/RoundImageView.java new file mode 100644 index 0000000..fb6c3ba --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/RoundImageView.java @@ -0,0 +1,72 @@ +package com.example.baseframe.weight; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; + +import androidx.appcompat.widget.AppCompatImageView; + +import com.example.baseframe.utils.DensityUtil; + +/** + + Created by zz on 2018/1/3. + 圆角ImageView + */ +public class RoundImageView extends AppCompatImageView { + + private Context mContext; + float width,height; + private int mRadiusPx; + + public RoundImageView(Context context) { + this(context, null); + } + + public RoundImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RoundImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mContext = context; + mRadiusPx = DensityUtil.dip2px(5); + if (Build.VERSION.SDK_INT < 18) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + width = getWidth(); + height = getHeight(); + } + + @Override + protected void onDraw(Canvas canvas) { + + //这里的目的是将画布设置成一个顶部边缘是圆角的矩形 + if (width > mRadiusPx && height > mRadiusPx) { + Path path = new Path(); + + path.moveTo(mRadiusPx, 0); + path.lineTo(width - mRadiusPx, 0); + path.quadTo(width, 0, width, mRadiusPx); + path.lineTo(width, height - mRadiusPx); + path.quadTo(width, height, width - mRadiusPx, height); + path.lineTo(mRadiusPx, height); + path.quadTo(0, height, 0, height - mRadiusPx); + path.lineTo(0, mRadiusPx); + path.quadTo(0, 0, mRadiusPx, 0); + + + canvas.clipPath(path); + } + + super.onDraw(canvas); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/weight/RunningTextView.java b/app/src/main/java/com/example/baseframe/weight/RunningTextView.java new file mode 100644 index 0000000..1ef681a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/RunningTextView.java @@ -0,0 +1,108 @@ +package com.example.baseframe.weight; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatTextView; + +import java.text.DecimalFormat; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Created by yzh-t105 on 2017/8/29. + */ +public class RunningTextView extends AppCompatTextView { + + public double content;// 最后显示的数字 + private int frames = 40;// 总共跳跃的帧数,默认40跳 + private double nowNumber = 0.00;// 显示的时间 + private ExecutorService thread_pool; + private Handler handler; + private DecimalFormat formater;// 格式化时间,保留两位小数 + + public RunningTextView (Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public RunningTextView (Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public RunningTextView (Context context) { + super(context); + init(); + } + + + // 设置帧数 + public void setFrames(int frames) { + this.frames = frames; + } + + // mRunningtextview.setFormat("¥00.00"); + /** + * 设置数字格式,具体查DecimalFormat类的api + * @param pattern + */ + public void setFormat(String pattern) { + formater = new DecimalFormat(pattern); + } + + // 初始化 + + private void init() { + + thread_pool = Executors.newFixedThreadPool(2);// 2个线程的线程池 + formater = new DecimalFormat("0.00");// 最多两位小数,而且不够两位整数用0占位。可以通过setFormat再次设置 + handler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + RunningTextView.this.setText(formater.format(nowNumber) + .toString());// 更新显示的数字 + nowNumber += Double.parseDouble(msg.obj.toString());// 跳跃arg1那么多的数字间隔 +// Log.v("nowNumber增加之后的值", nowNumber + ""); + // + if (nowNumber < content) { + Message msg2 = handler.obtainMessage(); + msg2.obj = msg.obj; + handler.sendMessage(msg2);// 继续发送通知改变UI + } else { + RunningTextView.this.setText(formater.format(content) + .toString());// 最后显示的数字,动画停止 + } + } + }; + } + + /** + * 播放数字动画的方法 + * + * @param moneyNumber + */ + public void playNumber(double moneyNumber) { + if (moneyNumber == 0) { + RunningTextView.this.setText("0.00"); + return; + } + content = moneyNumber;// 设置最后要显示的数字 + nowNumber = 0.00;// 默认都是从0开始动画 + thread_pool.execute(new Runnable() { + @Override + public void run() { + Message msg = handler.obtainMessage(); + double temp = content / frames; + msg.obj = temp < 0.01 ? 0.01 : temp;// 如果每帧的间隔比1小,就设置为1 +// Log.v("每帧跳跃的数量:", "" + msg.obj.toString()); + handler.sendMessage(msg);// 发送通知改变UI + } + }); + } + +} + diff --git a/app/src/main/java/com/example/baseframe/weight/ShadowLayout.java b/app/src/main/java/com/example/baseframe/weight/ShadowLayout.java new file mode 100644 index 0000000..4ede646 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/ShadowLayout.java @@ -0,0 +1,340 @@ +package com.example.baseframe.weight; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.example.baseframe.R; + + +/** + * 定制化的阴影Layout,阴影效果比cadrview更好 + * + * app:hl_shadowColor="#2a000000" 阴影的颜色可以随便改变,透明度的改变可以改变阴影的清晰程度 + * 特别注意:系统方法,颜色值必须带透明度。如果你不想加透明度,则默认透明度为16% + * + */ +public class ShadowLayout extends FrameLayout { + + private int mBackGroundColor; + private int mShadowColor; + private float mShadowLimit; + private float mCornerRadius; + private float mDx; + private float mDy; + private boolean leftShow; + private boolean rightShow; + private boolean topShow; + private boolean bottomShow; + private Paint shadowPaint; + private Paint paint; + + private int leftPading; + private int topPading; + private int rightPading; + private int bottomPading; + //阴影布局子空间区域 + private RectF rectf = new RectF(); + + + public ShadowLayout(Context context) { + this(context, null); + } + + public ShadowLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + + public ShadowLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + public void setMDx(float mDx) { + if (Math.abs(mDx) > mShadowLimit) { + if (mDx > 0) { + this.mDx = mShadowLimit; + } else { + this.mDx = -mShadowLimit; + } + } else { + this.mDx = mDx; + } + setPading(); + } + + + public void setMDy(float mDy) { + if (Math.abs(mDy) > mShadowLimit) { + if (mDy > 0) { + this.mDy = mShadowLimit; + } else { + this.mDy = -mShadowLimit; + } + } else { + this.mDy = mDy; + } + setPading(); + } + + + public float getmCornerRadius() { + return mCornerRadius; + } + + + public void setmCornerRadius(int mCornerRadius) { + this.mCornerRadius = mCornerRadius; + setBackgroundCompat(getWidth(), getHeight()); + } + + + public float getmShadowLimit() { + return mShadowLimit; + } + + public void setmShadowLimit(int mShadowLimit) { + this.mShadowLimit = mShadowLimit; + setPading(); + } + + + public void setmShadowColor(int mShadowColor) { + this.mShadowColor = mShadowColor; + setBackgroundCompat(getWidth(), getHeight()); + } + + public void setLeftShow(boolean leftShow) { + this.leftShow = leftShow; + setPading(); + } + + public void setRightShow(boolean rightShow) { + this.rightShow = rightShow; + setPading(); + } + + public void setTopShow(boolean topShow) { + this.topShow = topShow; + setPading(); + } + + public void setBottomShow(boolean bottomShow) { + this.bottomShow = bottomShow; + setPading(); + } + + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w > 0 && h > 0) { + setBackgroundCompat(w, h); + } + } + + + private void initView(Context context, AttributeSet attrs) { + initAttributes(attrs); + shadowPaint = new Paint(); + shadowPaint.setAntiAlias(true); + shadowPaint.setStyle(Paint.Style.FILL); + + + //矩形画笔 + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(mBackGroundColor); + + setPading(); + } + + + public void setPading() { + int xPadding = (int) (mShadowLimit + Math.abs(mDx)); + int yPadding = (int) (mShadowLimit + Math.abs(mDy)); + + if (leftShow) { + leftPading = xPadding; + } else { + leftPading = 0; + } + + if (topShow) { + topPading = yPadding; + } else { + topPading = 0; + } + + + if (rightShow) { + rightPading = xPadding; + } else { + rightPading = 0; + } + + if (bottomShow) { + bottomPading = yPadding; + } else { + bottomPading = 0; + } + + setPadding(leftPading, topPading, rightPading, bottomPading); + } + + + @SuppressWarnings("deprecation") + private void setBackgroundCompat(int w, int h) { + Bitmap bitmap = createShadowBitmap(w, h, mCornerRadius, mShadowLimit, mDx, mDy, mShadowColor, Color.TRANSPARENT); + BitmapDrawable drawable = new BitmapDrawable(bitmap); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) { + setBackgroundDrawable(drawable); + } else { + setBackground(drawable); + } + } + + + private void initAttributes(AttributeSet attrs) { + TypedArray attr = getContext().obtainStyledAttributes(attrs, R.styleable.ShadowLayout); + if (attr == null) { + return; + } + + try { + //默认是显示 + leftShow = attr.getBoolean(R.styleable.ShadowLayout_hl_leftShow, true); + rightShow = attr.getBoolean(R.styleable.ShadowLayout_hl_rightShow, true); + bottomShow = attr.getBoolean(R.styleable.ShadowLayout_hl_bottomShow, true); + topShow = attr.getBoolean(R.styleable.ShadowLayout_hl_topShow, true); + mCornerRadius = attr.getDimension(R.styleable.ShadowLayout_hl_cornerRadius, getResources().getDimension(R.dimen.dp_0)); + //默认扩散区域宽度 + mShadowLimit = attr.getDimension(R.styleable.ShadowLayout_hl_shadowLimit, getResources().getDimension(R.dimen.dp_5)); + + //x轴偏移量 + mDx = attr.getDimension(R.styleable.ShadowLayout_hl_dx, 0); + //y轴偏移量 + mDy = attr.getDimension(R.styleable.ShadowLayout_hl_dy, 0); + mShadowColor = attr.getColor(R.styleable.ShadowLayout_hl_shadowColor, getResources().getColor(R.color.default_shadow_color)); + //判断传入的颜色值是否有透明度 + isAddAlpha(mShadowColor); + mBackGroundColor = attr.getColor(R.styleable.ShadowLayout_hl_shadowBackColor, getResources().getColor(R.color.default_shadowback_color)); + } finally { + attr.recycle(); + } + } + + + private Bitmap createShadowBitmap(int shadowWidth, int shadowHeight, float cornerRadius, float shadowRadius, + float dx, float dy, int shadowColor, int fillColor) { + //优化阴影bitmap大小,将尺寸缩小至原来的1/4。 + dx = dx / 4; + dy = dy / 4; + shadowWidth = shadowWidth / 4; + shadowHeight = shadowHeight / 4; + cornerRadius = cornerRadius / 4; + shadowRadius = shadowRadius / 4; + + Bitmap output = Bitmap.createBitmap(shadowWidth, shadowHeight, Bitmap.Config.ARGB_4444); + Canvas canvas = new Canvas(output); + + RectF shadowRect = new RectF( + shadowRadius, + shadowRadius, + shadowWidth - shadowRadius, + shadowHeight - shadowRadius); + + if (dy > 0) { + shadowRect.top += dy; + shadowRect.bottom -= dy; + } else if (dy < 0) { + shadowRect.top += Math.abs(dy); + shadowRect.bottom -= Math.abs(dy); + } + + if (dx > 0) { + shadowRect.left += dx; + shadowRect.right -= dx; + } else if (dx < 0) { + + shadowRect.left += Math.abs(dx); + shadowRect.right -= Math.abs(dx); + } + + + + shadowPaint.setColor(fillColor); + if (!isInEditMode()) { + shadowPaint.setShadowLayer(shadowRadius, dx, dy, shadowColor); + } + + canvas.drawRoundRect(shadowRect, cornerRadius, cornerRadius, shadowPaint); + return output; + } + + + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + rectf.left = leftPading; + rectf.top = topPading; + rectf.right = getWidth() - rightPading; + rectf.bottom = getHeight() - bottomPading; + int trueHeight = (int) (rectf.bottom - rectf.top); + if (mCornerRadius > trueHeight / 2) { + //画圆角矩形 + canvas.drawRoundRect(rectf, trueHeight / 2, trueHeight / 2, paint); +// canvas.drawRoundRect(rectf, trueHeight / 2, trueHeight / 2, paintStroke); + } else { + canvas.drawRoundRect(rectf, mCornerRadius, mCornerRadius, paint); +// canvas.drawRoundRect(rectf, mCornerRadius, mCornerRadius, paintStroke); + } + } + + + public void isAddAlpha(int color) { + //获取单签颜色值的透明度,如果没有设置透明度,默认加上#2a + if (Color.alpha(color) == 255) { + String red = Integer.toHexString(Color.red(color)); + String green = Integer.toHexString(Color.green(color)); + String blue = Integer.toHexString(Color.blue(color)); + + if (red.length() == 1) { + red = "0" + red; + } + + if (green.length() == 1) { + green = "0" + green; + } + + if (blue.length() == 1) { + blue = "0" + blue; + } + String endColor = "#2a" + red + green + blue; + mShadowColor = convertToColorInt(endColor); + } + } + + + public static int convertToColorInt(String argb) + throws IllegalArgumentException { + + if (!argb.startsWith("#")) { + argb = "#" + argb; + } + + return Color.parseColor(argb); + } + +} + diff --git a/app/src/main/java/com/example/baseframe/weight/SmartScrollView.java b/app/src/main/java/com/example/baseframe/weight/SmartScrollView.java new file mode 100644 index 0000000..f4b0d43 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/SmartScrollView.java @@ -0,0 +1,91 @@ +package com.example.baseframe.weight; + + + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ScrollView; + + +/** + * @author yzh + * 监听ScrollView滚动到顶部或者底部 + */ +public class SmartScrollView extends ScrollView { + + private boolean isScrolledToTop = true;// 初始化的时候设置一下值 + private boolean isScrolledToBottom = false; + public SmartScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private ISmartScrollChangedListener mSmartScrollChangedListener; + + /** 定义监听接口 */ + public interface ISmartScrollChangedListener { + void onScrolledToBottom(); + void onScrolledToTop(); + } + + public void setScrollChangedListener(ISmartScrollChangedListener smartScrollChangedListener) { + mSmartScrollChangedListener = smartScrollChangedListener; + } + + + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + + if (getScrollY() == 0) { // 小心踩坑1: 这里不能是getScrollY() <= 0 + isScrolledToTop = true; + isScrolledToBottom = false; + Log.d("SmartScrollView","到顶部了"); + } else if (getScrollY() + getHeight() - getPaddingTop()-getPaddingBottom() == getChildAt(0).getHeight()) { + // 小心踩坑2: 这里不能是 >= + // 小心踩坑3(可能忽视的细节2):这里最容易忽视的就是ScrollView上下的padding  + isScrolledToBottom = true; + isScrolledToTop = false; + Log.d("SmartScrollView","到底部了"); + } else { + isScrolledToTop = false; + isScrolledToBottom = false; + } + notifyScrollChangedListeners(); + + // 有时候写代码习惯了,为了兼容一些边界奇葩情况,上面的代码就会写成<=,>=的情况,结果就出bug了 + // 我写的时候写成这样:getScrollY() + getHeight() >= getChildAt(0).getHeight() + // 结果发现快滑动到底部但是还没到时,会发现上面的条件成立了,导致判断错误 + // 原因:getScrollY()值不是绝对靠谱的,它会超过边界值,但是它自己会恢复正确,导致上面的计算条件不成立 + // 仔细想想也感觉想得通,系统的ScrollView在处理滚动的时候动态计算那个scrollY的时候也会出现超过边界再修正的情况 + } + + private void notifyScrollChangedListeners() { + if (isScrolledToTop) { + if (mSmartScrollChangedListener != null) { + mSmartScrollChangedListener.onScrolledToTop(); + } + } else if (isScrolledToBottom) { + if (mSmartScrollChangedListener != null) { + mSmartScrollChangedListener.onScrolledToBottom(); + } + } + } + + /** + * 是否滑动到顶部 + * @return + */ + public boolean isScrolledToTop() { + return isScrolledToTop; + } + + /** + * 是否滑动到底部 + * @return + */ + public boolean isScrolledToBottom() { + return isScrolledToBottom; + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/SuperTextView.java b/app/src/main/java/com/example/baseframe/weight/SuperTextView.java new file mode 100644 index 0000000..e73d8cc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/SuperTextView.java @@ -0,0 +1,241 @@ +package com.example.baseframe.weight; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.TextView; + +import androidx.annotation.ColorRes; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +import com.example.baseframe.R; + + +/** + * 逐渐变色的TextView + * Created by yzh. 2019-11-28 + * 基于 @author Jenly 修改 Jenly + */ + +@SuppressLint("AppCompatCustomView") +public class SuperTextView extends TextView{ + + private int mDuration = 200; + + private boolean mIsStart; + + public boolean isStart() { + return mIsStart; + } + + private CharSequence mText; + + private int mPosition; + + private int mSelectedColor = 0xffff00ff; + + private OnDynamicListener mOnDynamicListener; + + private DynamicStyle mDynamicStyle = DynamicStyle.NORMAL; + + public enum DynamicStyle{ + NORMAL(0),TYPEWRITING(1),CHANGE_COLOR(2); + + private int mValue; + DynamicStyle(int value){ + this.mValue = value; + } + + private static DynamicStyle getFromInt(int value){ + + for(DynamicStyle style : DynamicStyle.values()){ + if(style.mValue == value){ + return style; + } + } + + return DynamicStyle.NORMAL; + } + } + + public SuperTextView(Context context) { + this(context,null); + } + + public SuperTextView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SuperTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context,attrs); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public SuperTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context,attrs); + } + + private void init(Context context,AttributeSet attrs){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuperTextView); + mText = a.getText(R.styleable.SuperTextView_dynamicText); + mDuration = a.getInt(R.styleable.SuperTextView_duration,mDuration); + mSelectedColor = a.getColor(R.styleable.SuperTextView_selectedColor,mSelectedColor); + mDynamicStyle = DynamicStyle.getFromInt(a.getInt(R.styleable.SuperTextView_dynamicStyle,0)); + a.recycle(); + } + + + public void start(){ + if(mIsStart){ + return; + } + if(TextUtils.isEmpty(mText)){//如果动态文本为空、则取getText()的文本内容 + mText = getText(); + } + mPosition = 0; + if(!TextUtils.isEmpty(mText)){ + mIsStart = true; + post(mRunnable); + }else{ + mIsStart = false; + if(mOnDynamicListener!=null){ + mOnDynamicListener.onCompile(); + } + } + } + + /** + * 停止 + */ + public void stop(){ + mIsStart = false; + mPosition = 0; + removeCallbacks(mRunnable); + } + /** + * 暂停 + */ + public void push(){ + mIsStart = false; + removeCallbacks(mRunnable); + } + + public void reStart(){ + mIsStart = true; + postDelayed(mRunnable,mDuration); + } + + private Runnable mRunnable = new Runnable() { + @Override + public void run() { + if(DynamicStyle.TYPEWRITING == mDynamicStyle){ + setText(mText.subSequence(0, mPosition)); + }else if(DynamicStyle.CHANGE_COLOR == mDynamicStyle){ + setChangeColorText(mPosition); + }else{ + setText(mText); + mIsStart = false; + if(mOnDynamicListener!=null){ + mOnDynamicListener.onCompile(); + } + return; + } + + if (mPosition < mText.length()) { + if(mOnDynamicListener!=null){ + mOnDynamicListener.onChange(mPosition,mText.length()); + } + mPosition++; + postDelayed(mRunnable,mDuration); + }else{ + if(mOnDynamicListener!=null){ + mOnDynamicListener.onChange(mPosition,mText.length()); + } + mIsStart = false; + if(mOnDynamicListener!=null){ + mOnDynamicListener.onCompile(); + } + } + } + }; + + private void setChangeColorText(int position){ + SpannableString spannableString = new SpannableString(mText); + spannableString.setSpan(new ForegroundColorSpan(mSelectedColor), 0, position, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + setText(spannableString); + } + + public void setDynamicText(@StringRes int resId){ + this.mText = getResources().getText(resId); + } + + public void setDynamicText(CharSequence text){ + this.mText = text; + } + + public CharSequence getDynamicText(){ + return mText; + } + + public void setDuration(int duration){ + this.mDuration = duration; + } + + /*** + * 根据指定播放完的总时间,计算出mDuration + * @param time 毫秒 + */ + public void setDurationByToalTime(long time){ + if(mText==null){ + return; + } + + Log.i("SuperTextView","总时长==="+time); + this.mDuration = (int) (time/mText.length()); + Log.d("SuperTextView","mDuration==="+mDuration); + + } + + public int getDuration(){ + return mDuration; + } + + public DynamicStyle getDynamicStyle(){ + return mDynamicStyle; + } + + public void setDynamicStyle(DynamicStyle dynamicStyle){ + this.mDynamicStyle = dynamicStyle; + } + + public void setOnDynamicListener(OnDynamicListener onDynamicListener){ + this.mOnDynamicListener = onDynamicListener; + } + + public void setSelectedColor(int selectedColor){ + this.mSelectedColor = selectedColor; + } + + public void setSelectedColorResource(@ColorRes int resId){ + this.mSelectedColor = ContextCompat.getColor(getContext(),resId); + } + + + public interface OnDynamicListener{ + void onChange(int position, int total); + void onCompile(); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/Banner.java b/app/src/main/java/com/example/baseframe/weight/banner/Banner.java new file mode 100644 index 0000000..99cbcc9 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/Banner.java @@ -0,0 +1,621 @@ +package com.example.baseframe.weight.banner; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.example.baseframe.R; +import com.example.baseframe.weight.banner.loader.ImageLoaderInterface; +import com.example.baseframe.weight.banner.loader.OnBannerListener; +import com.example.baseframe.weight.banner.view.BannerViewPager; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + + +//https://github.com/youth5201314/banner +public class Banner extends FrameLayout implements ViewPager.OnPageChangeListener { + public String tag = "banner"; + private int mIndicatorMargin = BannerConfig.PADDING_SIZE; + private int mIndicatorWidth; + private int mIndicatorHeight; + private int indicatorSize; + private int bannerBackgroundImage; + private int bannerStyle = BannerConfig.CIRCLE_INDICATOR; + private int delayTime = BannerConfig.TIME; + private int scrollTime = BannerConfig.DURATION; + private boolean isAutoPlay = BannerConfig.IS_AUTO_PLAY; + private boolean isScroll = BannerConfig.IS_SCROLL; + private int mIndicatorSelectedResId = R.drawable.gray_radius; + private int mIndicatorUnselectedResId = R.drawable.white_radius; + private int mLayoutResId = R.layout.banner; + private int titleHeight; + private int titleBackground; + private int titleTextColor; + private int titleTextSize; + private int count = 0; + private int currentItem; + private int gravity = -1; + private int lastPosition = 1; + private int scaleType = 1; + private List titles; + private List imageUrls; + private List imageViews; + private List indicatorImages; + private Context context; + private BannerViewPager viewPager; + private TextView bannerTitle, numIndicatorInside, numIndicator; + private LinearLayout indicator, indicatorInside, titleView; + private ImageView bannerDefaultImage; + private ImageLoaderInterface imageLoader; + private BannerPagerAdapter adapter; + private ViewPager.OnPageChangeListener mOnPageChangeListener; + private BannerScroller mScroller; + private OnBannerListener listener; + private DisplayMetrics dm; + + private WeakHandler handler = new WeakHandler(); + + public Banner(Context context) { + this(context, null); + } + + public Banner(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Banner(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + titles = new ArrayList<>(); + imageUrls = new ArrayList<>(); + imageViews = new ArrayList<>(); + indicatorImages = new ArrayList<>(); + dm = context.getResources().getDisplayMetrics(); + indicatorSize = dm.widthPixels / 80; + initView(context, attrs); + } + + private void initView(Context context, AttributeSet attrs) { + imageViews.clear(); + handleTypedArray(context, attrs); + View view = LayoutInflater.from(context).inflate(mLayoutResId, this, true); + bannerDefaultImage = (ImageView) view.findViewById(R.id.bannerDefaultImage); + viewPager = (BannerViewPager) view.findViewById(R.id.bannerViewPager); + titleView = (LinearLayout) view.findViewById(R.id.titleView); + indicator = (LinearLayout) view.findViewById(R.id.circleIndicator); + indicatorInside = (LinearLayout) view.findViewById(R.id.indicatorInside); + bannerTitle = (TextView) view.findViewById(R.id.bannerTitle); + numIndicator = (TextView) view.findViewById(R.id.numIndicator); + numIndicatorInside = (TextView) view.findViewById(R.id.numIndicatorInside); + bannerDefaultImage.setImageResource(bannerBackgroundImage); + initViewPagerScroll(); + } + + private void handleTypedArray(Context context, AttributeSet attrs) { + if (attrs == null) { + return; + } + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Banner); + mIndicatorWidth = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_width, indicatorSize); + mIndicatorHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_height, indicatorSize); + mIndicatorMargin = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_margin, BannerConfig.PADDING_SIZE); + mIndicatorSelectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_selected, R.drawable.indicator_select); + mIndicatorUnselectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_unselected, R.drawable.indicator_unselect); + scaleType = typedArray.getInt(R.styleable.Banner_image_scale_type, scaleType); + delayTime = typedArray.getInt(R.styleable.Banner_delay_time, BannerConfig.TIME); + scrollTime = typedArray.getInt(R.styleable.Banner_scroll_time, BannerConfig.DURATION); + isAutoPlay = typedArray.getBoolean(R.styleable.Banner_is_auto_play, BannerConfig.IS_AUTO_PLAY); + titleBackground = typedArray.getColor(R.styleable.Banner_title_background, BannerConfig.TITLE_BACKGROUND); + titleHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_title_height, BannerConfig.TITLE_HEIGHT); + titleTextColor = typedArray.getColor(R.styleable.Banner_title_textcolor, BannerConfig.TITLE_TEXT_COLOR); + titleTextSize = typedArray.getDimensionPixelSize(R.styleable.Banner_title_textsize, BannerConfig.TITLE_TEXT_SIZE); + mLayoutResId = typedArray.getResourceId(R.styleable.Banner_banner_layout, mLayoutResId); + bannerBackgroundImage = typedArray.getResourceId(R.styleable.Banner_banner_default_image, R.drawable.no_banner); + typedArray.recycle(); + } + + private void initViewPagerScroll() { + try { + Field mField = ViewPager.class.getDeclaredField("mScroller"); + mField.setAccessible(true); + mScroller = new BannerScroller(viewPager.getContext()); + mScroller.setDuration(scrollTime); + mField.set(viewPager, mScroller); + } catch (Exception e) { + Log.e(tag, e.getMessage()); + } + } + + + public Banner isAutoPlay(boolean isAutoPlay) { + this.isAutoPlay = isAutoPlay; + return this; + } + + public Banner setImageLoader(ImageLoaderInterface imageLoader) { + this.imageLoader = imageLoader; + return this; + } + + public Banner setDelayTime(int delayTime) { + this.delayTime = delayTime; + return this; + } + + public Banner setIndicatorGravity(int type) { + switch (type) { + case BannerConfig.LEFT: + this.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; + break; + case BannerConfig.CENTER: + this.gravity = Gravity.CENTER; + break; + case BannerConfig.RIGHT: + this.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; + break; + } + return this; + } + + public Banner setBannerAnimation(Class transformer) { + try { + setPageTransformer(true, transformer.newInstance()); + } catch (Exception e) { + Log.e(tag, "Please set the PageTransformer class"); + } + return this; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + * @param limit How many pages will be kept offscreen in an idle state. + * @return Banner + */ + public Banner setOffscreenPageLimit(int limit) { + if (viewPager != null) { + viewPager.setOffscreenPageLimit(limit); + } + return this; + } + + /** + * Set a {@link ViewPager.PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + * @return Banner + */ + public Banner setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) { + viewPager.setPageTransformer(reverseDrawingOrder, transformer); + return this; + } + + public Banner setBannerTitles(List titles) { + this.titles = titles; + return this; + } + + public Banner setBannerStyle(int bannerStyle) { + this.bannerStyle = bannerStyle; + return this; + } + + public Banner setViewPagerIsScroll(boolean isScroll) { + this.isScroll = isScroll; + return this; + } + + public Banner setImages(List imageUrls) { + this.imageUrls = imageUrls; + this.count = imageUrls.size(); + return this; + } + + public Banner update(List imageUrls, List titles) { + this.titles.clear(); + this.titles.addAll(titles); + update(imageUrls); + return this; + } + + public void update(List imageUrls) { + this.imageUrls.clear(); + this.imageViews.clear(); + this.indicatorImages.clear(); + this.imageUrls.addAll(imageUrls); + this.count = this.imageUrls.size(); + start(); + } + + public void updateBannerStyle(int bannerStyle) { + indicator.setVisibility(GONE); + numIndicator.setVisibility(GONE); + numIndicatorInside.setVisibility(GONE); + indicatorInside.setVisibility(GONE); + bannerTitle.setVisibility(View.GONE); + titleView.setVisibility(View.GONE); + this.bannerStyle = bannerStyle; + start(); + } + + public Banner start() { + setBannerStyleUI(); + setImageList(imageUrls); + setData(); + return this; + } + + private void setTitleStyleUI() { + if (titles.size() != imageUrls.size()) { + throw new RuntimeException("[Banner] --> The number of titles and images is different"); + } + if (titleBackground != -1) { + titleView.setBackgroundColor(titleBackground); + } + if (titleHeight != -1) { + titleView.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, titleHeight)); + } + if (titleTextColor != -1) { + bannerTitle.setTextColor(titleTextColor); + } + if (titleTextSize != -1) { + bannerTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleTextSize); + } + if (titles != null && titles.size() > 0) { + bannerTitle.setText(titles.get(0)); + bannerTitle.setVisibility(View.VISIBLE); + titleView.setVisibility(View.VISIBLE); + } + } + + private void setBannerStyleUI() { + int visibility = count > 1 ? View.VISIBLE : View.GONE; + switch (bannerStyle) { + case BannerConfig.CIRCLE_INDICATOR: + indicator.setVisibility(visibility); + break; + case BannerConfig.NUM_INDICATOR: + numIndicator.setVisibility(visibility); + break; + case BannerConfig.NUM_INDICATOR_TITLE: + numIndicatorInside.setVisibility(visibility); + setTitleStyleUI(); + break; + case BannerConfig.CIRCLE_INDICATOR_TITLE: + indicator.setVisibility(visibility); + setTitleStyleUI(); + break; + case BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE: + indicatorInside.setVisibility(visibility); + setTitleStyleUI(); + break; + } + } + + private void initImages() { + imageViews.clear(); + if (bannerStyle == BannerConfig.CIRCLE_INDICATOR || + bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE || + bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE) { + createIndicator(); + } else if (bannerStyle == BannerConfig.NUM_INDICATOR_TITLE) { + numIndicatorInside.setText("1/" + count); + } else if (bannerStyle == BannerConfig.NUM_INDICATOR) { + numIndicator.setText("1/" + count); + } + } + + private void setImageList(List imagesUrl) { + if (imagesUrl == null || imagesUrl.size() <= 0) { + bannerDefaultImage.setVisibility(VISIBLE); + Log.e(tag, "The image data set is empty."); + return; + } + bannerDefaultImage.setVisibility(GONE); + initImages(); + for (int i = 0; i <= count + 1; i++) { + View imageView = null; + if (imageLoader != null) { + imageView = imageLoader.createImageView(context); + } + if (imageView == null) { + imageView = new ImageView(context); + } + setScaleType(imageView); + Object url = null; + if (i == 0) { + url = imagesUrl.get(count - 1); + } else if (i == count + 1) { + url = imagesUrl.get(0); + } else { + url = imagesUrl.get(i - 1); + } + imageViews.add(imageView); + if (imageLoader != null) + imageLoader.displayImage(context, url, imageView); + else + Log.e(tag, "Please set images loader."); + } + } + + private void setScaleType(View imageView) { + if (imageView instanceof ImageView) { + ImageView view = ((ImageView) imageView); + switch (scaleType) { + case 0: + view.setScaleType(ScaleType.CENTER); + break; + case 1: + view.setScaleType(ScaleType.CENTER_CROP); + break; + case 2: + view.setScaleType(ScaleType.CENTER_INSIDE); + break; + case 3: + view.setScaleType(ScaleType.FIT_CENTER); + break; + case 4: + view.setScaleType(ScaleType.FIT_END); + break; + case 5: + view.setScaleType(ScaleType.FIT_START); + break; + case 6: + view.setScaleType(ScaleType.FIT_XY); + break; + case 7: + view.setScaleType(ScaleType.MATRIX); + break; + } + + } + } + + private void createIndicator() { + indicatorImages.clear(); + indicator.removeAllViews(); + indicatorInside.removeAllViews(); + for (int i = 0; i < count; i++) { + ImageView imageView = new ImageView(context); +// imageView.setScaleType(ScaleType.CENTER_CROP); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + params.leftMargin = mIndicatorMargin; + params.rightMargin = mIndicatorMargin; + if (i == 0) { + imageView.setImageResource(mIndicatorSelectedResId); + } else { + imageView.setImageResource(mIndicatorUnselectedResId); + } + indicatorImages.add(imageView); + if (bannerStyle == BannerConfig.CIRCLE_INDICATOR || + bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE) + indicator.addView(imageView, params); + else if (bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE) + indicatorInside.addView(imageView, params); + } + } + + + private void setData() { + currentItem = 1; + if (adapter == null) { + adapter = new BannerPagerAdapter(); + viewPager.addOnPageChangeListener(this); + } + viewPager.setAdapter(adapter); + viewPager.setFocusable(true); + viewPager.setCurrentItem(1); + if (gravity != -1) + indicator.setGravity(gravity); + if (isScroll && count > 1) { + viewPager.setScrollable(true); + } else { + viewPager.setScrollable(false); + } + if (isAutoPlay) + startAutoPlay(); + } + + + public void startAutoPlay() { + handler.removeCallbacks(task); + handler.postDelayed(task, delayTime); + } + + public void stopAutoPlay() { + handler.removeCallbacks(task); + } + + private final Runnable task = new Runnable() { + @Override + public void run() { + if (count > 1 && isAutoPlay) { + currentItem = currentItem % (count + 1) + 1; +// Log.i(tag, "curr:" + currentItem + " count:" + count); + if (currentItem == 1) { + viewPager.setCurrentItem(currentItem, false); + handler.post(task); + } else { + viewPager.setCurrentItem(currentItem); + handler.postDelayed(task, delayTime); + } + } + } + }; + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { +// Log.i(tag, ev.getAction() + "--" + isAutoPlay); + if (isAutoPlay) { + int action = ev.getAction(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_OUTSIDE) { + startAutoPlay(); + } else if (action == MotionEvent.ACTION_DOWN) { + stopAutoPlay(); + } + } + return super.dispatchTouchEvent(ev); + } + + /** + * 返回真实的位置 + * + * @param position + * @return 下标从0开始 + */ + public int toRealPosition(int position) { + int realPosition = (position - 1) % count; + if (realPosition < 0) + realPosition += count; + return realPosition; + } + + class BannerPagerAdapter extends PagerAdapter { + + @Override + public int getCount() { + return imageViews.size(); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public Object instantiateItem(ViewGroup container, final int position) { + container.addView(imageViews.get(position)); + View view = imageViews.get(position); + + if (listener != null) { + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + listener.OnBannerClick(toRealPosition(position)); + } + }); + } + return view; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + + } + + @Override + public void onPageScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } +// Log.i(tag,"currentItem: "+currentItem); + switch (state) { + case 0://No operation + if (currentItem == 0) { + viewPager.setCurrentItem(count, false); + } else if (currentItem == count + 1) { + viewPager.setCurrentItem(1, false); + } + break; + case 1://start Sliding + if (currentItem == count + 1) { + viewPager.setCurrentItem(1, false); + } else if (currentItem == 0) { + viewPager.setCurrentItem(count, false); + } + break; + case 2://end Sliding + break; + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(toRealPosition(position), positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + currentItem = position; + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(toRealPosition(position)); + } + if (bannerStyle == BannerConfig.CIRCLE_INDICATOR || + bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE || + bannerStyle == BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE) { + indicatorImages.get((lastPosition - 1 + count) % count).setImageResource(mIndicatorUnselectedResId); + indicatorImages.get((position - 1 + count) % count).setImageResource(mIndicatorSelectedResId); + lastPosition = position; + } + if (position == 0) position = count; + if (position > count) position = 1; + switch (bannerStyle) { + case BannerConfig.CIRCLE_INDICATOR: + break; + case BannerConfig.NUM_INDICATOR: + numIndicator.setText(position + "/" + count); + break; + case BannerConfig.NUM_INDICATOR_TITLE: + numIndicatorInside.setText(position + "/" + count); + bannerTitle.setText(titles.get(position - 1)); + break; + case BannerConfig.CIRCLE_INDICATOR_TITLE: + bannerTitle.setText(titles.get(position - 1)); + break; + case BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE: + bannerTitle.setText(titles.get(position - 1)); + break; + } + + } + + + /** + * 废弃了旧版接口,新版的接口下标是从1开始,同时解决下标越界问题 + * + * @param listener + * @return + */ + public Banner setOnBannerListener(OnBannerListener listener) { + this.listener = listener; + return this; + } + + public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) { + mOnPageChangeListener = onPageChangeListener; + } + + public void releaseBanner() { + handler.removeCallbacksAndMessages(null); + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/BannerConfig.java b/app/src/main/java/com/example/baseframe/weight/banner/BannerConfig.java new file mode 100644 index 0000000..8c4ea46 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/BannerConfig.java @@ -0,0 +1,40 @@ +package com.example.baseframe.weight.banner; + + +public class BannerConfig { + /** + * indicator style + */ + public static final int NOT_INDICATOR = 0; + public static final int CIRCLE_INDICATOR = 1; + public static final int NUM_INDICATOR = 2; + public static final int NUM_INDICATOR_TITLE = 3; + public static final int CIRCLE_INDICATOR_TITLE = 4; + public static final int CIRCLE_INDICATOR_TITLE_INSIDE = 5; + + + /** + * indicator gravity + */ + public static final int LEFT = 5; + public static final int CENTER = 6; + public static final int RIGHT = 7; + + /** + * banner + */ + public static final int PADDING_SIZE = 5; + public static final int TIME = 2000; + public static final int DURATION = 800; + public static final boolean IS_AUTO_PLAY = true; + public static final boolean IS_SCROLL = true; + + /** + * title style + */ + public static final int TITLE_BACKGROUND = -1; + public static final int TITLE_HEIGHT = -1; + public static final int TITLE_TEXT_COLOR = -1; + public static final int TITLE_TEXT_SIZE = -1; + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/BannerScroller.java b/app/src/main/java/com/example/baseframe/weight/banner/BannerScroller.java new file mode 100644 index 0000000..2ac3a01 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/BannerScroller.java @@ -0,0 +1,36 @@ +package com.example.baseframe.weight.banner; + +import android.content.Context; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +public class BannerScroller extends Scroller { + private int mDuration = BannerConfig.DURATION; + + public BannerScroller(Context context) { + super(context); + } + + public BannerScroller(Context context, Interpolator interpolator) { + super(context, interpolator); + } + + public BannerScroller(Context context, Interpolator interpolator, boolean flywheel) { + super(context, interpolator, flywheel); + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + super.startScroll(startX, startY, dx, dy, mDuration); + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy) { + super.startScroll(startX, startY, dx, dy, mDuration); + } + + public void setDuration(int time) { + mDuration = time; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/Transformer.java b/app/src/main/java/com/example/baseframe/weight/banner/Transformer.java new file mode 100644 index 0000000..b8c90d0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/Transformer.java @@ -0,0 +1,43 @@ +package com.example.baseframe.weight.banner; + +import androidx.viewpager.widget.ViewPager.PageTransformer; + +import com.example.baseframe.weight.banner.transformer.AccordionTransformer; +import com.example.baseframe.weight.banner.transformer.BackgroundToForegroundTransformer; +import com.example.baseframe.weight.banner.transformer.CubeInTransformer; +import com.example.baseframe.weight.banner.transformer.CubeOutTransformer; +import com.example.baseframe.weight.banner.transformer.CubePageTransformer; +import com.example.baseframe.weight.banner.transformer.DefaultTransformer; +import com.example.baseframe.weight.banner.transformer.DepthPageTransformer; +import com.example.baseframe.weight.banner.transformer.FlipHorizontalTransformer; +import com.example.baseframe.weight.banner.transformer.FlipVerticalTransformer; +import com.example.baseframe.weight.banner.transformer.ForegroundToBackgroundTransformer; +import com.example.baseframe.weight.banner.transformer.RotateDownTransformer; +import com.example.baseframe.weight.banner.transformer.RotateUpTransformer; +import com.example.baseframe.weight.banner.transformer.ScaleInOutTransformer; +import com.example.baseframe.weight.banner.transformer.StackTransformer; +import com.example.baseframe.weight.banner.transformer.TabletTransformer; +import com.example.baseframe.weight.banner.transformer.ZoomInTransformer; +import com.example.baseframe.weight.banner.transformer.ZoomOutSlideTransformer; +import com.example.baseframe.weight.banner.transformer.ZoomOutTranformer; + +public class Transformer { + public static Class Default = DefaultTransformer.class; + public static Class Accordion = AccordionTransformer.class; + public static Class BackgroundToForeground = BackgroundToForegroundTransformer.class; + public static Class ForegroundToBackground = ForegroundToBackgroundTransformer.class; + public static Class CubeIn = CubeInTransformer.class; + public static Class CubeOut = CubeOutTransformer.class; + public static Class DepthPage = DepthPageTransformer.class; + public static Class FlipHorizontal = FlipHorizontalTransformer.class; + public static Class FlipVertical = FlipVerticalTransformer.class; + public static Class RotateDown = RotateDownTransformer.class; + public static Class RotateUp = RotateUpTransformer.class; + public static Class ScaleInOut = ScaleInOutTransformer.class; + public static Class Stack = StackTransformer.class; + public static Class Tablet = TabletTransformer.class; + public static Class ZoomIn = ZoomInTransformer.class; + public static Class ZoomOut = ZoomOutTranformer.class; + public static Class ZoomOutSlide = ZoomOutSlideTransformer.class; + public static Class CubePage = CubePageTransformer.class; +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/WeakHandler.java b/app/src/main/java/com/example/baseframe/weight/banner/WeakHandler.java new file mode 100644 index 0000000..89bc83d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/WeakHandler.java @@ -0,0 +1,478 @@ +package com.example.baseframe.weight.banner; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.lang.ref.WeakReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@SuppressWarnings("unused") +public class WeakHandler { + private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory + private final ExecHandler mExec; + private Lock mLock = new ReentrantLock(); + @SuppressWarnings("ConstantConditions") + @VisibleForTesting + final ChainedRef mRunnables = new ChainedRef(mLock, null); + + /** + * Default constructor associates this handler with the {@link Looper} for the + * current thread. + * + * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + */ + public WeakHandler() { + mCallback = null; + mExec = new ExecHandler(); + } + + /** + * Constructor associates this handler with the {@link Looper} for the + * current thread and takes a callback interface in which you can handle + * messages. + * + * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + * + * @param callback The callback interface in which to handle messages, or null. + */ + public WeakHandler(@Nullable Handler.Callback callback) { + mCallback = callback; // Hard referencing body + mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler + } + + /** + * Use the provided {@link Looper} instead of the default one. + * + * @param looper The looper, must not be null. + */ + public WeakHandler(@NonNull Looper looper) { + mCallback = null; + mExec = new ExecHandler(looper); + } + + /** + * Use the provided {@link Looper} instead of the default one and take a callback + * interface in which to handle messages. + * + * @param looper The looper, must not be null. + * @param callback The callback interface in which to handle messages, or null. + */ + public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) { + mCallback = callback; + mExec = new ExecHandler(looper, new WeakReference<>(callback)); + } + + /** + * Causes the Runnable r to be added to the message queue. + * The runnable will be run on the thread to which this handler is + * attached. + * + * @param r The Runnable that will be executed. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean post(@NonNull Runnable r) { + return mExec.post(wrapRunnable(r)); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) { + return mExec.postAtTime(wrapRunnable(r), uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + * + * @see android.os.SystemClock#uptimeMillis + */ + public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { + return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * after the specified amount of time elapses. + * The runnable will be run on the thread to which this handler + * is attached. + * + * @param r The Runnable that will be executed. + * @param delayMillis The delay (in milliseconds) until the Runnable + * will be executed. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- + * if the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postDelayed(Runnable r, long delayMillis) { + return mExec.postDelayed(wrapRunnable(r), delayMillis); + } + + /** + * Posts a message to an object that implements Runnable. + * Causes the Runnable r to executed on the next iteration through the + * message queue. The runnable will be run on the thread to which this + * handler is attached. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @param r The Runnable that will be executed. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean postAtFrontOfQueue(Runnable r) { + return mExec.postAtFrontOfQueue(wrapRunnable(r)); + } + + /** + * Remove any pending posts of Runnable r that are in the message queue. + */ + public final void removeCallbacks(Runnable r) { + final WeakRunnable runnable = mRunnables.remove(r); + if (runnable != null) { + mExec.removeCallbacks(runnable); + } + } + + /** + * Remove any pending posts of Runnable r with Object + * token that are in the message queue. If token is null, + * all callbacks will be removed. + */ + public final void removeCallbacks(Runnable r, Object token) { + final WeakRunnable runnable = mRunnables.remove(r); + if (runnable != null) { + mExec.removeCallbacks(runnable, token); + } + } + + /** + * Pushes a message onto the end of the message queue after all pending messages + * before the current time. It will be received in callback, + * in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessage(Message msg) { + return mExec.sendMessage(msg); + } + + /** + * Sends a Message containing only the what value. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessage(int what) { + return mExec.sendEmptyMessage(what); + } + + /** + * Sends a Message containing only the what value, to be delivered + * after the specified amount of time elapses. + * @see #sendMessageDelayed(Message, long) + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + return mExec.sendEmptyMessageDelayed(what, delayMillis); + } + + /** + * Sends a Message containing only the what value, to be delivered + * at a specific time. + * @see #sendMessageAtTime(Message, long) + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { + return mExec.sendEmptyMessageAtTime(what, uptimeMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before (current time + delayMillis). You will receive it in + * callback, in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean sendMessageDelayed(Message msg, long delayMillis) { + return mExec.sendMessageDelayed(msg, delayMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before the absolute time (in milliseconds) uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * You will receive it in callback, in the thread attached + * to this handler. + * + * @param uptimeMillis The absolute time at which the message should be + * delivered, using the + * {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + return mExec.sendMessageAtTime(msg, uptimeMillis); + } + + /** + * Enqueue a message at the front of the message queue, to be processed on + * the next iteration of the message loop. You will receive it in + * callback, in the thread attached to this handler. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessageAtFrontOfQueue(Message msg) { + return mExec.sendMessageAtFrontOfQueue(msg); + } + + /** + * Remove any pending posts of messages with code 'what' that are in the + * message queue. + */ + public final void removeMessages(int what) { + mExec.removeMessages(what); + } + + /** + * Remove any pending posts of messages with code 'what' and whose obj is + * 'object' that are in the message queue. If object is null, + * all messages will be removed. + */ + public final void removeMessages(int what, Object object) { + mExec.removeMessages(what, object); + } + + /** + * Remove any pending posts of callbacks and sent messages whose + * obj is token. If token is null, + * all callbacks and messages will be removed. + */ + public final void removeCallbacksAndMessages(Object token) { + mExec.removeCallbacksAndMessages(token); + } + + /** + * Check if there are any pending posts of messages with code 'what' in + * the message queue. + */ + public final boolean hasMessages(int what) { + return mExec.hasMessages(what); + } + + /** + * Check if there are any pending posts of messages with code 'what' and + * whose obj is 'object' in the message queue. + */ + public final boolean hasMessages(int what, Object object) { + return mExec.hasMessages(what, object); + } + + public final Looper getLooper() { + return mExec.getLooper(); + } + + private WeakRunnable wrapRunnable(@NonNull Runnable r) { + //noinspection ConstantConditions + if (r == null) { + throw new NullPointerException("Runnable can't be null"); + } + final ChainedRef hardRef = new ChainedRef(mLock, r); + mRunnables.insertAfter(hardRef); + return hardRef.wrapper; + } + + private static class ExecHandler extends Handler { + private final WeakReference mCallback; + + ExecHandler() { + mCallback = null; + } + + ExecHandler(WeakReference callback) { + mCallback = callback; + } + + ExecHandler(Looper looper) { + super(looper); + mCallback = null; + } + + ExecHandler(Looper looper, WeakReference callback) { + super(looper); + mCallback = callback; + } + + @Override + public void handleMessage(@NonNull Message msg) { + if (mCallback == null) { + return; + } + final Callback callback = mCallback.get(); + if (callback == null) { // Already disposed + return; + } + callback.handleMessage(msg); + } + } + + static class WeakRunnable implements Runnable { + private final WeakReference mDelegate; + private final WeakReference mReference; + + WeakRunnable(WeakReference delegate, WeakReference reference) { + mDelegate = delegate; + mReference = reference; + } + + @Override + public void run() { + final Runnable delegate = mDelegate.get(); + final ChainedRef reference = mReference.get(); + if (reference != null) { + reference.remove(); + } + if (delegate != null) { + delegate.run(); + } + } + } + + static class ChainedRef { + @Nullable + ChainedRef next; + @Nullable + ChainedRef prev; + @NonNull + final Runnable runnable; + @NonNull + final WeakRunnable wrapper; + + @NonNull + Lock lock; + + public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) { + this.runnable = r; + this.lock = lock; + this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); + } + + public WeakRunnable remove() { + lock.lock(); + try { + if (prev != null) { + prev.next = next; + } + if (next != null) { + next.prev = prev; + } + prev = null; + next = null; + } finally { + lock.unlock(); + } + return wrapper; + } + + public void insertAfter(@NonNull ChainedRef candidate) { + lock.lock(); + try { + if (this.next != null) { + this.next.prev = candidate; + } + + candidate.next = this.next; + this.next = candidate; + candidate.prev = this; + } finally { + lock.unlock(); + } + } + + @Nullable + public WeakRunnable remove(Runnable obj) { + lock.lock(); + try { + ChainedRef curr = this.next; // Skipping head + while (curr != null) { + if (curr.runnable == obj) { // We do comparison exactly how Handler does inside + return curr.remove(); + } + curr = curr.next; + } + } finally { + lock.unlock(); + } + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoader.java b/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoader.java new file mode 100644 index 0000000..46ae130 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoader.java @@ -0,0 +1,15 @@ +package com.example.baseframe.weight.banner.loader; + +import android.content.Context; +import android.widget.ImageView; + + +public abstract class ImageLoader implements ImageLoaderInterface { + + @Override + public ImageView createImageView(Context context) { + ImageView imageView = new ImageView(context); + return imageView; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoaderInterface.java b/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoaderInterface.java new file mode 100644 index 0000000..1b2e95b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/loader/ImageLoaderInterface.java @@ -0,0 +1,14 @@ +package com.example.baseframe.weight.banner.loader; + +import android.content.Context; +import android.view.View; + +import java.io.Serializable; + + +public interface ImageLoaderInterface extends Serializable { + + void displayImage(Context context, Object path, T imageView); + + T createImageView(Context context); +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/loader/OnBannerListener.java b/app/src/main/java/com/example/baseframe/weight/banner/loader/OnBannerListener.java new file mode 100644 index 0000000..efb0f5b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/loader/OnBannerListener.java @@ -0,0 +1,5 @@ +package com.example.baseframe.weight.banner.loader; + +public interface OnBannerListener { + public void OnBannerClick(int position); +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ABaseTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ABaseTransformer.java new file mode 100644 index 0000000..d840a28 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ABaseTransformer.java @@ -0,0 +1,131 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +import androidx.viewpager.widget.ViewPager; + +public abstract class ABaseTransformer implements ViewPager.PageTransformer { + + /** + * Called each {@link #transformPage(View, float)}. + * + * @param page + * Apply the transformation to this page + * @param position + * Position of page relative to the current front-and-center position of the pager. 0 is front and + * center. 1 is one full page position to the right, and -1 is one page position to the left. + */ + protected abstract void onTransform(View page, float position); + + /** + * Apply a property transformation to the given page. For most use cases, this method should not be overridden. + * Instead use {@link #transformPage(View, float)} to perform typical transformations. + * + * @param page + * Apply the transformation to this page + * @param position + * Position of page relative to the current front-and-center position of the pager. 0 is front and + * center. 1 is one full page position to the right, and -1 is one page position to the left. + */ + @Override + public void transformPage(View page, float position) { + onPreTransform(page, position); + onTransform(page, position); + onPostTransform(page, position); + } + + /** + * If the position offset of a fragment is less than negative one or greater than one, returning true will set the + * fragment alpha to 0f. Otherwise fragment alpha is always defaulted to 1f. + * + * @return + */ + protected boolean hideOffscreenPages() { + return true; + } + + /** + * Indicates if the default animations of the view pager should be used. + * + * @return + */ + protected boolean isPagingEnabled() { + return false; + } + + /** + * Called each {@link #transformPage(View, float)} before {{@link #onTransform(View, float)}. + *

    + * The default implementation attempts to reset all view properties. This is useful when toggling transforms that do + * not modify the same page properties. For instance changing from a transformation that applies rotation to a + * transformation that fades can inadvertently leave a fragment stuck with a rotation or with some degree of applied + * alpha. + * + * @param page + * Apply the transformation to this page + * @param position + * Position of page relative to the current front-and-center position of the pager. 0 is front and + * center. 1 is one full page position to the right, and -1 is one page position to the left. + */ + protected void onPreTransform(View page, float position) { + final float width = page.getWidth(); + + page.setRotationX(0); + page.setRotationY(0); + page.setRotation(0); + page.setScaleX(1); + page.setScaleY(1); + page.setPivotX(0); + page.setPivotY(0); + page.setTranslationY(0); + page.setTranslationX(isPagingEnabled() ? 0f : -width * position); + + if (hideOffscreenPages()) { + page.setAlpha(position <= -1f || position >= 1f ? 0f : 1f); +// page.setEnabled(false); + } else { +// page.setEnabled(true); + page.setAlpha(1f); + } + } + + /** + * Called each {@link #transformPage(View, float)} after {@link #onTransform(View, float)}. + * + * @param page + * Apply the transformation to this page + * @param position + * Position of page relative to the current front-and-center position of the pager. 0 is front and + * center. 1 is one full page position to the right, and -1 is one page position to the left. + */ + protected void onPostTransform(View page, float position) { + } + + /** + * Same as {@link Math#min(double, double)} without double casting, zero closest to infinity handling, or NaN support. + * + * @param val + * @param min + * @return + */ + protected static final float min(float val, float min) { + return val < min ? min : val; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/AccordionTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/AccordionTransformer.java new file mode 100644 index 0000000..a73224b --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/AccordionTransformer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class AccordionTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + view.setPivotX(position < 0 ? 0 : view.getWidth()); + view.setScaleX(position < 0 ? 1f + position : 1f - position); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/BackgroundToForegroundTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/BackgroundToForegroundTransformer.java new file mode 100644 index 0000000..6f8aa39 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/BackgroundToForegroundTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class BackgroundToForegroundTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float height = view.getHeight(); + final float width = view.getWidth(); + final float scale = min(position < 0 ? 1f : Math.abs(1f - position), 0.5f); + + view.setScaleX(scale); + view.setScaleY(scale); + view.setPivotX(width * 0.5f); + view.setPivotY(height * 0.5f); + view.setTranslationX(position < 0 ? width * position : -width * position * 0.25f); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeInTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeInTransformer.java new file mode 100644 index 0000000..a0a801d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeInTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class CubeInTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + // Rotate the fragment on the left or right edge + view.setPivotX(position > 0 ? 0 : view.getWidth()); + view.setPivotY(0); + view.setRotationY(-90f * position); + } + + @Override + public boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeOutTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeOutTransformer.java new file mode 100644 index 0000000..f1e48a1 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubeOutTransformer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class CubeOutTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + view.setPivotX(position < 0f ? view.getWidth() : 0f); + view.setPivotY(view.getHeight() * 0.5f); + view.setRotationY(90f * position); + } + + @Override + public boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubePageTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubePageTransformer.java new file mode 100644 index 0000000..a6f5a90 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/CubePageTransformer.java @@ -0,0 +1,71 @@ +package com.example.baseframe.weight.banner.transformer; + + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.ViewPager; + +/** + * 创建时间:15/6/19 17:39 + * 描述: //立方体 + */ +public class CubePageTransformer implements ViewPager.PageTransformer { + private float mMaxRotation = 90.0f; + + public CubePageTransformer() { + } + + public CubePageTransformer(float maxRotation) { + setMaxRotation(maxRotation); + } + + + @Override + public void transformPage(@NonNull View view, float position) { + if (position < -1.0f) { + // [-Infinity,-1) + // This page is way off-screen to the left. + handleInvisiblePage(view, position); + } else if (position <= 0.0f) { + // [-1,0] + // Use the default slide transition when moving to the left page + handleLeftPage(view, position); + } else if (position <= 1.0f) { + // (0,1] + handleRightPage(view, position); + } else { + // (1,+Infinity] + // This page is way off-screen to the right. + handleInvisiblePage(view, position); + } + } + + + public void handleInvisiblePage(View view, float position) { + view.setPivotX(view.getMeasuredWidth()); + view.setPivotY( view.getMeasuredHeight() * 0.5f); + view.setRotationY(0); + } + + + public void handleLeftPage(View view, float position) { + view.setPivotX(view.getMeasuredWidth()); + view.setPivotY(view.getMeasuredHeight() * 0.5f); + view.setRotationY(mMaxRotation * position); + } + + + public void handleRightPage(View view, float position) { + view.setPivotX(0); + view.setPivotY(view.getMeasuredHeight() * 0.5f); + view.setRotationY(mMaxRotation * position); + } + + public void setMaxRotation(float maxRotation) { + if (maxRotation >= 0.0f && maxRotation <= 90.0f) { + mMaxRotation = maxRotation; + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/DefaultTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/DefaultTransformer.java new file mode 100644 index 0000000..9a65ae0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/DefaultTransformer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class DefaultTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + } + + @Override + public boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/DepthPageTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/DepthPageTransformer.java new file mode 100644 index 0000000..61e4e61 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/DepthPageTransformer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class DepthPageTransformer extends ABaseTransformer { + + private static final float MIN_SCALE = 0.75f; + + @Override + protected void onTransform(View view, float position) { + if (position <= 0f) { + view.setTranslationX(0f); + view.setScaleX(1f); + view.setScaleY(1f); + } else if (position <= 1f) { + final float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)); + view.setAlpha(1 - position); + view.setPivotY(0.5f * view.getHeight()); + view.setTranslationX(view.getWidth() * -position); + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + } + } + + @Override + protected boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipHorizontalTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipHorizontalTransformer.java new file mode 100644 index 0000000..231ecf3 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipHorizontalTransformer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class FlipHorizontalTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float rotation = 180f * position; + + view.setAlpha(rotation > 90f || rotation < -90f ? 0 : 1); + view.setPivotX(view.getWidth() * 0.5f); + view.setPivotY(view.getHeight() * 0.5f); + view.setRotationY(rotation); + } + + @Override + protected void onPostTransform(View page, float position) { + super.onPostTransform(page, position); + + //resolve problem: new page can't handle click event! + if (position > -0.5f && position < 0.5f) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipVerticalTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipVerticalTransformer.java new file mode 100644 index 0000000..451dffd --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/FlipVerticalTransformer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class FlipVerticalTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float rotation = -180f * position; + + view.setAlpha(rotation > 90f || rotation < -90f ? 0f : 1f); + view.setPivotX(view.getWidth() * 0.5f); + view.setPivotY(view.getHeight() * 0.5f); + view.setRotationX(rotation); + } + + @Override + protected void onPostTransform(View page, float position) { + super.onPostTransform(page, position); + + if (position > -0.5f && position < 0.5f) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ForegroundToBackgroundTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ForegroundToBackgroundTransformer.java new file mode 100644 index 0000000..6d85c47 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ForegroundToBackgroundTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class ForegroundToBackgroundTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float height = view.getHeight(); + final float width = view.getWidth(); + final float scale = min(position > 0 ? 1f : Math.abs(1f + position), 0.5f); + + view.setScaleX(scale); + view.setScaleY(scale); + view.setPivotX(width * 0.5f); + view.setPivotY(height * 0.5f); + view.setTranslationX(position > 0 ? width * position : -width * position * 0.25f); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateDownTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateDownTransformer.java new file mode 100644 index 0000000..4e16566 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateDownTransformer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class RotateDownTransformer extends ABaseTransformer { + + private static final float ROT_MOD = -15f; + + @Override + protected void onTransform(View view, float position) { + final float width = view.getWidth(); + final float height = view.getHeight(); + final float rotation = ROT_MOD * position * -1.25f; + + view.setPivotX(width * 0.5f); + view.setPivotY(height); + view.setRotation(rotation); + } + + @Override + protected boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateUpTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateUpTransformer.java new file mode 100644 index 0000000..efd5d27 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/RotateUpTransformer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class RotateUpTransformer extends ABaseTransformer { + + private static final float ROT_MOD = -15f; + + @Override + protected void onTransform(View view, float position) { + final float width = view.getWidth(); + final float rotation = ROT_MOD * position; + + view.setPivotX(width * 0.5f); + view.setPivotY(0f); + view.setTranslationX(0f); + view.setRotation(rotation); + } + + @Override + protected boolean isPagingEnabled() { + return true; + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ScaleInOutTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ScaleInOutTransformer.java new file mode 100644 index 0000000..4966b39 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ScaleInOutTransformer.java @@ -0,0 +1,16 @@ +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class ScaleInOutTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + view.setPivotX(position < 0 ? 0 : view.getWidth()); + view.setPivotY(view.getHeight() / 2f); + float scale = position < 0 ? 1f + position : 1f - position; + view.setScaleX(scale); + view.setScaleY(scale); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/StackTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/StackTransformer.java new file mode 100644 index 0000000..0e97e1a --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/StackTransformer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class StackTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + view.setTranslationX(position < 0 ? 0f : -view.getWidth() * position); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/TabletTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/TabletTransformer.java new file mode 100644 index 0000000..c693efd --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/TabletTransformer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.graphics.Camera; +import android.graphics.Matrix; +import android.view.View; + +public class TabletTransformer extends ABaseTransformer { + + private static final Matrix OFFSET_MATRIX = new Matrix(); + private static final Camera OFFSET_CAMERA = new Camera(); + private static final float[] OFFSET_TEMP_FLOAT = new float[2]; + + @Override + protected void onTransform(View view, float position) { + final float rotation = (position < 0 ? 30f : -30f) * Math.abs(position); + + view.setTranslationX(getOffsetXForRotation(rotation, view.getWidth(), view.getHeight())); + view.setPivotX(view.getWidth() * 0.5f); + view.setPivotY(0); + view.setRotationY(rotation); + } + + protected static final float getOffsetXForRotation(float degrees, int width, int height) { + OFFSET_MATRIX.reset(); + OFFSET_CAMERA.save(); + OFFSET_CAMERA.rotateY(Math.abs(degrees)); + OFFSET_CAMERA.getMatrix(OFFSET_MATRIX); + OFFSET_CAMERA.restore(); + + OFFSET_MATRIX.preTranslate(-width * 0.5f, -height * 0.5f); + OFFSET_MATRIX.postTranslate(width * 0.5f, height * 0.5f); + OFFSET_TEMP_FLOAT[0] = width; + OFFSET_TEMP_FLOAT[1] = height; + OFFSET_MATRIX.mapPoints(OFFSET_TEMP_FLOAT); + return (width - OFFSET_TEMP_FLOAT[0]) * (degrees > 0.0f ? 1.0f : -1.0f); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomInTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomInTransformer.java new file mode 100644 index 0000000..7c651de --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomInTransformer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class ZoomInTransformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float scale = position < 0 ? position + 1f : Math.abs(1f - position); + view.setScaleX(scale); + view.setScaleY(scale); + view.setPivotX(view.getWidth() * 0.5f); + view.setPivotY(view.getHeight() * 0.5f); + view.setAlpha(position < -1f || position > 1f ? 0f : 1f - (scale - 1f)); + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutSlideTransformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutSlideTransformer.java new file mode 100644 index 0000000..2ca88f0 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutSlideTransformer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class ZoomOutSlideTransformer extends ABaseTransformer { + + private static final float MIN_SCALE = 0.85f; + private static final float MIN_ALPHA = 0.5f; + + @Override + protected void onTransform(View view, float position) { + if (position >= -1 || position <= 1) { + // Modify the default slide transition to shrink the page as well + final float height = view.getHeight(); + final float width = view.getWidth(); + final float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position)); + final float vertMargin = height * (1 - scaleFactor) / 2; + final float horzMargin = width * (1 - scaleFactor) / 2; + + // Center vertically + view.setPivotY(0.5f * height); + view.setPivotX(0.5f * width); + + if (position < 0) { + view.setTranslationX(horzMargin - vertMargin / 2); + } else { + view.setTranslationX(-horzMargin + vertMargin / 2); + } + + // Scale the page down (between MIN_SCALE and 1) + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + + // Fade the page relative to its size. + view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA)); + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutTranformer.java b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutTranformer.java new file mode 100644 index 0000000..997abfe --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/transformer/ZoomOutTranformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Toxic Bakery + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.baseframe.weight.banner.transformer; + +import android.view.View; + +public class ZoomOutTranformer extends ABaseTransformer { + + @Override + protected void onTransform(View view, float position) { + final float scale = 1f + Math.abs(position); + view.setScaleX(scale); + view.setScaleY(scale); + view.setPivotX(view.getWidth() * 0.5f); + view.setPivotY(view.getHeight() * 0.5f); + view.setAlpha(position < -1f || position > 1f ? 0f : 1f - (scale - 1f)); + if(position == -1){ + view.setTranslationX(view.getWidth() * -1); + } + } + +} diff --git a/app/src/main/java/com/example/baseframe/weight/banner/view/BannerViewPager.java b/app/src/main/java/com/example/baseframe/weight/banner/view/BannerViewPager.java new file mode 100644 index 0000000..9cc426d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/banner/view/BannerViewPager.java @@ -0,0 +1,48 @@ +package com.example.baseframe.weight.banner.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + + +public class BannerViewPager extends ViewPager { + private boolean scrollable = true; + + public BannerViewPager(Context context) { + super(context); + } + + public BannerViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if(this.scrollable) { + if (getCurrentItem() == 0 && getChildCount() == 0) { + return false; + } + return super.onTouchEvent(ev); + } else { + return false; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if(this.scrollable) { + if (getCurrentItem() == 0 && getChildCount() == 0) { + return false; + } + return super.onInterceptTouchEvent(ev); + } else { + return false; + } + } + + public void setScrollable(boolean scrollable) { + this.scrollable = scrollable; + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/recyclerview/DividerLine.java b/app/src/main/java/com/example/baseframe/weight/recyclerview/DividerLine.java new file mode 100644 index 0000000..e0cd0bc --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/recyclerview/DividerLine.java @@ -0,0 +1,174 @@ +package com.example.baseframe.weight.recyclerview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +public class DividerLine extends RecyclerView.ItemDecoration { + private static final String TAG = DividerLine.class.getCanonicalName(); + //默认分隔线厚度为2dp + private static final int DEFAULT_DIVIDER_SIZE = 1; + //控制分隔线的属性,值为一个drawable + private static final int ATTRS[] = {android.R.attr.listDivider}; + //divider对应的drawable + private Drawable dividerDrawable; + private Context mContext; + private int dividerSize; + //默认为null + private LineDrawMode mMode = null; + + /** + * 分隔线绘制模式,水平,垂直,两者都绘制 + */ + public enum LineDrawMode { + HORIZONTAL, VERTICAL, BOTH + } + + public DividerLine(Context context) { + mContext = context; + //获取样式中对应的属性值 + TypedArray attrArray = context.obtainStyledAttributes(ATTRS); + dividerDrawable = attrArray.getDrawable(0); + attrArray.recycle(); + } + + public DividerLine(Context context, LineDrawMode mode) { + this(context); + mMode = mode; + } + + public DividerLine(Context context, int dividerSize, LineDrawMode mode) { + this(context, mode); + this.dividerSize = dividerSize; + } + + public int getDividerSize() { + return dividerSize; + } + + public void setDividerSize(int dividerSize) { + this.dividerSize = dividerSize; + } + + public LineDrawMode getMode() { + return mMode; + } + + public void setMode(LineDrawMode mode) { + mMode = mode; + } + + /** + * Item绘制完毕之后绘制分隔线 + * 根据不同的模式绘制不同的分隔线 + * + * @param c + * @param parent + * @param state + */ + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + super.onDrawOver(c, parent, state); + if (getMode() == null) { + throw new IllegalStateException("assign LineDrawMode,please!"); + } + switch (getMode()) { + case VERTICAL: + drawVertical(c, parent, state); + break; + case HORIZONTAL: + drawHorizontal(c, parent, state); + break; + case BOTH: + drawHorizontal(c, parent, state); + drawVertical(c, parent, state); + break; + } + } + + /** + * 绘制垂直分隔线 + * + * @param c + * @param parent + * @param state + */ + private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) { + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getTop() - params.topMargin; + final int bottom = child.getBottom() + params.bottomMargin; + final int left = child.getRight() + params.rightMargin; + final int right = getDividerSize() == 0 ? left + dip2px(mContext, DEFAULT_DIVIDER_SIZE) : left + getDividerSize(); + dividerDrawable.setBounds(left, top, right, bottom); + dividerDrawable.draw(c); + } + } + + /** + * 绘制水平分隔线 + * + * @param c + * @param parent + * @param state + */ + private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) { + int childCount = parent.getChildCount(); +// try { +// //水平绘制的时候查找是否存在RefreshRecyclerView +// Class viewClass = Class.forName("cn.com.gz01.smartcity.ui.widget.LoadMoreRecyclerView"); +// if (viewClass != null) { +// if (viewClass == parent.getClass()){ +// //存在这个类并使用了这个类,就去掉footer的绘制分割线 +// childCount = childCount - 1; +// } +// } +// } catch (ClassNotFoundException e) { +// KLog.e(e.getMessage()); +// } + for (int i = 0; i < childCount; i++) { + //分别为每个item绘制分隔线,首先要计算出item的边缘在哪里,给分隔线定位,定界 + final View child = parent.getChildAt(i); + //RecyclerView的LayoutManager继承自ViewGroup,支持了margin + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + //child的左边缘(也是分隔线的左边) + final int left = child.getLeft() - params.leftMargin; + //child的底边缘(恰好是分隔线的顶边) + final int top = child.getBottom() + params.topMargin; + //child的右边(也是分隔线的右边) + final int right = child.getRight() - params.rightMargin; + //分隔线的底边所在的位置(那就是分隔线的顶边加上分隔线的高度) + final int bottom = getDividerSize() == 0 ? top + dip2px(mContext, DEFAULT_DIVIDER_SIZE) : top + getDividerSize(); + dividerDrawable.setBounds(left, top, right, bottom); + //画上去 + dividerDrawable.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.bottom = getDividerSize() == 0 ? dip2px(mContext, DEFAULT_DIVIDER_SIZE) : getDividerSize(); + outRect.right = getDividerSize() == 0 ? dip2px(mContext, DEFAULT_DIVIDER_SIZE) : getDividerSize(); + } + + /** + * 将dip或dp值转换为px值,保证尺寸大小不变 + * + * @param dipValue + * @param context(DisplayMetrics类中属性density) + * @return + */ + public static int dip2px(Context context, float dipValue) { + float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } +} diff --git a/app/src/main/java/com/example/baseframe/weight/recyclerview/LayoutManagers.java b/app/src/main/java/com/example/baseframe/weight/recyclerview/LayoutManagers.java new file mode 100644 index 0000000..bf36c57 --- /dev/null +++ b/app/src/main/java/com/example/baseframe/weight/recyclerview/LayoutManagers.java @@ -0,0 +1,135 @@ +package com.example.baseframe.weight.recyclerview; + +import android.content.Context; + +import androidx.annotation.IntDef; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.chad.library.adapter.base.BaseQuickAdapter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A collection of factories to create RecyclerView LayoutManagers so that you can easily set them + * in your layout. + */ +public class LayoutManagers { + + + //Animation 默认提供5种方法(渐显、缩放、从下到上,从左到右、从右到左) + /** + * 渐显 + */ + public static final int ALPHAIN = BaseQuickAdapter.ALPHAIN; + /** + * 缩放 + */ + public static final int SCALEIN = BaseQuickAdapter.SCALEIN; + /** + * 从下到上 + */ + public static final int SLIDEIN_BOTTOM = BaseQuickAdapter.SLIDEIN_BOTTOM; + /** + * 从左到右 + */ + public static final int SLIDEIN_LEFT = BaseQuickAdapter.SLIDEIN_LEFT; + /** + * 从右到左 + */ + public static final int SLIDEIN_RIGHT = BaseQuickAdapter.SLIDEIN_RIGHT; + + + protected LayoutManagers() { + } + + public interface LayoutManagerFactory { + RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context); + } + + /** + * A {@link LinearLayoutManager}. + */ + public static LayoutManagerFactory linear() { + return new LayoutManagerFactory() { + @Override + public RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context) { + if (context == null) { + return new LinearLayoutManager(recyclerView.getContext()); + } else { + return new LinearLayoutManager(context); + } + } + }; + } + + /** + * A {@link LinearLayoutManager} with the given orientation and reverseLayout. + */ + public static LayoutManagerFactory linear(@Orientation final int orientation, final boolean reverseLayout) { + return new LayoutManagerFactory() { + @Override + public RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context) { + if (context == null) { + return new LinearLayoutManager(recyclerView.getContext(), orientation, reverseLayout); + } else { + return new LinearLayoutManager(context, orientation, reverseLayout); + } + } + }; + } + + /** + * A {@link GridLayoutManager} with the given spanCount. + */ + public static LayoutManagerFactory grid(final int spanCount) { + return new LayoutManagerFactory() { + @Override + public RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context) { + if (context == null) { + return new GridLayoutManager(recyclerView.getContext(), spanCount); + } else { + return new GridLayoutManager(context, spanCount); + } + + } + }; + } + + /** + * A {@link GridLayoutManager} with the given spanCount, orientation and reverseLayout. + **/ + public static LayoutManagerFactory grid(final int spanCount, @Orientation final int orientation, final boolean reverseLayout) { + return new LayoutManagerFactory() { + @Override + public RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context) { + if (context == null) { + return new GridLayoutManager(recyclerView.getContext(), spanCount, orientation, reverseLayout); + } else { + return new GridLayoutManager(context, spanCount, orientation, reverseLayout); + } + + } + }; + } + + /** + * A {@link StaggeredGridLayoutManager} with the given spanCount and orientation. + */ + public static LayoutManagerFactory staggeredGrid(final int spanCount, @Orientation final int orientation) { + return new LayoutManagerFactory() { + @Override + public RecyclerView.LayoutManager create(RecyclerView recyclerView, Context context) { + return new StaggeredGridLayoutManager(spanCount, orientation); + } + }; + } + + @IntDef({LinearLayoutManager.HORIZONTAL, LinearLayoutManager.VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Orientation { + } +} diff --git a/app/src/main/java/com/example/baseframe/widget/ControlDistributeLinearLayout.java b/app/src/main/java/com/example/baseframe/widget/ControlDistributeLinearLayout.java new file mode 100644 index 0000000..7a74c7d --- /dev/null +++ b/app/src/main/java/com/example/baseframe/widget/ControlDistributeLinearLayout.java @@ -0,0 +1,48 @@ +package com.example.baseframe.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; + +import com.example.baseframe.R; + +/** + * Created by goldze on 2017/3/16. + * 控制事件分发的LinearLayout + */ +public class ControlDistributeLinearLayout extends LinearLayout { + //默认是不拦截事件,分发事件给子View + private boolean isDistributeEvent = false; + + public ControlDistributeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ControlDistributeLinearLayout); + isDistributeEvent = typedArray.getBoolean(R.styleable.ControlDistributeLinearLayout_distribute_event, false); + } + + public ControlDistributeLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ControlDistributeLinearLayout(Context context) { + this(context, null); + } + + /** + * 重写事件分发方法,false 为分发 , true 为父控件自己消耗, 由外面传进来的参数决定 + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return isDistributeEvent(); + } + + public boolean isDistributeEvent() { + return isDistributeEvent; + } + + public void setDistributeEvent(boolean distributeEvent) { + isDistributeEvent = distributeEvent; + } +} diff --git a/app/src/main/res/drawable-mdpi/customactivityoncrash_error_image.png b/app/src/main/res/drawable-mdpi/customactivityoncrash_error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..465cec6196b41fd393d3fca3e8a69a971c2c5b47 GIT binary patch literal 4672 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v24/customactivityoncrash_error_image.png b/app/src/main/res/drawable-v24/customactivityoncrash_error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..7bc4005aa2a1bd7645d41ccb72ce7b87da36b992 GIT binary patch literal 7790 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/customactivityoncrash_error_image.png b/app/src/main/res/drawable-xhdpi/customactivityoncrash_error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..5f9a26043fef977b72f11ad274c3d22bb686f050 GIT binary patch literal 11080 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 W=L(H22HAhVNabZzq{}6Z1O5-BkjpIq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/login_clear_input.xml b/app/src/main/res/drawable-xhdpi/login_clear_input.xml new file mode 100644 index 0000000..a940941 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/login_clear_input.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-xxhdpi/actionbar_more.png b/app/src/main/res/drawable-xxhdpi/actionbar_more.png new file mode 100644 index 0000000000000000000000000000000000000000..78a25c36cf4fb861a6ad568f418ceb5448e37dd7 GIT binary patch literal 222 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/home_arrow_right_gray.png b/app/src/main/res/drawable-xxhdpi/home_arrow_right_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..c995535c07f9220d10e23aa6aa330d0ef1573992 GIT binary patch literal 1107 zcmb7@{WIGK9LGQ3}=?*$|OoFDNSu<^tO38TC5iB=seUzl8Z|@ z-PzVH%}E>G8B6dA)!6JluWmUUkXb z1gfK#BLGl2?D!9e`4IBixVH4QkG$Oa{v7W7X86R!$a@Wq;~unZV_!A z560SYj$NbU*G@-+*}U_8tl;q$VbYPMiP|a5l+}>S8Te_eXo#8DV;MBI1th)<>0C+Z zCv*EN>bn5{k~l&$^2@AA09Q&`ib{Wg(I`A4#tmS_hZZV#05CgUk<6gLFQbf3=8=G< zAg}0}nqbqq6PKFG=(yeg#dK9nbo5Tp&Fl}Ytss-j8EVg(kTma{O5-4Na$RV;-wGlc z_eOC*N4Wt1_U{MGABmAsr^c+EN zv=!mw3_&^I<8jF-&|3Tg?E(9hMz;fW#Dz{FNTWLd`pSjAhaink1@sA4PXXay*g@bo z`v~>|EyAyn1VTOb6!=R)Bo5$Z@i@`Fg|#5WyG!!~8y+Wh0mCbw{#Xx?SX95y1+sLH z)2Rq}9cT>l1=eKF=lf7d-^`YhNKjIln{kAQn!NAV^eC($%}>S0V9lX91qDxNjwgsU zN1(t4P3>^x*5ip6XMv4s1WOfRQiY7$Hn)GWNS6$`_sXY=g{QjbpPygdc#Y`|em#dp z7j&}A-HMb3vt-MbCDOgykHYbMqgt*0b75gY+mSeRuKR}3I6XA%vN3OQ^%Yg=7c-$y zF*{(TtZtSMmYOBsCJr-=uWUT!%2=8j9Zw1rY5Fb~G?)jrY=?`jR~b(H2KV0D%+UQg zvI=8Y*mv5ATfi55UfAdgKD`o-BnE_*ls6TLCF^+Z%b#;^l&6al}C!elN}lGCOlG6hFw8zLQKFqR0)el z=SSOG_MNJ`zx^@&a=weLwZAP6eGkEQt3BCVP}L}0#}Ry|t%%$LG}~rxT^L|P)&sF5 zSnk#0?#_JE9MdPz3zkcnUcv4}h%=uTgUaYp{T0zzV^I^s*~=bwe3Mw5xffKY_6%Vl zk{?i(6ZB9P5M(INfQZSp_#-r1*>V$EbC02oOH4BS@(!=zS%-8x1Q%Ucmqir>N=$Cl z>m*GNL#|5eb?Y`{XXTcN2OGqG;2h(| JcOK0W{sVdr$Q%Fw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/icon_back.png b/app/src/main/res/drawable-xxhdpi/icon_back.png new file mode 100644 index 0000000000000000000000000000000000000000..9056978e9a5a4e7a15788696aff4137a4cd183b2 GIT binary patch literal 887 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`ol6-Q7+nHDV6f(t z1o;Isct3m|9QO17o47yE(*OSOD){%+t@4vy%XTO zm&JMZpB3gmdhF7U6$_{Lwc1w|dcJ(+|HSegi|$4a#XAT1BrXUXzA3*&Yl->9&n|VL z*^yRC7sVb=s*mz`8a>%l<^wPu%RF5iLn02py>1@PbxF>i^vi0%j%{@X5!aNtxNyuoPv6`qcCt^;i(Bpzhn%6wj zAEXqonLNM2I5J^w5~ttJLsnNcEiQ*%TRG+7l&&?wGa8L&r_YTM^HV!(wNhoyPL|h4 zk_(x@2`WWB*=hk7?12mt^Nvo3%5%yw}tjB&=|C>myc?xT+tTaZ6@* zeP1EEUS&~*f7Hi$yVqCf-woXpo_MtSa@TdC&^>J(@=tb0?b)W}^y^1XouqxW@_+OD Y=MoIk4o_L}5E%0ep00i_>zopr0B@U04*&oF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/indicator_select.png b/app/src/main/res/drawable-xxhdpi/indicator_select.png new file mode 100644 index 0000000000000000000000000000000000000000..94336b50557c86d3ad7525f88933a5b4a9fdbe06 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^l0eMI!3HF6S#5U!Qk(@Ik;M!Q0<%Gwv7~d3H&Aef zr;B5V#p%6M9C;5L@VLe|DF`qr3UDnHU|Sl%mY5vC#@*uZLf}tqmgeoMnPT5P_dR)_ zuKka1(vJi-wFMunwF{VE-CnJ-T4`k_&*~*9d#evF+ZZA|bIHl})t-8vRFD6iY&%nF z!dz9$BBqbo%Z~>)FJZEo!{V=SQ>9$}gTe~DWM4fL_t%L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/indicator_unselect.png b/app/src/main/res/drawable-xxhdpi/indicator_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..d11af9bcbd7a1319856ce80fa71e1b606525d885 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2@)1s;*b3=9IZL71_mbB;Gqu*}oN zF+}71+)07FhYfgG*E;J+ZAE=S!?MEwbvCXncL=>dduIIU=O`j!DhvAVEaW+#t5kvUXQ!# p@2m187nl3?w9W4Bci1KMrXsv6Q0}qx3!n=aJYD@<);T3K0RYZEMCkwk literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/no_banner.png b/app/src/main/res/drawable-xxhdpi/no_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a4566bff823439d91dac0c8fd9d3954887880b GIT binary patch literal 2545 zcmb7`do&Rz-%tH6~NxlVC~$k+CtT~XWOk^b@C(P8n4L9w9*(p*vk8 zgTh0-LW4q*VqS*Yt^^%Jx%(jd`ufCT@#N&>@bK`&!~}=K85tQ7iA4SV{X(Je{rmT9 zHhXAj=);E(d_I44baZ@t90b9!u`z)_z~ypzJl?>-z~JEE)YKGw*L1Lgf{LxHv!l;> zwupN*mJL^MIN^HRY|`Y&lAQcjx6jZa;)eg)2}+V>vbt;dce%OEnXu%eDfJ4tlsu{- zUFD0?`C|Rf{~Y8|)Bc#_e*uPc+Sg@2CHQlof}^W|vfVbGj?84kSB%s*rI!huX;DD9 zU^6JLVDzZc>nk2+XHl0k_H$1nm`FI@GjcbeyTyI-Nmj$dgL3{3KzBVj#!7fRy1&Qz zxdXE|pAq|nN_rVEf6VJbwGEZ^v@uQk?ihCi*i){TUY8=RkF!@SFjUh4u^tCWMZG5G z_0%*S9G&)`1W^}f*5T$x=TW4wRt{_*Rq$hp^#X{yrUTlP__3e{%H7M-?d5nP|3Kwt z@MBR+T;};TP}7OZrA`&;TF|Cw!vw~-l1ene)v};?R8tu-o>iA7y-sHxA-{AI=fer- zLP8F0?1aR5EgyRyohn9jDUdk+?9p_)p~^}UtlvpF|!U6#V=l|(rXN(zAlun?fp#`6~s))`dN2DU?O zNdc1go~?LX8?>|@Cj`LmiATb0{|?UKV7tSq?1NB|u9tlxqIhQFg)El1w(bJxz09;5 z%$W#mymQ}?sffEv3@?GmiyBxD%QS*A@G&f`OSIRU#hYAM=}Q+BOyrPm62Y2PZAoE>T?;Ah#YL6zR*#IWA} znI$Dk03Dl;CXf!wqA&X5Xv9byL}uo9Kh?A#x$EFu00^s+ZaJs+1C1B~&XiN~leYQO zv3Y2lw@6g<@ha6HXv<2VaV?6py6$z=6{#W}|GgP--yyJA3-qL#TmI^PkEXBOiIch& zfI|Ft!E{H+IN~d4M;S-3_!kdPlsuu^o~}qI+0||YRl$w@DdDOhZ*pe-3czWeMJhvC zUiVjjOl2Q(WWIa@50}Uc0A34Bl-L2OfK{;sreCoKDw>?F7gTnM`26GemGZ?kb5r}O zj%QWOAbE^;rJvsTSHDXh6|;I1wu=Oi!^CJ+=}KdNUmh`ziTNA7RWLFCRSU$uzqCbtLJ zMMf#cRE9|HG!osy!M54dYg3wldB;V$sd!PDqQ)b12ghODu`@6YDLK36BrK@*@q@^o zA5D+f`k(I9F=<~YK}}M9oVh~DE{p2UXRTAfhbib3Y=2b2b)NZ?&KY!chyLXiiysCIXa zPzS2Y?&S!H0pW$1B z&QGV9RLtwY;)4(x1Zgp|_#W$mCU6KF3+YaLqi5uYox4j3FaNNdJ{3mUc1d2PS5uRG z#%JG#Y``TVq>rlY6O(fiw#<$$a7H_mKOY)b`X+Z>*D%=87-_Fo;_m0tMjcSZf3Q%q077kaw+?*EJHUN5 zzmziTF6QmgD`&-6(P;x~AUETJa=Zx}v_0qB%8&bQG&e-L3ZH)s@ZEf5T=UfGI*!jC zTmyqr-z1o^P2OE5X%@_q(HU+;v+c^vyo6uOw1gsU{OM%8&fUGQdw5CX(H6Yc<(gO8 zN30x=qhNs2Z|a}cXCTpX_{#q)Mg0E&2K;;GFGW9>`d6T2^|`IzBmM{u1Ny7hD(LR3 Wy%Vy$GkRq_Q*cFkI9EG;pYjjBNfO}z literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/customactivityoncrash_error_image.png b/app/src/main/res/drawable-xxxhdpi/customactivityoncrash_error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1108f7f7c7d5e93b8be41e560ed05d74214915 GIT binary patch literal 27398 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 XYd125vqDiZ00000NkvXXu0mjf&a`ym literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/black_background.xml b/app/src/main/res/drawable/black_background.xml new file mode 100644 index 0000000..205c376 --- /dev/null +++ b/app/src/main/res/drawable/black_background.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_shape_bgs.xml b/app/src/main/res/drawable/dialog_shape_bgs.xml new file mode 100644 index 0000000..9796ee0 --- /dev/null +++ b/app/src/main/res/drawable/dialog_shape_bgs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gray_radius.xml b/app/src/main/res/drawable/gray_radius.xml new file mode 100644 index 0000000..e179aca --- /dev/null +++ b/app/src/main/res/drawable/gray_radius.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/pb_notify_custom.xml b/app/src/main/res/drawable/pb_notify_custom.xml new file mode 100644 index 0000000..bdc0f38 --- /dev/null +++ b/app/src/main/res/drawable/pb_notify_custom.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/progress_large_holo.xml b/app/src/main/res/drawable/progress_large_holo.xml new file mode 100644 index 0000000..868ea8f --- /dev/null +++ b/app/src/main/res/drawable/progress_large_holo.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_bg_loading.xml b/app/src/main/res/drawable/shape_bg_loading.xml new file mode 100644 index 0000000..d2ce7b6 --- /dev/null +++ b/app/src/main/res/drawable/shape_bg_loading.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/spinner_76_inner_holo.png b/app/src/main/res/drawable/spinner_76_inner_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..299031c82cc672cf96f10169ac8d74e1aa435ebf GIT binary patch literal 4728 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/spinner_76_outer_holo.png b/app/src/main/res/drawable/spinner_76_outer_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..abfb07fb9a761bea7f438c6e6bca324d1b2335f0 GIT binary patch literal 3438 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/toast_bg.png b/app/src/main/res/drawable/toast_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..66e01a98aa04a4ba55c209d246007f0c78eeabba GIT binary patch literal 17740 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

    ?`tpPh!d+3j1V991E5P4+s=H0F)r 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 X^@pu&IPwwc00000NkvXXu0mjfIc+vv literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/toast_shape_bg.xml b/app/src/main/res/drawable/toast_shape_bg.xml new file mode 100644 index 0000000..b6a5519 --- /dev/null +++ b/app/src/main/res/drawable/toast_shape_bg.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/white_radius.xml b/app/src/main/res/drawable/white_radius.xml new file mode 100644 index 0000000..e2d61b4 --- /dev/null +++ b/app/src/main/res/drawable/white_radius.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_binding.xml b/app/src/main/res/layout/activity_binding.xml new file mode 100644 index 0000000..0396a64 --- /dev/null +++ b/app/src/main/res/layout/activity_binding.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_container.xml b/app/src/main/res/layout/activity_container.xml new file mode 100644 index 0000000..4f14776 --- /dev/null +++ b/app/src/main/res/layout/activity_container.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/activity_main_detail.xml b/app/src/main/res/layout/activity_main_detail.xml new file mode 100644 index 0000000..af6878d --- /dev/null +++ b/app/src/main/res/layout/activity_main_detail.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_new_main.xml b/app/src/main/res/layout/activity_new_main.xml new file mode 100644 index 0000000..d62848a --- /dev/null +++ b/app/src/main/res/layout/activity_new_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_tab_bar.xml b/app/src/main/res/layout/activity_tab_bar.xml new file mode 100644 index 0000000..8afcc76 --- /dev/null +++ b/app/src/main/res/layout/activity_tab_bar.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_test_weight.xml b/app/src/main/res/layout/activity_test_weight.xml new file mode 100644 index 0000000..f888605 --- /dev/null +++ b/app/src/main/res/layout/activity_test_weight.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_webview.xml b/app/src/main/res/layout/activity_webview.xml new file mode 100644 index 0000000..0b644c3 --- /dev/null +++ b/app/src/main/res/layout/activity_webview.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/banner.xml b/app/src/main/res/layout/banner.xml new file mode 100644 index 0000000..c583e27 --- /dev/null +++ b/app/src/main/res/layout/banner.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/comm_load_dialogs.xml b/app/src/main/res/layout/comm_load_dialogs.xml new file mode 100644 index 0000000..43b1f17 --- /dev/null +++ b/app/src/main/res/layout/comm_load_dialogs.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/comm_show_dialogs.xml b/app/src/main/res/layout/comm_show_dialogs.xml new file mode 100644 index 0000000..4d0e6ef --- /dev/null +++ b/app/src/main/res/layout/comm_show_dialogs.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + +